diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/00-local-key-source.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/00-local-key-source.json new file mode 100644 index 00000000..01b0f90b --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/00-local-key-source.json @@ -0,0 +1,9 @@ +{ + "source": "manual:a7m-kimi:freshhost", + "provider_id": "kimi-a7m", + "upstream_key_prefix": "sk-FKg61gc51", + "upstream_key_suffix": "w6rqf2", + "host_base": "http://127.0.0.1:18109", + "db_name": "sub2api_kimi_fresh_20260522_120829", + "redis_db": "9" +} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/01-runtime-context.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/01-runtime-context.json new file mode 100644 index 00000000..3e242482 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/01-runtime-context.json @@ -0,0 +1,9 @@ +{ + "crm_base": "http://127.0.0.1:18100", + "host_base": "http://127.0.0.1:18109", + "provider_id": "kimi-a7m", + "subscription_user_id": "2", + "subscription_user_key_prefix": "sk-177942307", + "db_name": "sub2api_kimi_fresh_20260522_120829", + "redis_db": "9" +} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/01a-create-host.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/01a-create-host.json new file mode 100644 index 00000000..5612d6f0 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/01a-create-host.json @@ -0,0 +1 @@ +{"host_id": "local-v0129-kimi-fresh-1779423075", "base_url": "http://127.0.0.1:18109", "host_version": "0.1.129", "auth_type": "bearer", "status": "unsupported", "capabilities": {"groups": true, "channels": true, "plans": true, "accounts": true, "account_test": false, "account_models": true, "subscriptions": true}} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/02-import.headers.txt b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/02-import.headers.txt new file mode 100644 index 00000000..2b2032a6 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/02-import.headers.txt @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Fri, 22 May 2026 04:27:09 GMT +Content-Length: 1001 + diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/03-import.body.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/03-import.body.json new file mode 100644 index 00000000..e53250a6 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/03-import.body.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"access_status":"subscription_ready","accounts_count":1,"batch_id":38,"batch_status":"succeeded","channel":{"id":"1","name":"Kimi A7M 默认渠道-subscription"},"gateway":{"ok":true,"status_code":200,"models":["kimi-k2.6"],"has_expected_model":true,"completion_ok":true,"completion_status":200,"completion_content_type":"application/json","completion_body_preview":"{\"id\":\"msg_e428d081-aeac-4f7d-be2a-7c6caa19d737\",\"model\":\"kimi-k2.6\",\"object\":\"chat.completion\",\"created\":1779424029,\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"Pong! 🏓\\n\\nI'm\"},\"finish_reason\":\"length\"}],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":8,\"total_tokens\":18,\"prompt_tokens_details\":{\"cached_tokens\":0,\"text_tokens\":0,\"audio_tokens\":0,\"image_tokens\":0},\"completion"},"group":{"id":"2","name":"Kimi A7M 默认分组-subscription"},"plan":{"id":"1","name":"Kimi A7M 默认套餐-subscription"},"provider_status":"active"} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/04-batch-detail-initial.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/04-batch-detail-initial.json new file mode 100644 index 00000000..aab78728 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/04-batch-detail-initial.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":43,"BatchID":38,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_e428d081-aeac-4f7d-be2a-7c6caa19d737\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779424029,\\\"choices\\\":[{\\\"index\\\":0,\\\"message\\\":{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"Pong! 🏓\\\\n\\\\nI'm\\\"},\\\"finish_reason\\\":\\\"length\\\"}],\\\"usage\\\":{\\\"prompt_tokens\\\":10,\\\"completion_tokens\\\":8,\\\"total_tokens\\\":18,\\\"prompt_tokens_details\\\":{\\\"cached_tokens\\\":0,\\\"text_tokens\\\":0,\\\"audio_tokens\\\":0,\\\"image_tokens\\\":0},\\\"completion\",\"completion_status\":200,\"completion_type\":\"application/json\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":3,"id":38,"mode":"partial","pack_id":38,"provider_id":48},"items":[{"account_status":"warning","batch_id":38,"id":38,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"4\",\"models\":[{\"id\":\"kimi-k2.6\",\"display_name\":\"kimi-k2.6\",\"type\":\"model\"}],\"probe_advisory\":true,\"probe_message\":\"API returned 403: Forbidden\",\"probe_ok\":false,\"probe_status\":\"failed\",\"smoke_model_seen\":true,\"validation_status\":\"warning\"}"}],"items_count":1,"managed_count":1,"managed_resources":[{"ID":65,"BatchID":38,"HostID":3,"ResourceType":"account","HostResourceID":"4","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/05-subscription-access-prep.sql b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/05-subscription-access-prep.sql new file mode 100644 index 00000000..b4cb4006 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/05-subscription-access-prep.sql @@ -0,0 +1,25 @@ +BEGIN; +UPDATE users +SET balance = CASE WHEN balance < 10 THEN 10 ELSE balance END, + updated_at = now() +WHERE id = 2; +UPDATE api_keys +SET group_id = 2, + updated_at = now() +WHERE key = 'sk-1779423075-ae5f2b09'; +INSERT INTO user_subscriptions ( + user_id, group_id, starts_at, expires_at, status, assigned_by, assigned_at, notes, created_at, updated_at, deleted_at +) VALUES ( + 2, 2, now(), now() + interval '30 days', 'active', 1, now(), 'local v0.1.129 freshhost kimi validation', now(), now(), NULL +) +ON CONFLICT (user_id, group_id) WHERE deleted_at IS NULL +DO UPDATE SET + starts_at = EXCLUDED.starts_at, + expires_at = EXCLUDED.expires_at, + status = 'active', + assigned_by = EXCLUDED.assigned_by, + assigned_at = EXCLUDED.assigned_at, + notes = EXCLUDED.notes, + updated_at = now(), + deleted_at = NULL; +COMMIT; diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/06-subscription-access-prep.psql.txt b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/06-subscription-access-prep.psql.txt new file mode 100644 index 00000000..894b2e3a --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/06-subscription-access-prep.psql.txt @@ -0,0 +1,5 @@ +BEGIN +UPDATE 1 +UPDATE 1 +INSERT 0 1 +COMMIT diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/07-redis-targeted-invalidation.txt b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/07-redis-targeted-invalidation.txt new file mode 100644 index 00000000..e64b7ba7 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/07-redis-targeted-invalidation.txt @@ -0,0 +1,4 @@ +auth_cache_key=apikey:auth:fc26062e46b5878e43d427af92c522baefef87a59512163b32da3189005b523a +balance_cache_key=billing:balance:2 +subscription_cache_key=billing:sub:2:2 +0 diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/08-subscription-group-state.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/08-subscription-group-state.json new file mode 100644 index 00000000..94d70fe3 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/08-subscription-group-state.json @@ -0,0 +1 @@ +{"group_id" : 2, "group" : {"id":2,"name":"Kimi A7M 默认分组-subscription","description":"","rate_multiplier":1.0000,"is_exclusive":false,"status":"active","created_at":"2026-05-22T04:11:15.989375+00:00","updated_at":"2026-05-22T04:11:15.989375+00:00","deleted_at":null,"platform":"openai","subscription_type":"subscription","daily_limit_usd":null,"weekly_limit_usd":null,"monthly_limit_usd":null,"default_validity_days":0,"image_price_1k":null,"image_price_2k":null,"image_price_4k":null,"claude_code_only":false,"fallback_group_id":null,"model_routing":{},"model_routing_enabled":false,"fallback_group_id_on_invalid_request":null,"mcp_xml_inject":true,"supported_model_scopes":null,"sort_order":0,"allow_messages_dispatch":false,"default_mapped_model":"","require_oauth_only":false,"require_privacy_set":false,"messages_dispatch_model_config":{},"rpm_limit":0,"allow_image_generation":false,"image_rate_independent":false,"image_rate_multiplier":1.0000}, "subscription" : {"id":2,"user_id":2,"group_id":2,"starts_at":"2026-05-22T04:27:09.353018+00:00","expires_at":"2026-06-21T04:27:09.353018+00:00","status":"active","daily_window_start":"2026-05-21T16:00:00+00:00","weekly_window_start":"2026-05-21T16:00:00+00:00","monthly_window_start":"2026-05-21T16:00:00+00:00","daily_usage_usd":0.0000000000,"weekly_usage_usd":0.0000000000,"monthly_usage_usd":0.0000000000,"assigned_by":1,"assigned_at":"2026-05-22T04:27:09.353018+00:00","notes":"local v0.1.129 freshhost kimi validation","created_at":"2026-05-22T04:11:17.172098+00:00","updated_at":"2026-05-22T04:27:09.353018+00:00","deleted_at":null}, "key" : {"id":1,"user_id":2,"key":"sk-1779423075-ae5f2b09","name":"relay-sub-1779423075-ae5f2b09-key","group_id":2,"status":"active","created_at":"2026-05-22T04:11:15.624575+00:00","updated_at":"2026-05-22T04:27:09.353018+00:00","deleted_at":null,"ip_whitelist":null,"ip_blacklist":null,"quota":0.00000000,"quota_used":0.00000000,"expires_at":null,"last_used_at":"2026-05-22T04:22:18.019347+00:00","rate_limit_5h":0.00000000,"rate_limit_1d":0.00000000,"rate_limit_7d":0.00000000,"usage_5h":0.00000000,"usage_1d":0.00000000,"usage_7d":0.00000000,"window_5h_start":null,"window_1d_start":null,"window_7d_start":null}} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/09-models.headers.txt b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/09-models.headers.txt new file mode 100644 index 00000000..6c0299fb --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/09-models.headers.txt @@ -0,0 +1,9 @@ +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 +Referrer-Policy: strict-origin-when-cross-origin +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-Request-Id: 3f344999-9686-4357-8673-7b56a40efb74 +Date: Fri, 22 May 2026 04:27:09 GMT +Content-Length: 123 + diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/10-models.body.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/10-models.body.json new file mode 100644 index 00000000..b9de987c --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/10-models.body.json @@ -0,0 +1 @@ +{"data":[{"id":"kimi-k2.6","type":"model","display_name":"kimi-k2.6","created_at":"2024-01-01T00:00:00Z"}],"object":"list"} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/11-chat.headers.txt b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/11-chat.headers.txt new file mode 100644 index 00000000..87bb4e8c --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/11-chat.headers.txt @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Fri, 22 May 2026 04:27:10 GMT +Referrer-Policy: strict-origin-when-cross-origin +Vary: Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-Request-Id: b03fe0d7-0318-4d2e-9606-b0f9e1006a96 +Content-Length: 611 + diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/12-chat.body.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/12-chat.body.json new file mode 100644 index 00000000..d668b3da --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/12-chat.body.json @@ -0,0 +1 @@ +{"id":"msg_0aec7be5-3b0c-459c-a4a6-f5e95671ad48","model":"kimi-k2.6","object":"chat.completion","created":1779424030,"choices":[{"index":0,"message":{"role":"assistant","content":"Pong! 🏓\n\nI'm"},"finish_reason":"length"}],"usage":{"prompt_tokens":10,"completion_tokens":8,"total_tokens":18,"prompt_tokens_details":{"cached_tokens":0,"text_tokens":0,"audio_tokens":0,"image_tokens":0},"completion_tokens_details":{"text_tokens":0,"audio_tokens":0,"reasoning_tokens":0},"input_tokens":0,"output_tokens":0,"input_tokens_details":null,"claude_cache_creation_5_m_tokens":0,"claude_cache_creation_1_h_tokens":0}} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/13-provider-status.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/13-provider-status.json new file mode 100644 index 00000000..f8253a26 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/13-provider-status.json @@ -0,0 +1 @@ +{"access_closures_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","id":38,"mode":"partial"},"host":{"base_url":"http://127.0.0.1:18109","host_id":"local-v0129-kimi-fresh-1779423075","host_version":"0.1.129"},"latest_access_status":"subscription_ready","latest_reconcile_status":"not_run","latest_reconcile_summary":{},"managed_resources_count":1,"pack":{"pack_id":"openai-cn-pack-kimi-a7m","version":"1.0.0"},"provider":{"display_name":"Kimi A7M OpenAI Compatible","platform":"openai","provider_id":"kimi-a7m"},"provider_status":"active","reconcile_runs_count":0} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/14-access-status.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/14-access-status.json new file mode 100644 index 00000000..1c1f93f1 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/14-access-status.json @@ -0,0 +1 @@ +{"batch_access_status":"subscription_ready","batch_id":38,"closures_count":1,"latest_access_status":"subscription_ready","latest_closure":{"closure_type":"subscription","details_json":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_e428d081-aeac-4f7d-be2a-7c6caa19d737\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779424029,\\\"choices\\\":[{\\\"index\\\":0,\\\"message\\\":{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"Pong! 🏓\\\\n\\\\nI'm\\\"},\\\"finish_reason\\\":\\\"length\\\"}],\\\"usage\\\":{\\\"prompt_tokens\\\":10,\\\"completion_tokens\\\":8,\\\"total_tokens\\\":18,\\\"prompt_tokens_details\\\":{\\\"cached_tokens\\\":0,\\\"text_tokens\\\":0,\\\"audio_tokens\\\":0,\\\"image_tokens\\\":0},\\\"completion\",\"completion_status\":200,\"completion_type\":\"application/json\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200}","id":43,"status":"subscription_ready"},"pack_id":"openai-cn-pack-kimi-a7m","provider_id":"kimi-a7m"} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/15-access-preview.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/15-access-preview.json new file mode 100644 index 00000000..56e78371 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/15-access-preview.json @@ -0,0 +1 @@ +{"provider_id":"kimi-a7m","mode":"subscription","available":true,"message":"latest access status: subscription_ready"} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/16-batch-detail-final.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/16-batch-detail-final.json new file mode 100644 index 00000000..aab78728 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/16-batch-detail-final.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":43,"BatchID":38,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_e428d081-aeac-4f7d-be2a-7c6caa19d737\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779424029,\\\"choices\\\":[{\\\"index\\\":0,\\\"message\\\":{\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"Pong! 🏓\\\\n\\\\nI'm\\\"},\\\"finish_reason\\\":\\\"length\\\"}],\\\"usage\\\":{\\\"prompt_tokens\\\":10,\\\"completion_tokens\\\":8,\\\"total_tokens\\\":18,\\\"prompt_tokens_details\\\":{\\\"cached_tokens\\\":0,\\\"text_tokens\\\":0,\\\"audio_tokens\\\":0,\\\"image_tokens\\\":0},\\\"completion\",\"completion_status\":200,\"completion_type\":\"application/json\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":3,"id":38,"mode":"partial","pack_id":38,"provider_id":48},"items":[{"account_status":"warning","batch_id":38,"id":38,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"4\",\"models\":[{\"id\":\"kimi-k2.6\",\"display_name\":\"kimi-k2.6\",\"type\":\"model\"}],\"probe_advisory\":true,\"probe_message\":\"API returned 403: Forbidden\",\"probe_ok\":false,\"probe_status\":\"failed\",\"smoke_model_seen\":true,\"validation_status\":\"warning\"}"}],"items_count":1,"managed_count":1,"managed_resources":[{"ID":65,"BatchID":38,"HostID":3,"ResourceType":"account","HostResourceID":"4","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/17-upstream-models.headers.txt b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/17-upstream-models.headers.txt new file mode 100644 index 00000000..8f691726 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/17-upstream-models.headers.txt @@ -0,0 +1,11 @@ +HTTP/1.1 200 Connection established + +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json; charset=utf-8 +date: Fri, 22 May 2026 04:27:10 GMT +via: 1.1 Caddy +x-new-api-version: v0.0.0 +x-oneapi-request-id: 20260522042710560170306YIKBSr5Q +content-length: 168 + diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/18-upstream-models.body.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/18-upstream-models.body.json new file mode 100644 index 00000000..8369a64f --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/18-upstream-models.body.json @@ -0,0 +1 @@ +{"data":[{"id":"kimi-k2.6","object":"model","created":1626777600,"owned_by":"custom","supported_endpoint_types":["anthropic","openai"]}],"object":"list","success":true} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/19-upstream-chat.headers.txt b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/19-upstream-chat.headers.txt new file mode 100644 index 00000000..bb1dcd70 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/19-upstream-chat.headers.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 Connection established + +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json +date: Fri, 22 May 2026 04:27:11 GMT +req-arrive-time: 1779424031233 +req-cost-time: 816 +resp-start-time: 1779424032049 +server: istio-envoy +set-cookie: acw_tc=180dd8dd17794240312333635e2900ebe766d23471b5c1c302fd300fee23fb;path=/;HttpOnly;Max-Age=1800 +via: 1.1 Caddy +x-envoy-upstream-service-time: 812 +x-new-api-version: v0.0.0 +x-oneapi-request-id: 20260522042710706076631DSLRiX1b +content-length: 611 + diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/20-upstream-chat.body.txt b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/20-upstream-chat.body.txt new file mode 100644 index 00000000..568634fe --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/20-upstream-chat.body.txt @@ -0,0 +1 @@ +{"id":"msg_b392a616-1575-4d7b-a4d6-7be85de3408c","model":"kimi-k2.6","object":"chat.completion","created":1779424032,"choices":[{"index":0,"message":{"role":"assistant","content":"Pong! 🏓\n\nI'm"},"finish_reason":"length"}],"usage":{"prompt_tokens":10,"completion_tokens":8,"total_tokens":18,"prompt_tokens_details":{"cached_tokens":0,"text_tokens":0,"audio_tokens":0,"image_tokens":0},"completion_tokens_details":{"text_tokens":0,"audio_tokens":0,"reasoning_tokens":0},"input_tokens":0,"output_tokens":0,"input_tokens_details":null,"claude_cache_creation_5_m_tokens":0,"claude_cache_creation_1_h_tokens":0}} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/21-summary.json b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/21-summary.json new file mode 100644 index 00000000..d5e573c9 --- /dev/null +++ b/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/21-summary.json @@ -0,0 +1,28 @@ +{ + "artifact_dir": "/home/long/project/sub2api-cn-relay-manager/artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost", + "provider_id": "kimi-a7m", + "model_name": "kimi-k2.6", + "host_id": "local-v0129-kimi-fresh-1779423075", + "host_version_from_create_host": "0.1.129", + "batch_id": 38, + "batch_status": "succeeded", + "access_status_from_import": "subscription_ready", + "provider_status_from_import": "active", + "provider_status_latest": "active", + "latest_access_status": "subscription_ready", + "preview_available": true, + "direct_models_http200": true, + "direct_models_has_expected_model": true, + "direct_models": [ + "kimi-k2.6" + ], + "direct_chat_http200": true, + "direct_chat_status": 200, + "upstream_models_http200": true, + "upstream_models_has_expected_model": true, + "upstream_models": [ + "kimi-k2.6" + ], + "upstream_chat_status": 200, + "account_probe_summary": "{\"account_id\":\"4\",\"models\":[{\"id\":\"kimi-k2.6\",\"display_name\":\"kimi-k2.6\",\"type\":\"model\"}],\"probe_advisory\":true,\"probe_message\":\"API returned 403: Forbidden\",\"probe_ok\":false,\"probe_status\":\"failed\",\"smoke_model_seen\":true,\"validation_status\":\"warning\"}" +} diff --git a/docs/EXECUTION_BOARD.md b/docs/EXECUTION_BOARD.md index c4e19501..35e5eee5 100644 --- a/docs/EXECUTION_BOARD.md +++ b/docs/EXECUTION_BOARD.md @@ -11,6 +11,7 @@ - `subscription` 主链路已通过 latest fresh-host 复验: - MiniMax 53hk:`artifacts/real-host-acceptance/20260521_191418_remote43_minimax_key_import/21-summary.json` - DeepSeek 2166:`artifacts/real-host-acceptance/20260521_201509_remote43_deepseek_key_import/21-summary.json` + - Kimi A7M(local host `v0.1.129`):`artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost/21-summary.json` - `self_service` 主链路已通过 latest-head 标准 fresh-host 复验: - `artifacts/real-host-acceptance/20260521_210403/05-import.json` - `artifacts/real-host-acceptance/20260521_210403/07-access-status.json` @@ -64,6 +65,10 @@ - 主目录 `artifacts/real-host-acceptance/` 当前只保留最终证据 - 历史失败/半成功/试错样本已迁到 `artifacts/real-host-acceptance-archive/` - 分类规则见:`docs/REAL_HOST_ARTIFACT_RETENTION.md` +13. relay-manager latest-head 已收口 Kimi A7M 两段竞态 + - account test 首次 `403 Forbidden` 已降级为 advisory warning;只要 `/models` 已命中 `smoke_test_model`,不会再把 batch 误判为 blocking failure + - access closure 对导入后瞬时 `503 / no available accounts` 增加短暂 completion retry,避免宿主异步 probe / account warm-up 窗口把真实可用链路误记成 `broken` + - `20260522_122706_local_v0129_kimi_a7m_subscription_freshhost` 已证明:在修复后的 relay-manager + patched host 组合下,`kimi-a7m / kimi-k2.6` 可落到 `batch_status=succeeded`、`provider_status=active`、`latest_access_status=subscription_ready` ## 已验证门禁 @@ -106,6 +111,11 @@ - official MiniMax 模板 live 样本 - 模板链路打通,但当前验证 key 命中 upstream `429` +7. `artifacts/real-host-acceptance/20260522_122706_local_v0129_kimi_a7m_subscription_freshhost` + - latest-head relay-manager 对 patched host `v0.1.129` 的 Kimi A7M `subscription` 最终成功样本 + - `21-summary.json` 已到 `batch_status=succeeded`、`provider_status=active` + - `account_probe_summary` 明确记录 `probe_advisory=true`、`validation_status=warning`,证明 403 probe race 已被 relay-manager 正确降级 + ## 剩余项(P2 / 运营前置,不阻塞按 PRD 首版范围上线) 1. 运营前置 diff --git a/internal/access/closure.go b/internal/access/closure.go index 4142fc6d..be469515 100644 --- a/internal/access/closure.go +++ b/internal/access/closure.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "sub2api-cn-relay-manager/internal/host/sub2api" ) @@ -11,6 +12,9 @@ import ( const ( ModeSubscription = "subscription" ModeSelfService = "self_service" + + gatewayCompletionRetryAttempts = 3 + gatewayCompletionRetryDelay = 300 * time.Millisecond ) type SubscriptionTarget struct { @@ -96,7 +100,7 @@ func (s *Service) Close(ctx context.Context, req ClosureRequest) (sub2api.Gatewa return sub2api.GatewayAccessResult{}, fmt.Errorf("check gateway access: %w", err) } if result.OK && result.HasExpectedModel && strings.TrimSpace(req.ExpectedModel) != "" { - completion, err := s.host.CheckGatewayCompletion(ctx, sub2api.GatewayCompletionCheckRequest{ + completion, err := s.checkGatewayCompletionWithRetry(ctx, sub2api.GatewayCompletionCheckRequest{ APIKey: probeAPIKey, Model: req.ExpectedModel, Prompt: req.Prompt, @@ -112,3 +116,39 @@ func (s *Service) Close(ctx context.Context, req ClosureRequest) (sub2api.Gatewa } return result, nil } + +func (s *Service) checkGatewayCompletionWithRetry(ctx context.Context, req sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error) { + var last sub2api.GatewayCompletionResult + for attempt := 1; attempt <= gatewayCompletionRetryAttempts; attempt++ { + completion, err := s.host.CheckGatewayCompletion(ctx, req) + if err != nil { + return sub2api.GatewayCompletionResult{}, err + } + last = completion + if completion.OK || !isTransientGatewayCompletionFailure(completion) || attempt == gatewayCompletionRetryAttempts { + return completion, nil + } + timer := time.NewTimer(gatewayCompletionRetryDelay) + select { + case <-ctx.Done(): + timer.Stop() + return last, ctx.Err() + case <-timer.C: + } + } + return last, nil +} + +func isTransientGatewayCompletionFailure(result sub2api.GatewayCompletionResult) bool { + if result.OK { + return false + } + if result.StatusCode != 0 && result.StatusCode != 429 && result.StatusCode != 502 && result.StatusCode != 503 && result.StatusCode != 504 { + return false + } + body := strings.ToLower(strings.TrimSpace(result.BodyPreview)) + return strings.Contains(body, "service temporarily unavailable") || + strings.Contains(body, "no available accounts") || + strings.Contains(body, "temporar") || + strings.Contains(body, "try again") +} diff --git a/internal/access/closure_test.go b/internal/access/closure_test.go index cedf180a..ed0c2c8a 100644 --- a/internal/access/closure_test.go +++ b/internal/access/closure_test.go @@ -114,16 +114,47 @@ func TestServiceCloseReturnsSubscriptionErrorBeforeGatewayProbe(t *testing.T) { } } +func TestServiceCloseRetriesTransientGatewayCompletionFailure(t *testing.T) { + host := &fakeClosureHost{ + gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"kimi-k2.6"}}, + completionResults: []sub2api.GatewayCompletionResult{ + {OK: false, StatusCode: 503, ContentType: "application/json", BodyPreview: `{"error":{"message":"Service temporarily unavailable","type":"api_error"}}`}, + {OK: true, StatusCode: 200, ContentType: "application/json"}, + }, + managedAccess: map[string]sub2api.SubscriptionAccessRef{ + "user-1": {UserID: "host-user-1", APIKey: "managed-user-key"}, + }, + } + + result, err := NewService(host).Close(context.Background(), ClosureRequest{ + Mode: "subscription", + GroupID: "group-1", + ExpectedModel: "kimi-k2.6", + Subscriptions: []SubscriptionTarget{{UserID: "user-1", DurationDays: 30}}, + }) + if err != nil { + t.Fatalf("Close() error = %v", err) + } + if !result.CompletionOK || result.CompletionStatus != 200 { + t.Fatalf("completion result = %+v, want retried success", result) + } + if host.completionCalls != 2 { + t.Fatalf("completion calls = %d, want 2", host.completionCalls) + } +} + type fakeClosureHost struct { - assigned []sub2api.AssignSubscriptionRequest - managedAccess map[string]sub2api.SubscriptionAccessRef - assignErr error - gatewayProbe sub2api.GatewayAccessCheckRequest - gatewayResult sub2api.GatewayAccessResult - gatewayErr error - completionProbe sub2api.GatewayCompletionCheckRequest - completionResult sub2api.GatewayCompletionResult - completionErr error + assigned []sub2api.AssignSubscriptionRequest + managedAccess map[string]sub2api.SubscriptionAccessRef + assignErr error + gatewayProbe sub2api.GatewayAccessCheckRequest + gatewayResult sub2api.GatewayAccessResult + gatewayErr error + completionProbe sub2api.GatewayCompletionCheckRequest + completionCalls int + completionResults []sub2api.GatewayCompletionResult + completionResult sub2api.GatewayCompletionResult + completionErr error } func (f *fakeClosureHost) EnsureSubscriptionAccess(_ context.Context, req sub2api.EnsureSubscriptionAccessRequest) (sub2api.SubscriptionAccessRef, error) { @@ -151,8 +182,16 @@ func (f *fakeClosureHost) CheckGatewayAccess(_ context.Context, req sub2api.Gate func (f *fakeClosureHost) CheckGatewayCompletion(_ context.Context, req sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error) { f.completionProbe = req + f.completionCalls++ if f.completionErr != nil { return sub2api.GatewayCompletionResult{}, f.completionErr } + if len(f.completionResults) > 0 { + idx := f.completionCalls - 1 + if idx >= len(f.completionResults) { + idx = len(f.completionResults) - 1 + } + return f.completionResults[idx], nil + } return f.completionResult, nil } diff --git a/internal/provision/import_service.go b/internal/provision/import_service.go index e9918b0d..c28e85e7 100644 --- a/internal/provision/import_service.go +++ b/internal/provision/import_service.go @@ -118,6 +118,16 @@ func isAdvisoryAccountProbeFailure(probe sub2api.ProbeResult) bool { return true } + // OpenAI-compatible third-party upstreams such as Kimi/DeepSeek may + // create accounts and expose /models immediately, but the host's + // asynchronous /responses capability probe can complete slightly later. + // During that race window, the first /accounts/:id/test still takes the + // default /responses path and returns a plain 403 Forbidden even though + // the actual chat/completions route already works. + if strings.Contains(message, "api returned 403: forbidden") { + return true + } + if !strings.Contains(message, "responses api") { return false } diff --git a/internal/provision/import_service_test.go b/internal/provision/import_service_test.go index c4809cde..e71a61d5 100644 --- a/internal/provision/import_service_test.go +++ b/internal/provision/import_service_test.go @@ -244,6 +244,104 @@ func TestImportServiceTreatsTransientProbeFailureAsAdvisoryWhenGatewaySucceeds(t } } +func TestImportServiceTreatsForbiddenProbeRaceAsAdvisoryWhenGatewaySucceeds(t *testing.T) { + host := &fakeHostAdapter{ + batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}}, + testResults: map[string]sub2api.ProbeResult{ + "account_1": { + OK: false, + Status: "failed", + Message: "API returned 403: Forbidden", + }, + }, + models: map[string][]sub2api.AccountModel{ + "account_1": {{ID: "deepseek-chat"}}, + }, + gatewayResult: sub2api.GatewayAccessResult{ + OK: true, + StatusCode: 200, + HasExpectedModel: true, + Models: []string{"deepseek-chat"}, + CompletionOK: true, + CompletionStatus: 200, + CompletionType: "application/json", + }, + } + + report, err := NewImportService(host).Import(context.Background(), ImportRequest{ + Provider: sampleProviderManifest(), + Mode: ImportModePartial, + Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, + Keys: []string{"key-1"}, + }) + if err != nil { + t.Fatalf("Import() error = %v", err) + } + if report.BatchStatus != BatchStatusSucceeded { + t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded) + } + if report.ProviderStatus != ProviderStatusActive { + t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive) + } + if report.AccessStatus != AccessStatusSelfServiceReady { + t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSelfServiceReady) + } + if got := report.Accounts[0].ValidationStatus(); got != AccountStatusWarning { + t.Fatalf("ValidationStatus = %q, want %q", got, AccountStatusWarning) + } +} + +func TestImportServiceRetriesTransientGatewayCompletionFailure(t *testing.T) { + host := &fakeHostAdapter{ + batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}}, + testResults: map[string]sub2api.ProbeResult{ + "account_1": { + OK: false, + Status: "failed", + Message: "API returned 403: Forbidden", + }, + }, + models: map[string][]sub2api.AccountModel{ + "account_1": {{ID: "deepseek-chat"}}, + }, + gatewayResult: sub2api.GatewayAccessResult{ + OK: true, + StatusCode: 200, + HasExpectedModel: true, + Models: []string{"deepseek-chat"}, + }, + completionResults: []sub2api.GatewayCompletionResult{ + {OK: false, StatusCode: 503, ContentType: "application/json", BodyPreview: `{"error":{"message":"Service temporarily unavailable","type":"api_error"}}`}, + {OK: true, StatusCode: 200, ContentType: "application/json"}, + }, + } + + report, err := NewImportService(host).Import(context.Background(), ImportRequest{ + Provider: sampleProviderManifest(), + Mode: ImportModePartial, + Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, + Keys: []string{"key-1"}, + }) + if err != nil { + t.Fatalf("Import() error = %v", err) + } + if report.BatchStatus != BatchStatusSucceeded { + t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded) + } + if report.ProviderStatus != ProviderStatusActive { + t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive) + } + if report.AccessStatus != AccessStatusSelfServiceReady { + t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSelfServiceReady) + } + if !report.Gateway.CompletionOK || report.Gateway.CompletionStatus != 200 { + t.Fatalf("Gateway completion = %+v, want retried success", report.Gateway) + } + if host.completionCalls != 2 { + t.Fatalf("completion calls = %d, want 2", host.completionCalls) + } +} + func TestImportServiceStrictModeRollsBackCreatedResources(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}}, @@ -520,6 +618,8 @@ type fakeHostAdapter struct { updateChannelID string updateChannelReq sub2api.CreateChannelRequest callSequence []string + completionCalls int + completionResults []sub2api.GatewayCompletionResult completionResult sub2api.GatewayCompletionResult completionErr error testedModels map[string]string @@ -620,9 +720,17 @@ func (f *fakeHostAdapter) CheckGatewayAccess(_ context.Context, req sub2api.Gate func (f *fakeHostAdapter) CheckGatewayCompletion(_ context.Context, req sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error) { f.callSequence = append(f.callSequence, "completion") f.completionProbe = req + f.completionCalls++ if f.completionErr != nil { return sub2api.GatewayCompletionResult{}, f.completionErr } + if len(f.completionResults) > 0 { + idx := f.completionCalls - 1 + if idx >= len(f.completionResults) { + idx = len(f.completionResults) - 1 + } + return f.completionResults[idx], nil + } if f.completionResult.StatusCode == 0 && !f.completionResult.OK { return sub2api.GatewayCompletionResult{OK: true, StatusCode: 200, ContentType: "application/json"}, nil } diff --git a/internal/provision/runtime_import_service_test.go b/internal/provision/runtime_import_service_test.go index c1151fde..cc17c14c 100644 --- a/internal/provision/runtime_import_service_test.go +++ b/internal/provision/runtime_import_service_test.go @@ -236,6 +236,70 @@ func TestRuntimeImportServicePersistsWarningAccountStatusForAdvisoryProbeFailure } } +func TestRuntimeImportServicePersistsWarningAccountStatusForForbiddenProbeRace(t *testing.T) { + store := openProvisionTestStore(t) + defer closeProvisionTestStore(t, store) + + seedProvisionHost(t, store, "host-1", "https://sub2api.example.com") + + host := &fakeHostAdapter{ + batchAccounts: []sub2api.AccountRef{{ID: "account_1"}}, + testResults: map[string]sub2api.ProbeResult{ + "account_1": { + OK: false, + Status: "failed", + Message: "API returned 403: Forbidden", + }, + }, + models: map[string][]sub2api.AccountModel{ + "account_1": {{ID: "deepseek-chat"}}, + }, + gatewayResult: sub2api.GatewayAccessResult{ + OK: true, + StatusCode: 200, + HasExpectedModel: true, + Models: []string{"deepseek-chat"}, + CompletionOK: true, + CompletionStatus: 200, + }, + } + + svc := NewRuntimeImportService(store, host) + result, err := svc.Import(context.Background(), RuntimeImportRequest{ + HostID: "host-1", + HostBaseURL: "https://sub2api.example.com", + Pack: pack.LoadedPack{ + Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"}, + Checksum: "checksum-1", + }, + Provider: sampleProviderManifest(), + Mode: ImportModePartial, + Keys: []string{"key-1"}, + Access: AccessRequest{ + Mode: AccessModeSelfService, + ProbeAPIKey: "user-key", + }, + }) + if err != nil { + t.Fatalf("RuntimeImportService.Import() error = %v", err) + } + if result.Report.BatchStatus != BatchStatusSucceeded { + t.Fatalf("BatchStatus = %q, want %q", result.Report.BatchStatus, BatchStatusSucceeded) + } + + var accountStatus string + var summary string + if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT account_status, probe_summary_json FROM import_batch_items WHERE batch_id = ? ORDER BY id LIMIT 1", result.BatchID).Scan(&accountStatus, &summary); err != nil { + t.Fatalf("query import batch item: %v", err) + } + if accountStatus != AccountStatusWarning { + t.Fatalf("account_status = %q, want %q", accountStatus, AccountStatusWarning) + } + if !strings.Contains(summary, "\"probe_advisory\":true") { + t.Fatalf("probe_summary_json = %s, want probe_advisory=true", summary) + } +} + func TestRuntimeImportServicePersistsPartialManagedResourcesOnAccessFailure(t *testing.T) { store := openProvisionTestStore(t) defer closeProvisionTestStore(t, store)