From 83a05b4889995271a6b8b058c2c49c7c504651d5 Mon Sep 17 00:00:00 2001 From: phamnazage-jpg Date: Tue, 26 May 2026 07:50:43 +0800 Subject: [PATCH] feat: add kimi a7m overlay workflow and remote43 validation --- .../00-artifact-guide.txt | 36 ++ .../01-create-host.json | 1 + .../02-probe-host.json | 1 + .../03-install-pack.json | 1 + .../04-preview-import.json | 1 + .../05-import.json | 1 + .../05a-batch-detail-pre-access.json | 1 + .../06-access-preview.json | 1 + .../07-access-status.json | 1 + .../08-provider-status.json | 1 + .../09-reconcile.json | 1 + .../10-batch-detail.json | 1 + .../21-summary.json | 49 +++ .../22-patched-host-validation.json | 49 +++ .../23-sub2api-host-patch-notes.md | 24 ++ .../00-artifact-guide.txt | 34 ++ .../01-create-host.json | 1 + .../02-probe-host.json | 1 + .../03-install-pack.json | 1 + .../04-preview-import.json | 1 + .../05-import.json | 1 + .../05a-batch-detail-pre-access.json | 1 + .../06-access-preview.json | 1 + .../07-access-status.json | 1 + .../08-provider-status.json | 1 + .../09-reconcile.json | 1 + .../10-batch-detail.json | 1 + .../20-acceptance.stderr.txt | 0 .../20-acceptance.stdout.txt | 8 + .../21-summary.json | 16 + .../00-artifact-guide.txt | 34 ++ .../00-artifact-guide.txt | 34 ++ .../01-create-host.json | 1 + .../02-probe-host.json | 1 + .../03-install-pack.json | 1 + .../04-preview-import.json | 1 + .../05-import.json | 1 + .../05a-batch-detail-pre-access.json | 1 + .../06-access-preview.json | 1 + .../07-access-status.json | 1 + .../08-provider-status.json | 1 + .../09-reconcile.json | 1 + .../10-batch-detail.json | 1 + .../11-rollback.json | 1 + .../21-summary.json | 34 ++ .../00-local-key-source.json | 10 + .../01-runtime-context.json | 16 + .../01a-create-host.json | 0 .../00-local-key-source.json | 10 + .../01-runtime-context.json | 25 ++ .../01a-create-host.json | 1 + .../02-import.headers.txt | 5 + .../03-import.body.json | 1 + .../04-batch-detail-initial.json | 1 + .../05-subscription-access-prep.summary.json | 12 + .../06-subscription-access-prep.psql.txt | 5 + .../07-redis-targeted-invalidation.json | 6 + .../08-subscription-group-state.json | 28 ++ .../09-models.headers.txt | 9 + .../10-models.body.json | 1 + .../11-chat.headers.txt | 10 + .../12-chat.body.json | 1 + .../13-provider-status.json | 1 + .../14-access-status.json | 1 + .../15-access-preview.json | 1 + .../16-batch-detail-final.json | 1 + .../17-upstream-models.headers.txt | 9 + .../18-upstream-models.body.json | 1 + .../19-upstream-chat.headers.txt | 14 + .../20-upstream-chat.body.txt | 1 + .../21-summary.json | 1 + .../00-local-key-source.json | 10 + .../01-runtime-context.json | 25 ++ .../01a-create-host.json | 1 + .../02-import.headers.txt | 5 + .../03-import.body.json | 1 + .../04-batch-detail-initial.json | 1 + .../05-subscription-access-prep.summary.json | 12 + .../06-subscription-access-prep.psql.txt | 5 + .../07-redis-targeted-invalidation.json | 6 + .../08-subscription-group-state.json | 28 ++ .../09-models.headers.txt | 9 + .../10-models.body.json | 1 + .../11-chat.headers.txt | 10 + .../12-chat.body.json | 1 + .../13-provider-status.json | 0 .../14-access-status.json | 0 .../17-upstream-models.headers.txt | 9 + .../18-upstream-models.body.json | 1 + .../19-upstream-chat.headers.txt | 14 + .../20-upstream-chat.body.txt | 1 + .../00-local-key-source.json | 10 + .../01-runtime-context.json | 25 ++ .../01a-create-host.json | 1 + .../02-import.headers.txt | 5 + .../03-import.body.json | 1 + .../04-batch-detail-initial.json | 1 + .../05-subscription-access-prep.summary.json | 12 + .../06-subscription-access-prep.psql.txt | 5 + .../07-redis-targeted-invalidation.json | 6 + .../08-subscription-group-state.json | 28 ++ .../09-models.headers.txt | 9 + .../10-models.body.json | 1 + .../11-chat.headers.txt | 10 + .../12-chat.body.json | 1 + .../13-provider-status.json | 0 .../17-upstream-models.headers.txt | 9 + .../18-upstream-models.body.json | 1 + .../19-upstream-chat.headers.txt | 14 + .../20-upstream-chat.body.txt | 1 + .../00-local-key-source.json | 10 + .../01-runtime-context.json | 25 ++ .../01a-create-host.json | 1 + .../02-import.headers.txt | 5 + .../03-import.body.json | 1 + .../04-batch-detail-initial.json | 1 + .../05-subscription-access-prep.summary.json | 12 + .../06-subscription-access-prep.psql.txt | 5 + .../07-redis-targeted-invalidation.json | 6 + .../08-subscription-group-state.json | 28 ++ .../09-models.headers.txt | 9 + .../10-models.body.json | 1 + .../11-chat.headers.txt | 10 + .../12-chat.body.json | 1 + .../13-provider-status.json | 1 + .../14-access-status.json | 1 + .../15-access-preview.json | 1 + .../16-batch-detail-final.json | 1 + .../17-upstream-models.headers.txt | 9 + .../18-upstream-models.body.json | 1 + .../19-upstream-chat.headers.txt | 14 + .../20-upstream-chat.body.txt | 1 + .../21-summary.json | 1 + cmd/cli/apply_host_overlay.go | 87 +++++ cmd/cli/apply_host_overlay_test.go | 115 ++++++ cmd/cli/batch_import_test.go | 2 +- cmd/cli/main.go | 31 +- cmd/cli/main_test.go | 60 ++- docs/EXECUTION_BOARD.md | 52 ++- docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md | 48 ++- internal/access/closure_test.go | 42 ++- internal/access/openai_responses_repair.go | 19 +- .../access/openai_responses_repair_test.go | 47 ++- internal/access/types.go | 1 + internal/app/http_api.go | 41 +- .../host/sub2api/account_capability_repair.go | 24 ++ .../sub2api/account_capability_repair_test.go | 52 +++ internal/host/sub2api/client.go | 1 + internal/overlay/executor.go | 257 +++++++++++++ internal/overlay/executor_test.go | 127 +++++++ internal/pack/extra_test.go | 2 +- internal/pack/host_overlay.go | 49 +++ internal/pack/host_overlay_test.go | 35 ++ internal/pack/loader.go | 120 +++++- internal/pack/loader_test.go | 28 ++ internal/provision/import_service.go | 12 + internal/provision/import_service_test.go | 141 +++++-- internal/provision/preview_service.go | 14 +- internal/provision/preview_service_test.go | 30 ++ internal/provision/reconcile_service_test.go | 3 + internal/provision/runtime_import_service.go | 5 + .../provision/runtime_import_service_test.go | 54 +++ internal/reconcile/service.go | 2 +- internal/reconcile/service_runtime_test.go | 42 ++- packs/openai-cn-pack/README.md | 15 + packs/openai-cn-pack/checksums.txt | 5 +- .../overlays/kimi-a7m-sub2api-v0.1.129.md | 32 ++ .../overlays/kimi-a7m-sub2api-v0.1.129.patch | 356 ++++++++++++++++++ packs/openai-cn-pack/pack.json | 2 +- packs/openai-cn-pack/providers/kimi-a7m.json | 44 +++ scripts/import_remote43_provider.sh | 33 +- scripts/remote43_patched_stack_lib.sh | 246 ++++++++++++ scripts/setup_remote43_patched_stack.sh | 205 ++++++++++ scripts/test_real_host_scripts.sh | 117 +++++- 174 files changed, 3424 insertions(+), 122 deletions(-) create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/00-artifact-guide.txt create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/01-create-host.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/02-probe-host.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/03-install-pack.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/04-preview-import.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/05-import.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/05a-batch-detail-pre-access.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/06-access-preview.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/07-access-status.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/08-provider-status.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/09-reconcile.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/10-batch-detail.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/21-summary.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/22-patched-host-validation.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/23-sub2api-host-patch-notes.md create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/00-artifact-guide.txt create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/01-create-host.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/02-probe-host.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/03-install-pack.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/04-preview-import.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/05-import.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/05a-batch-detail-pre-access.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/06-access-preview.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/07-access-status.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/08-provider-status.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/09-reconcile.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/10-batch-detail.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/20-acceptance.stderr.txt create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/20-acceptance.stdout.txt create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/21-summary.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost/00-artifact-guide.txt create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/00-artifact-guide.txt create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/01-create-host.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/02-probe-host.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/03-install-pack.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/04-preview-import.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/05-import.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/05a-batch-detail-pre-access.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/06-access-preview.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/07-access-status.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/08-provider-status.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/09-reconcile.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/10-batch-detail.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/11-rollback.json create mode 100644 artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/21-summary.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/00-local-key-source.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/01-runtime-context.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/01a-create-host.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/00-local-key-source.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/01-runtime-context.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/01a-create-host.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/02-import.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/03-import.body.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/04-batch-detail-initial.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/05-subscription-access-prep.summary.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/06-subscription-access-prep.psql.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/07-redis-targeted-invalidation.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/08-subscription-group-state.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/09-models.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/10-models.body.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/11-chat.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/12-chat.body.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/13-provider-status.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/14-access-status.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/15-access-preview.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/16-batch-detail-final.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/17-upstream-models.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/18-upstream-models.body.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/19-upstream-chat.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/20-upstream-chat.body.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/21-summary.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/00-local-key-source.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/01-runtime-context.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/01a-create-host.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/02-import.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/03-import.body.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/04-batch-detail-initial.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/05-subscription-access-prep.summary.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/06-subscription-access-prep.psql.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/07-redis-targeted-invalidation.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/08-subscription-group-state.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/09-models.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/10-models.body.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/11-chat.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/12-chat.body.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/13-provider-status.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/14-access-status.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/17-upstream-models.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/18-upstream-models.body.json create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/19-upstream-chat.headers.txt create mode 100644 artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/20-upstream-chat.body.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/00-local-key-source.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/01-runtime-context.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/01a-create-host.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/02-import.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/03-import.body.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/04-batch-detail-initial.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/05-subscription-access-prep.summary.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/06-subscription-access-prep.psql.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/07-redis-targeted-invalidation.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/08-subscription-group-state.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/09-models.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/10-models.body.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/11-chat.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/12-chat.body.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/13-provider-status.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/17-upstream-models.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/18-upstream-models.body.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/19-upstream-chat.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/20-upstream-chat.body.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/00-local-key-source.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/01-runtime-context.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/01a-create-host.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/02-import.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/03-import.body.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/04-batch-detail-initial.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/05-subscription-access-prep.summary.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/06-subscription-access-prep.psql.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/07-redis-targeted-invalidation.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/08-subscription-group-state.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/09-models.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/10-models.body.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/11-chat.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/12-chat.body.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/13-provider-status.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/14-access-status.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/15-access-preview.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/16-batch-detail-final.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/17-upstream-models.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/18-upstream-models.body.json create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/19-upstream-chat.headers.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/20-upstream-chat.body.txt create mode 100644 artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/21-summary.json create mode 100644 cmd/cli/apply_host_overlay.go create mode 100644 cmd/cli/apply_host_overlay_test.go create mode 100644 internal/overlay/executor.go create mode 100644 internal/overlay/executor_test.go create mode 100644 internal/pack/host_overlay.go create mode 100644 internal/pack/host_overlay_test.go create mode 100644 packs/openai-cn-pack/overlays/kimi-a7m-sub2api-v0.1.129.md create mode 100644 packs/openai-cn-pack/overlays/kimi-a7m-sub2api-v0.1.129.patch create mode 100644 packs/openai-cn-pack/providers/kimi-a7m.json create mode 100644 scripts/remote43_patched_stack_lib.sh create mode 100755 scripts/setup_remote43_patched_stack.sh diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/00-artifact-guide.txt b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/00-artifact-guide.txt new file mode 100644 index 00000000..80388722 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/00-artifact-guide.txt @@ -0,0 +1,36 @@ +真实宿主验收产物 -> 速查清单对应 + +artifact security mode: safe +contains raw secrets: no +repository-safe: yes + +清单 1(环境 / host 前置) +- 01-create-host.json +- 02-probe-host.json + +清单 2(channel 宿主契约 / 导入落库) +- 03-install-pack.json +- 04-preview-import.json +- 05-import.json +- 05a-batch-detail-pre-access.json(若拿到 batch_id 且非 dry-run) +- 08-provider-status.json +- 09-reconcile.json +- 10-batch-detail.json(若拿到 batch_id 且非 dry-run) + +清单 3(access / key 闭环状态) +- 06-access-preview.json +- 07-access-status.json + +清单 4(必须分层留证据,不可混用) +- account 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /api/v1/admin/accounts/:id/models +- 普通用户 / managed key 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /v1/models +- completion 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 POST /v1/chat/completions +- stock host 出现运行时 gap 后的补丁验证:22-patched-host-validation.json +- 宿主补丁落点与 clean worktree 说明:23-sub2api-host-patch-notes.md + +红线: +- /api/v1/admin/accounts/:id/models 正确 ≠ /v1/models 正确 +- /v1/models 正确 ≠ /v1/chat/completions 正确 +- admin API 成功 ≠ 普通用户链路成功 + +当前 hook 配置:disabled diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/01-create-host.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/01-create-host.json new file mode 100644 index 00000000..c902865e --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/01-create-host.json @@ -0,0 +1 @@ +{"host_id":"local-v0129-kimi-hermes-20260525","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/20260525_local_v0129_kimi_a7m_from_hermes/02-probe-host.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/02-probe-host.json new file mode 100644 index 00000000..2178be5e --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/02-probe-host.json @@ -0,0 +1 @@ +{"host_id":"local-v0129-kimi-hermes-20260525","base_url":"http://127.0.0.1:18109","host_version":"0.1.129","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/20260525_local_v0129_kimi_a7m_from_hermes/03-install-pack.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/03-install-pack.json new file mode 100644 index 00000000..6af8d52c --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/03-install-pack.json @@ -0,0 +1 @@ +{"already_installed":false,"host_version":"0.1.129","pack_id":"openai-cn-pack","providers":[{"display_name":"DeepSeek OpenAI Compatible","provider_id":"deepseek"},{"display_name":"DeepSeek Chat 官方兼容","provider_id":"deepseek-chat-official"},{"display_name":"DeepSeek Reasoner 官方兼容","provider_id":"deepseek-reasoner-official"},{"display_name":"GLM 4.7 官方兼容","provider_id":"glm-4-7-official"},{"display_name":"GLM 5.1 官方兼容","provider_id":"glm-5-1-official"},{"display_name":"Kimi A7M OpenAI Compatible","provider_id":"kimi-a7m"},{"display_name":"Kimi K2.5 官方兼容","provider_id":"kimi-k2-5-official"},{"display_name":"Kimi K2 Thinking 官方兼容","provider_id":"kimi-k2-thinking-official"},{"display_name":"MiniMax OpenAI Compatible","provider_id":"minimax"},{"display_name":"MiniMax M2.7 官方兼容","provider_id":"minimax-m2-7-official"},{"display_name":"OpenAI 中转兼容","provider_id":"openai-zhongzhuan"},{"display_name":"Qwen Coder 官方兼容","provider_id":"qwen-coder-official"},{"display_name":"Qwen 官方兼容","provider_id":"qwen-official"},{"display_name":"Step 3.5 Flash 官方兼容","provider_id":"step-3-5-flash-official"}],"version":"1.1.1"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/04-preview-import.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/04-preview-import.json new file mode 100644 index 00000000..4529d6b9 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/04-preview-import.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"decisions":{"channel":{"Action":"create","Suggested":"Kimi A7M 默认渠道","ExistingID":"","Reason":""},"group":{"Action":"create","Suggested":"Kimi A7M 默认分组","ExistingID":"","Reason":""},"plan":{"Action":"create","Suggested":"Kimi A7M 默认套餐","ExistingID":"","Reason":""}},"names":{"Group":"Kimi A7M 默认分组","Channel":"Kimi A7M 默认渠道","Plan":"Kimi A7M 默认套餐"}} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/05-import.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/05-import.json new file mode 100644 index 00000000..c17bdbeb --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/05-import.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"access_status":"broken","accounts_count":1,"batch_id":1,"batch_status":"partially_succeeded","channel":{"id":"1","name":"Kimi A7M 默认渠道-subscription"},"gateway":{"ok":true,"status_code":200,"models":["kimi-k2.6"],"has_expected_model":true,"completion_ok":false,"completion_status":502,"completion_content_type":"application/json; charset=utf-8","completion_body_preview":"{\"error\":{\"message\":\"Upstream access forbidden, please contact administrator\",\"type\":\"upstream_error\"}}"},"group":{"id":"2","name":"Kimi A7M 默认分组-subscription"},"plan":{"id":"1","name":"Kimi A7M 默认套餐-subscription"},"provider_status":"degraded"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/05a-batch-detail-pre-access.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/05a-batch-detail-pre-access.json new file mode 100644 index 00000000..c108b193 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/05a-batch-detail-pre-access.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":1,"BatchID":1,"ClosureType":"subscription","Status":"broken","DetailsJSON":"{\"completion_ok\":false,\"completion_preview\":\"{\\\"error\\\":{\\\"message\\\":\\\"Upstream access forbidden, please contact administrator\\\",\\\"type\\\":\\\"upstream_error\\\"}}\",\"completion_status\":502,\"completion_type\":\"application/json; charset=utf-8\",\"effective_probe_key_fingerprint\":\"sha256:5869ae8ccd9b3f229ad29b22f626bc5dd2c873130cc5d73c82b5bc40b403871f\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-user\"]}"}],"access_count":1,"batch":{"access_status":"broken","batch_status":"partially_succeeded","host_id":1,"id":1,"mode":"partial","pack_id":1,"provider_id":6},"items":[{"account_status":"warning","batch_id":1,"id":1,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"1\",\"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":4,"managed_resources":[{"ID":1,"BatchID":1,"HostID":1,"ResourceType":"group","HostResourceID":"2","ResourceName":"Kimi A7M 默认分组-subscription"},{"ID":2,"BatchID":1,"HostID":1,"ResourceType":"channel","HostResourceID":"1","ResourceName":"Kimi A7M 默认渠道-subscription"},{"ID":3,"BatchID":1,"HostID":1,"ResourceType":"plan","HostResourceID":"1","ResourceName":"Kimi A7M 默认套餐-subscription"},{"ID":4,"BatchID":1,"HostID":1,"ResourceType":"account","HostResourceID":"1","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/06-access-preview.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/06-access-preview.json new file mode 100644 index 00000000..9940759a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/06-access-preview.json @@ -0,0 +1 @@ +{"provider_id":"kimi-a7m","mode":"subscription","available":false,"message":"access status broken does not satisfy mode subscription"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/07-access-status.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/07-access-status.json new file mode 100644 index 00000000..985f4192 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/07-access-status.json @@ -0,0 +1 @@ +{"batch_access_status":"broken","batch_id":1,"closures_count":1,"latest_access_status":"broken","latest_closure":{"closure_type":"subscription","details_json":"{\"completion_ok\":false,\"completion_preview\":\"{\\\"error\\\":{\\\"message\\\":\\\"Upstream access forbidden, please contact administrator\\\",\\\"type\\\":\\\"upstream_error\\\"}}\",\"completion_status\":502,\"completion_type\":\"application/json; charset=utf-8\",\"effective_probe_key_fingerprint\":\"sha256:5869ae8ccd9b3f229ad29b22f626bc5dd2c873130cc5d73c82b5bc40b403871f\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-user\"]}","id":1,"status":"broken"},"pack_id":"openai-cn-pack","provider_id":"kimi-a7m"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/08-provider-status.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/08-provider-status.json new file mode 100644 index 00000000..e6596bb0 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/08-provider-status.json @@ -0,0 +1 @@ +{"access_closures_count":1,"batch":{"access_status":"broken","batch_status":"partially_succeeded","id":1,"mode":"partial"},"host":{"base_url":"http://127.0.0.1:18109","host_id":"local-v0129-kimi-hermes-20260525","host_version":"0.1.129"},"latest_access_status":"broken","latest_reconcile_status":"not_run","latest_reconcile_summary":{},"managed_resources_count":4,"pack":{"pack_id":"openai-cn-pack","version":"1.1.1"},"provider":{"display_name":"Kimi A7M OpenAI Compatible","platform":"openai","provider_id":"kimi-a7m"},"provider_status":"partially_succeeded","reconcile_runs_count":0} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/09-reconcile.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/09-reconcile.json new file mode 100644 index 00000000..81c5f7ce --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/09-reconcile.json @@ -0,0 +1 @@ +{"batch_id":1,"extra_count":0,"missing_count":0,"provider_id":"kimi-a7m","stale_noise_count":0,"status":"active","summary":{"access_rechecked":false,"access_status":"broken","extra_count":0,"host_version":"0.1.129","missing_count":0,"probe_failures":0,"raw_extra_count":0,"stale_noise_accounts":[],"stale_noise_count":0}} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/10-batch-detail.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/10-batch-detail.json new file mode 100644 index 00000000..d391f3db --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/10-batch-detail.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":1,"BatchID":1,"ClosureType":"subscription","Status":"broken","DetailsJSON":"{\"completion_ok\":false,\"completion_preview\":\"{\\\"error\\\":{\\\"message\\\":\\\"Upstream access forbidden, please contact administrator\\\",\\\"type\\\":\\\"upstream_error\\\"}}\",\"completion_status\":502,\"completion_type\":\"application/json; charset=utf-8\",\"effective_probe_key_fingerprint\":\"sha256:5869ae8ccd9b3f229ad29b22f626bc5dd2c873130cc5d73c82b5bc40b403871f\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-user\"]}"}],"access_count":1,"batch":{"access_status":"broken","batch_status":"partially_succeeded","host_id":1,"id":1,"mode":"partial","pack_id":1,"provider_id":6},"items":[{"account_status":"warning","batch_id":1,"id":1,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"1\",\"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\",\"reconcile_rerun\":true,\"smoke_model_seen\":true,\"validation_status\":\"warning\"}"}],"items_count":1,"managed_count":4,"managed_resources":[{"ID":1,"BatchID":1,"HostID":1,"ResourceType":"group","HostResourceID":"2","ResourceName":"Kimi A7M 默认分组-subscription"},{"ID":2,"BatchID":1,"HostID":1,"ResourceType":"channel","HostResourceID":"1","ResourceName":"Kimi A7M 默认渠道-subscription"},{"ID":3,"BatchID":1,"HostID":1,"ResourceType":"plan","HostResourceID":"1","ResourceName":"Kimi A7M 默认套餐-subscription"},{"ID":4,"BatchID":1,"HostID":1,"ResourceType":"account","HostResourceID":"1","ResourceName":"kimi-a7m-01"}],"reconcile_count":1,"reconcile_runs":[{"ID":1,"BatchID":1,"HostID":1,"ProviderID":6,"Status":"active","SummaryJSON":"{\"access_rechecked\":false,\"access_status\":\"broken\",\"extra_count\":0,\"host_version\":\"0.1.129\",\"missing_count\":0,\"probe_failures\":0,\"raw_extra_count\":0,\"stale_noise_accounts\":[],\"stale_noise_count\":0}","CreatedAt":"2026-05-25 05:14:26"}]} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/21-summary.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/21-summary.json new file mode 100644 index 00000000..781d666a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/21-summary.json @@ -0,0 +1,49 @@ +{ + "artifact_dir": "/home/long/project/sub2api-cn-relay-manager/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes", + "provider_id": "kimi-a7m", + "model_name": "kimi-k2.6", + "host_id": "local-v0129-kimi-hermes-20260525", + "host_version_from_create_host": "0.1.129", + "pack_version": "1.1.1", + "batch_id": 1, + "batch_status": "partially_succeeded", + "access_status_from_import": "broken", + "provider_status_from_import": "degraded", + "provider_status_latest": "partially_succeeded", + "latest_access_status": "broken", + "gateway_models_http200": true, + "gateway_models_has_expected_model": true, + "gateway_completion_status": 502, + "account_models_http200": true, + "account_models_has_expected_model": true, + "manual_gateway_models_http200": true, + "manual_gateway_models_has_expected_model": false, + "manual_gateway_models_sample": [ + "gpt-5.5", + "gpt-5.4", + "gpt-5.4-mini" + ], + "manual_gateway_chat_status": 503, + "upstream_models_http200": true, + "upstream_models_has_expected_model": true, + "upstream_models": [ + "kimi-k2.6" + ], + "upstream_chat_status": 200, + "stock_host_conclusion": "host_compatibility_gap_or_runtime_drift", + "patched_host_validation_file": "22-patched-host-validation.json", + "patched_host_ready": true, + "patched_host_container": "sub2api-patched", + "patched_host_managed_models_http200": true, + "patched_host_managed_models": [ + "kimi-k2.6" + ], + "patched_host_managed_chat_http200": true, + "patched_host_managed_chat_model": "kimi-for-coding", + "patched_host_managed_chat_preview": "Hi there! How can I help you today?", + "patched_host_account_test_success": true, + "patched_host_account_extra_openai_responses_supported": false, + "patched_host_runtime_fallback_confirmed": true, + "patched_host_patch_notes_file": "23-sub2api-host-patch-notes.md", + "conclusion": "stock_host_gap_confirmed_patched_host_ready" +} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/22-patched-host-validation.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/22-patched-host-validation.json new file mode 100644 index 00000000..1f44dbdc --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/22-patched-host-validation.json @@ -0,0 +1,49 @@ +{ + "validation_scope": "patched_host_runtime_verification", + "source_summary": "stock_host import artifact remains unchanged; this file captures the follow-up patched-host proof", + "clean_worktree": "/tmp/sub2api-clean", + "patched_container": { + "name": "sub2api-patched", + "image": "weishaw/sub2api:0.1.129", + "published_port": "127.0.0.1:18129->8080", + "health": "healthy" + }, + "managed_key_runtime": { + "models_http200": true, + "models": [ + "kimi-k2.6" + ], + "chat_http200": true, + "chat_object": "chat.completion", + "chat_model": "kimi-for-coding", + "chat_finish_reason": "stop", + "chat_preview": "Hi there! How can I help you today?" + }, + "admin_runtime": { + "account_test_success": true, + "account_test_preview": "Hi there! How can I help you today?", + "account_status": "active", + "account_schedulable": true, + "account_error_message": "", + "account_extra": { + "openai_responses_supported": false + } + }, + "runtime_fallback": { + "triggered": true, + "log_marker": "openai chat_completions: fallback responses->raw chat after custom upstream incompatibility signal", + "responses_status": 403, + "base_url": "https://kimi.a7m.com.cn/v1" + }, + "host_patch_scope": { + "production_files": [ + "backend/internal/service/openai_apikey_responses_probe.go", + "backend/internal/service/openai_gateway_chat_completions.go", + "backend/internal/service/account_test_service.go" + ], + "test_files": [ + "backend/internal/service/openai_apikey_compat_fallback_test.go" + ] + }, + "final_status": "ready" +} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/23-sub2api-host-patch-notes.md b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/23-sub2api-host-patch-notes.md new file mode 100644 index 00000000..4d62f362 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/23-sub2api-host-patch-notes.md @@ -0,0 +1,24 @@ +patched host 补丁当前落在 clean worktree:`/tmp/sub2api-clean` + +原始仓库 `/home/long/project/sub2api` 工作树存在大量用户侧未提交删除,缺少 `backend/go.mod` 与多处 `internal/service` 依赖文件,不能直接作为可信构建基础。因此本轮没有把补丁强行同步回脏工作树,而是在 detached clean worktree 中完成补丁、单测与旁路容器验收。 + +补丁落点: +- `backend/internal/service/openai_apikey_responses_probe.go` + - 当 custom upstream 的 `/v1/responses` 返回 `403` 时,再交叉探测 `/v1/chat/completions` + - 若 chat 端点可达,则把 `extra.openai_responses_supported=false` 持久化,避免把 chat-only upstream 误判成 Responses-capable +- `backend/internal/service/openai_gateway_chat_completions.go` + - 当 chat 请求经 `Responses` 兼容层命中 `403/404/405`,且账号属于 custom base URL API key 时,立即运行时回退到 raw `/v1/chat/completions` + - 同时把 `openai_responses_supported=false` 写回内存和账号存储,避免后续请求重复踩坑 +- `backend/internal/service/account_test_service.go` + - 管理员 `accounts/:id/test` 对 chat-only upstream 不再直接报“仅支持 Responses API” + - 改为走真实的 chat completions SSE 测试路径,保证管理面测试结论与实际数据面一致 + +定向测试: +- `TestProbeOpenAIAPIKeyResponsesSupport_Responses403WithReachableChatMarksUnsupported` +- `TestForwardAsChatCompletions_CustomBaseURLResponses403FallsBackToRawChat` +- `TestAccountTestService_OpenAIChatOnlyAPIKeyUsesRawChatProbe` + +真实旁路验收结论: +- stock `weishaw/sub2api:0.1.129` 的原始 fresh-host 结论仍然是 `partially_succeeded / broken` +- 应用 clean worktree 补丁并以旁路容器运行后,managed key `/v1/models`、managed key `/v1/chat/completions`、管理员 `accounts/:id/test` 三条链路均已成功 +- 这说明 Hermes `A7M_KIMI_API_KEY` 与 relay-manager pack 接入都没有问题,真实 gap 在宿主对 third-party OpenAI-compatible upstream 的 `Responses -> chat completions` 兼容策略 diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/00-artifact-guide.txt b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/00-artifact-guide.txt new file mode 100644 index 00000000..66700bc6 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/00-artifact-guide.txt @@ -0,0 +1,34 @@ +真实宿主验收产物 -> 速查清单对应 + +artifact security mode: safe +contains raw secrets: no +repository-safe: yes + +清单 1(环境 / host 前置) +- 01-create-host.json +- 02-probe-host.json + +清单 2(channel 宿主契约 / 导入落库) +- 03-install-pack.json +- 04-preview-import.json +- 05-import.json +- 05a-batch-detail-pre-access.json(若拿到 batch_id 且非 dry-run) +- 08-provider-status.json +- 09-reconcile.json +- 10-batch-detail.json(若拿到 batch_id 且非 dry-run) + +清单 3(access / key 闭环状态) +- 06-access-preview.json +- 07-access-status.json + +清单 4(必须分层留证据,不可混用) +- account 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /api/v1/admin/accounts/:id/models +- 普通用户 / managed key 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /v1/models +- completion 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 POST /v1/chat/completions + +红线: +- /api/v1/admin/accounts/:id/models 正确 ≠ /v1/models 正确 +- /v1/models 正确 ≠ /v1/chat/completions 正确 +- admin API 成功 ≠ 普通用户链路成功 + +当前 hook 配置:disabled diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/01-create-host.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/01-create-host.json new file mode 100644 index 00000000..2cab0583 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/01-create-host.json @@ -0,0 +1 @@ +{"host_id":"local-v0129-patched-overlay-18139-clean","base_url":"http://127.0.0.1:18139","host_version":"0.1.126","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/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/02-probe-host.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/02-probe-host.json new file mode 100644 index 00000000..32ce2e20 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/02-probe-host.json @@ -0,0 +1 @@ +{"host_id":"local-v0129-patched-overlay-18139-clean","base_url":"http://127.0.0.1:18139","host_version":"0.1.126","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/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/03-install-pack.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/03-install-pack.json new file mode 100644 index 00000000..183c9fc2 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/03-install-pack.json @@ -0,0 +1 @@ +{"already_installed":false,"host_version":"0.1.126","pack_id":"openai-cn-pack","providers":[{"display_name":"DeepSeek OpenAI Compatible","provider_id":"deepseek"},{"display_name":"DeepSeek Chat 官方兼容","provider_id":"deepseek-chat-official"},{"display_name":"DeepSeek Reasoner 官方兼容","provider_id":"deepseek-reasoner-official"},{"display_name":"GLM 4.7 官方兼容","provider_id":"glm-4-7-official"},{"display_name":"GLM 5.1 官方兼容","provider_id":"glm-5-1-official"},{"display_name":"Kimi A7M OpenAI Compatible","provider_id":"kimi-a7m"},{"display_name":"Kimi K2.5 官方兼容","provider_id":"kimi-k2-5-official"},{"display_name":"Kimi K2 Thinking 官方兼容","provider_id":"kimi-k2-thinking-official"},{"display_name":"MiniMax OpenAI Compatible","provider_id":"minimax"},{"display_name":"MiniMax M2.7 官方兼容","provider_id":"minimax-m2-7-official"},{"display_name":"OpenAI 中转兼容","provider_id":"openai-zhongzhuan"},{"display_name":"Qwen Coder 官方兼容","provider_id":"qwen-coder-official"},{"display_name":"Qwen 官方兼容","provider_id":"qwen-official"},{"display_name":"Step 3.5 Flash 官方兼容","provider_id":"step-3-5-flash-official"}],"version":"1.1.3"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/04-preview-import.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/04-preview-import.json new file mode 100644 index 00000000..fa6d35b8 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/04-preview-import.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"decisions":{"channel":{"Action":"create","Suggested":"Kimi A7M 默认渠道","ExistingID":"","Reason":""},"group":{"Action":"create","Suggested":"Kimi A7M 默认分组","ExistingID":"","Reason":""},"plan":{"Action":"create","Suggested":"Kimi A7M 默认套餐","ExistingID":"","Reason":""}},"host_overlays":[],"names":{"Group":"Kimi A7M 默认分组","Channel":"Kimi A7M 默认渠道","Plan":"Kimi A7M 默认套餐"}} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/05-import.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/05-import.json new file mode 100644 index 00000000..cb0f0444 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/05-import.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"access_status":"subscription_ready","accounts_count":1,"batch_id":1,"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_2137ef96-8d8f-4767-9314-2625c5a3cdd0\",\"model\":\"kimi-k2.6\",\"object\":\"chat.completion\",\"created\":1779698918,\"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"},"host_overlays":[],"plan":{"id":"1","name":"Kimi A7M 默认套餐-subscription"},"provider_status":"active"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/05a-batch-detail-pre-access.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/05a-batch-detail-pre-access.json new file mode 100644 index 00000000..12eb9459 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/05a-batch-detail-pre-access.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":1,"BatchID":1,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_2137ef96-8d8f-4767-9314-2625c5a3cdd0\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779698918,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:40bdfa38a721ab2d5ba4e3b471753e441e913123ea04959fb3feca5c517b057b\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-kimi-overlay-clean\"]}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":1,"id":1,"mode":"partial","pack_id":1,"provider_id":6},"items":[{"account_status":"warning","batch_id":1,"id":1,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"1\",\"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":4,"managed_resources":[{"ID":1,"BatchID":1,"HostID":1,"ResourceType":"group","HostResourceID":"2","ResourceName":"Kimi A7M 默认分组-subscription"},{"ID":2,"BatchID":1,"HostID":1,"ResourceType":"channel","HostResourceID":"1","ResourceName":"Kimi A7M 默认渠道-subscription"},{"ID":3,"BatchID":1,"HostID":1,"ResourceType":"plan","HostResourceID":"1","ResourceName":"Kimi A7M 默认套餐-subscription"},{"ID":4,"BatchID":1,"HostID":1,"ResourceType":"account","HostResourceID":"1","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/06-access-preview.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/06-access-preview.json new file mode 100644 index 00000000..56e78371 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/06-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/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/07-access-status.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/07-access-status.json new file mode 100644 index 00000000..29b58e1f --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/07-access-status.json @@ -0,0 +1 @@ +{"batch_access_status":"subscription_ready","batch_id":1,"closures_count":1,"latest_access_status":"subscription_ready","latest_closure":{"closure_type":"subscription","details_json":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_2137ef96-8d8f-4767-9314-2625c5a3cdd0\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779698918,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:40bdfa38a721ab2d5ba4e3b471753e441e913123ea04959fb3feca5c517b057b\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-kimi-overlay-clean\"]}","id":1,"status":"subscription_ready"},"pack_id":"openai-cn-pack","provider_id":"kimi-a7m"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/08-provider-status.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/08-provider-status.json new file mode 100644 index 00000000..9382af0f --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/08-provider-status.json @@ -0,0 +1 @@ +{"access_closures_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","id":1,"mode":"partial"},"host":{"base_url":"http://127.0.0.1:18139","host_id":"local-v0129-patched-overlay-18139-clean","host_version":"0.1.126"},"latest_access_status":"subscription_ready","latest_reconcile_status":"not_run","latest_reconcile_summary":{},"managed_resources_count":4,"pack":{"pack_id":"openai-cn-pack","version":"1.1.3"},"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/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/09-reconcile.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/09-reconcile.json new file mode 100644 index 00000000..f3d01afd --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/09-reconcile.json @@ -0,0 +1 @@ +{"batch_id":1,"extra_count":0,"missing_count":0,"provider_id":"kimi-a7m","stale_noise_count":0,"status":"active","summary":{"access_rechecked":false,"access_status":"subscription_ready","extra_count":0,"host_version":"0.1.126","missing_count":0,"probe_failures":0,"raw_extra_count":0,"stale_noise_accounts":[],"stale_noise_count":0}} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/10-batch-detail.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/10-batch-detail.json new file mode 100644 index 00000000..d2fdbebf --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/10-batch-detail.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":1,"BatchID":1,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_2137ef96-8d8f-4767-9314-2625c5a3cdd0\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779698918,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:40bdfa38a721ab2d5ba4e3b471753e441e913123ea04959fb3feca5c517b057b\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-kimi-overlay-clean\"]}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":1,"id":1,"mode":"partial","pack_id":1,"provider_id":6},"items":[{"account_status":"passed","batch_id":1,"id":1,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"1\",\"models\":[{\"id\":\"kimi-k2.6\",\"display_name\":\"kimi-k2.6\",\"type\":\"model\"}],\"probe_advisory\":false,\"probe_message\":\"\",\"probe_ok\":true,\"probe_status\":\"passed\",\"reconcile_rerun\":true,\"smoke_model_seen\":true,\"validation_status\":\"passed\"}"}],"items_count":1,"managed_count":4,"managed_resources":[{"ID":1,"BatchID":1,"HostID":1,"ResourceType":"group","HostResourceID":"2","ResourceName":"Kimi A7M 默认分组-subscription"},{"ID":2,"BatchID":1,"HostID":1,"ResourceType":"channel","HostResourceID":"1","ResourceName":"Kimi A7M 默认渠道-subscription"},{"ID":3,"BatchID":1,"HostID":1,"ResourceType":"plan","HostResourceID":"1","ResourceName":"Kimi A7M 默认套餐-subscription"},{"ID":4,"BatchID":1,"HostID":1,"ResourceType":"account","HostResourceID":"1","ResourceName":"kimi-a7m-01"}],"reconcile_count":1,"reconcile_runs":[{"ID":1,"BatchID":1,"HostID":1,"ProviderID":6,"Status":"active","SummaryJSON":"{\"access_rechecked\":false,\"access_status\":\"subscription_ready\",\"extra_count\":0,\"host_version\":\"0.1.126\",\"missing_count\":0,\"probe_failures\":0,\"raw_extra_count\":0,\"stale_noise_accounts\":[],\"stale_noise_count\":0}","CreatedAt":"2026-05-25 08:48:39"}]} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/20-acceptance.stderr.txt b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/20-acceptance.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/20-acceptance.stdout.txt b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/20-acceptance.stdout.txt new file mode 100644 index 00000000..e007afc7 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/20-acceptance.stdout.txt @@ -0,0 +1,8 @@ +artifacts: /home/long/project/sub2api-cn-relay-manager/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean +host_id=local-v0129-patched-overlay-18139-clean +batch_id=1 +artifact guide: /home/long/project/sub2api-cn-relay-manager/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/00-artifact-guide.txt +checklist import evidence: 04-preview-import.json 05-import.json 05a-batch-detail-pre-access.json(optional) 08-provider-status.json 09-reconcile.json +checklist access evidence: 06-access-preview.json 07-access-status.json +checklist layered evidence: missing hook-generated /accounts/:id/models, /v1/models, /v1/chat/completions artifacts +acceptance flow completed diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/21-summary.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/21-summary.json new file mode 100644 index 00000000..55879fbd --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/21-summary.json @@ -0,0 +1,16 @@ +{ + "host_base_url": "http://127.0.0.1:18139", + "crm_base_url": "http://127.0.0.1:18141", + "provider_id": "kimi-a7m", + "batch_id": 1, + "import_batch_status": "succeeded", + "provider_status": "active", + "latest_access_status": "subscription_ready", + "latest_closure_status": "subscription_ready", + "effective_probe_key_source": "managed_subscription", + "models_http_status": 200, + "completion_ok": true, + "completion_status": 200, + "has_expected_model": true, + "reconcile_status": "active" +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost/00-artifact-guide.txt b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost/00-artifact-guide.txt new file mode 100644 index 00000000..66700bc6 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost/00-artifact-guide.txt @@ -0,0 +1,34 @@ +真实宿主验收产物 -> 速查清单对应 + +artifact security mode: safe +contains raw secrets: no +repository-safe: yes + +清单 1(环境 / host 前置) +- 01-create-host.json +- 02-probe-host.json + +清单 2(channel 宿主契约 / 导入落库) +- 03-install-pack.json +- 04-preview-import.json +- 05-import.json +- 05a-batch-detail-pre-access.json(若拿到 batch_id 且非 dry-run) +- 08-provider-status.json +- 09-reconcile.json +- 10-batch-detail.json(若拿到 batch_id 且非 dry-run) + +清单 3(access / key 闭环状态) +- 06-access-preview.json +- 07-access-status.json + +清单 4(必须分层留证据,不可混用) +- account 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /api/v1/admin/accounts/:id/models +- 普通用户 / managed key 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /v1/models +- completion 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 POST /v1/chat/completions + +红线: +- /api/v1/admin/accounts/:id/models 正确 ≠ /v1/models 正确 +- /v1/models 正确 ≠ /v1/chat/completions 正确 +- admin API 成功 ≠ 普通用户链路成功 + +当前 hook 配置:disabled diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/00-artifact-guide.txt b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/00-artifact-guide.txt new file mode 100644 index 00000000..66700bc6 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/00-artifact-guide.txt @@ -0,0 +1,34 @@ +真实宿主验收产物 -> 速查清单对应 + +artifact security mode: safe +contains raw secrets: no +repository-safe: yes + +清单 1(环境 / host 前置) +- 01-create-host.json +- 02-probe-host.json + +清单 2(channel 宿主契约 / 导入落库) +- 03-install-pack.json +- 04-preview-import.json +- 05-import.json +- 05a-batch-detail-pre-access.json(若拿到 batch_id 且非 dry-run) +- 08-provider-status.json +- 09-reconcile.json +- 10-batch-detail.json(若拿到 batch_id 且非 dry-run) + +清单 3(access / key 闭环状态) +- 06-access-preview.json +- 07-access-status.json + +清单 4(必须分层留证据,不可混用) +- account 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /api/v1/admin/accounts/:id/models +- 普通用户 / managed key 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /v1/models +- completion 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 POST /v1/chat/completions + +红线: +- /api/v1/admin/accounts/:id/models 正确 ≠ /v1/models 正确 +- /v1/models 正确 ≠ /v1/chat/completions 正确 +- admin API 成功 ≠ 普通用户链路成功 + +当前 hook 配置:disabled diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/01-create-host.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/01-create-host.json new file mode 100644 index 00000000..c902865e --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/01-create-host.json @@ -0,0 +1 @@ +{"host_id":"local-v0129-kimi-hermes-20260525","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/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/02-probe-host.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/02-probe-host.json new file mode 100644 index 00000000..2178be5e --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/02-probe-host.json @@ -0,0 +1 @@ +{"host_id":"local-v0129-kimi-hermes-20260525","base_url":"http://127.0.0.1:18109","host_version":"0.1.129","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/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/03-install-pack.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/03-install-pack.json new file mode 100644 index 00000000..35b4910f --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/03-install-pack.json @@ -0,0 +1 @@ +{"already_installed":true,"host_version":"0.1.129","pack_id":"openai-cn-pack","providers":[{"display_name":"DeepSeek OpenAI Compatible","provider_id":"deepseek"},{"display_name":"DeepSeek Chat 官方兼容","provider_id":"deepseek-chat-official"},{"display_name":"DeepSeek Reasoner 官方兼容","provider_id":"deepseek-reasoner-official"},{"display_name":"GLM 4.7 官方兼容","provider_id":"glm-4-7-official"},{"display_name":"GLM 5.1 官方兼容","provider_id":"glm-5-1-official"},{"display_name":"Kimi A7M OpenAI Compatible","provider_id":"kimi-a7m"},{"display_name":"Kimi K2.5 官方兼容","provider_id":"kimi-k2-5-official"},{"display_name":"Kimi K2 Thinking 官方兼容","provider_id":"kimi-k2-thinking-official"},{"display_name":"MiniMax OpenAI Compatible","provider_id":"minimax"},{"display_name":"MiniMax M2.7 官方兼容","provider_id":"minimax-m2-7-official"},{"display_name":"OpenAI 中转兼容","provider_id":"openai-zhongzhuan"},{"display_name":"Qwen Coder 官方兼容","provider_id":"qwen-coder-official"},{"display_name":"Qwen 官方兼容","provider_id":"qwen-official"},{"display_name":"Step 3.5 Flash 官方兼容","provider_id":"step-3-5-flash-official"}],"version":"1.1.1"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/04-preview-import.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/04-preview-import.json new file mode 100644 index 00000000..4529d6b9 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/04-preview-import.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"decisions":{"channel":{"Action":"create","Suggested":"Kimi A7M 默认渠道","ExistingID":"","Reason":""},"group":{"Action":"create","Suggested":"Kimi A7M 默认分组","ExistingID":"","Reason":""},"plan":{"Action":"create","Suggested":"Kimi A7M 默认套餐","ExistingID":"","Reason":""}},"names":{"Group":"Kimi A7M 默认分组","Channel":"Kimi A7M 默认渠道","Plan":"Kimi A7M 默认套餐"}} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/05-import.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/05-import.json new file mode 100644 index 00000000..90aef8da --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/05-import.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"access_status":"broken","accounts_count":1,"batch_id":3,"batch_status":"partially_succeeded","channel":{"id":"1","name":"Kimi A7M 默认渠道-subscription"},"gateway":{"ok":true,"status_code":200,"models":["kimi-k2.6"],"has_expected_model":true,"completion_ok":false,"completion_status":502,"completion_content_type":"application/json; charset=utf-8","completion_body_preview":"{\"error\":{\"message\":\"Upstream access forbidden, please contact administrator\",\"type\":\"upstream_error\"}}"},"group":{"id":"2","name":"Kimi A7M 默认分组-subscription"},"plan":{"id":"1","name":"Kimi A7M 默认套餐-subscription"},"provider_status":"degraded"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/05a-batch-detail-pre-access.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/05a-batch-detail-pre-access.json new file mode 100644 index 00000000..f0f9a48e --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/05a-batch-detail-pre-access.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":3,"BatchID":3,"ClosureType":"subscription","Status":"broken","DetailsJSON":"{\"completion_ok\":false,\"completion_preview\":\"{\\\"error\\\":{\\\"message\\\":\\\"Upstream access forbidden, please contact administrator\\\",\\\"type\\\":\\\"upstream_error\\\"}}\",\"completion_status\":502,\"completion_type\":\"application/json; charset=utf-8\",\"effective_probe_key_fingerprint\":\"sha256:5fed6b48a9184d0b6b75e725c4b49ae9b692537218f4ee6c85b0c6aced64ccf5\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-kimi-scheme-c\"]}"}],"access_count":1,"batch":{"access_status":"broken","batch_status":"partially_succeeded","host_id":1,"id":3,"mode":"partial","pack_id":1,"provider_id":6},"items":[{"account_status":"warning","batch_id":3,"id":3,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"3\",\"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":6,"BatchID":3,"HostID":1,"ResourceType":"account","HostResourceID":"3","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/06-access-preview.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/06-access-preview.json new file mode 100644 index 00000000..9940759a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/06-access-preview.json @@ -0,0 +1 @@ +{"provider_id":"kimi-a7m","mode":"subscription","available":false,"message":"access status broken does not satisfy mode subscription"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/07-access-status.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/07-access-status.json new file mode 100644 index 00000000..e6fad775 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/07-access-status.json @@ -0,0 +1 @@ +{"batch_access_status":"broken","batch_id":3,"closures_count":1,"latest_access_status":"broken","latest_closure":{"closure_type":"subscription","details_json":"{\"completion_ok\":false,\"completion_preview\":\"{\\\"error\\\":{\\\"message\\\":\\\"Upstream access forbidden, please contact administrator\\\",\\\"type\\\":\\\"upstream_error\\\"}}\",\"completion_status\":502,\"completion_type\":\"application/json; charset=utf-8\",\"effective_probe_key_fingerprint\":\"sha256:5fed6b48a9184d0b6b75e725c4b49ae9b692537218f4ee6c85b0c6aced64ccf5\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-kimi-scheme-c\"]}","id":3,"status":"broken"},"pack_id":"openai-cn-pack","provider_id":"kimi-a7m"} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/08-provider-status.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/08-provider-status.json new file mode 100644 index 00000000..8a898478 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/08-provider-status.json @@ -0,0 +1 @@ +{"access_closures_count":1,"batch":{"access_status":"broken","batch_status":"partially_succeeded","id":3,"mode":"partial"},"host":{"base_url":"http://127.0.0.1:18109","host_id":"local-v0129-kimi-hermes-20260525","host_version":"0.1.129"},"latest_access_status":"broken","latest_reconcile_status":"not_run","latest_reconcile_summary":{},"managed_resources_count":1,"pack":{"pack_id":"openai-cn-pack","version":"1.1.1"},"provider":{"display_name":"Kimi A7M OpenAI Compatible","platform":"openai","provider_id":"kimi-a7m"},"provider_status":"partially_succeeded","reconcile_runs_count":0} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/09-reconcile.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/09-reconcile.json new file mode 100644 index 00000000..e462bc1c --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/09-reconcile.json @@ -0,0 +1 @@ +{"batch_id":3,"extra_count":0,"missing_count":0,"provider_id":"kimi-a7m","stale_noise_count":0,"status":"active","summary":{"access_rechecked":false,"access_status":"broken","extra_count":0,"host_version":"0.1.129","missing_count":0,"probe_failures":0,"raw_extra_count":0,"stale_noise_accounts":[],"stale_noise_count":0}} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/10-batch-detail.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/10-batch-detail.json new file mode 100644 index 00000000..c89d314b --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/10-batch-detail.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":3,"BatchID":3,"ClosureType":"subscription","Status":"broken","DetailsJSON":"{\"completion_ok\":false,\"completion_preview\":\"{\\\"error\\\":{\\\"message\\\":\\\"Upstream access forbidden, please contact administrator\\\",\\\"type\\\":\\\"upstream_error\\\"}}\",\"completion_status\":502,\"completion_type\":\"application/json; charset=utf-8\",\"effective_probe_key_fingerprint\":\"sha256:5fed6b48a9184d0b6b75e725c4b49ae9b692537218f4ee6c85b0c6aced64ccf5\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"crm-kimi-scheme-c\"]}"}],"access_count":1,"batch":{"access_status":"broken","batch_status":"partially_succeeded","host_id":1,"id":3,"mode":"partial","pack_id":1,"provider_id":6},"items":[{"account_status":"warning","batch_id":3,"id":3,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"3\",\"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\",\"reconcile_rerun\":true,\"smoke_model_seen\":true,\"validation_status\":\"warning\"}"}],"items_count":1,"managed_count":1,"managed_resources":[{"ID":6,"BatchID":3,"HostID":1,"ResourceType":"account","HostResourceID":"3","ResourceName":"kimi-a7m-01"}],"reconcile_count":1,"reconcile_runs":[{"ID":2,"BatchID":3,"HostID":1,"ProviderID":6,"Status":"active","SummaryJSON":"{\"access_rechecked\":false,\"access_status\":\"broken\",\"extra_count\":0,\"host_version\":\"0.1.129\",\"missing_count\":0,\"probe_failures\":0,\"raw_extra_count\":0,\"stale_noise_accounts\":[],\"stale_noise_count\":0}","CreatedAt":"2026-05-25 07:17:17"}]} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/11-rollback.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/11-rollback.json new file mode 100644 index 00000000..2c76d942 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/11-rollback.json @@ -0,0 +1 @@ +{"batch_id":3,"deleted_accounts":1,"deleted_channels":0,"deleted_groups":0,"deleted_plans":0} diff --git a/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/21-summary.json b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/21-summary.json new file mode 100644 index 00000000..db565d25 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/21-summary.json @@ -0,0 +1,34 @@ +{ + "artifact_dir": "/home/long/project/sub2api-cn-relay-manager/artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun", + "provider_id": "kimi-a7m", + "model_name": "kimi-k2.6", + "host_id": "local-v0129-kimi-hermes-20260525", + "host_version_from_create_host": "0.1.129", + "pack_version": "1.1.1", + "batch_id": 3, + "batch_status": "partially_succeeded", + "access_status_from_import": "broken", + "provider_status_from_import": "degraded", + "provider_status_latest": "partially_succeeded", + "latest_access_status": "broken", + "preview_available": false, + "gateway_models_http200": true, + "gateway_models_has_expected_model": true, + "gateway_completion_status": 502, + "gateway_completion_ok": false, + "gateway_completion_error_type": "upstream_error", + "gateway_completion_error_message": "Upstream access forbidden, please contact administrator", + "effective_probe_key_source": "managed_subscription", + "effective_probe_key_fingerprint": "sha256:5fed6b48a9184d0b6b75e725c4b49ae9b692537218f4ee6c85b0c6aced64ccf5", + "account_probe_advisory": true, + "account_probe_message": "API returned 403: Forbidden", + "account_probe_reconcile_rerun": true, + "reconcile_status": "active", + "reconcile_access_rechecked": false, + "rollback_deleted_accounts": 1, + "stock_host_conclusion": "scheme_c_control_plane_only_not_sufficient", + "reference_stock_gap_file": "../20260525_local_v0129_kimi_a7m_from_hermes/21-summary.json", + "reference_patched_host_file": "../20260525_local_v0129_kimi_a7m_from_hermes/22-patched-host-validation.json", + "patched_host_ready_reference": true, + "conclusion": "stock_host_still_broken_after_scheme_c_rerun" +} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/00-local-key-source.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/00-local-key-source.json new file mode 100644 index 00000000..ffabcd0e --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/00-local-key-source.json @@ -0,0 +1,10 @@ +{ + "source": "file:/tmp/a7m_kimi_remote43.key", + "provider_id": "kimi-a7m", + "redacted": { + "present": true, + "prefix": "sk-F", + "suffix": "rqf2", + "fingerprint": "100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe" + } +} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/01-runtime-context.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/01-runtime-context.json new file mode 100644 index 00000000..29657a78 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/01-runtime-context.json @@ -0,0 +1,16 @@ +{ + "crm_base": "http://127.0.0.1:18142", + "host_base": "http://127.0.0.1:18149", + "crm_host_base": "http://127.0.0.1:18149", + "remote_host_base": "http://127.0.0.1:18139", + "provider_id": "kimi-a7m", + "subscription_group_id": null, + "import_group_id": null, + "subscription_user_id_hash": "d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35", + "subscription_user_key": { + "present": true, + "prefix": "sk-1", + "suffix": "e237", + "fingerprint": "729466d494b959e3e40deb27de81cafde8a11df2ba045c879011dd4c52003ecb" + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/01a-create-host.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost/01a-create-host.json new file mode 100644 index 00000000..e69de29b diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/00-local-key-source.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/00-local-key-source.json new file mode 100644 index 00000000..6e3d7803 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/00-local-key-source.json @@ -0,0 +1,10 @@ +{ + "source": "file:/tmp/a7m_kimi_remote43_remotecrm.key", + "provider_id": "kimi-a7m", + "redacted": { + "present": true, + "prefix": "sk-F", + "suffix": "rqf2", + "fingerprint": "100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe" + } +} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/01-runtime-context.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/01-runtime-context.json new file mode 100644 index 00000000..cd4f7d0c --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/01-runtime-context.json @@ -0,0 +1,25 @@ +{ + "crm_base": "http://127.0.0.1:18143", + "host_base": "http://127.0.0.1:18139", + "crm_host_base": "http://127.0.0.1:18139", + "remote_host_base": "http://127.0.0.1:18139", + "provider_id": "kimi-a7m", + "subscription_group_id": "3", + "import_group_id": null, + "subscription_user_id_hash": "d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35", + "managed_user_id_hash": "4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce", + "admin_user_id_hash": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b", + "managed_user_email_hash": "dd4380e47e3a3bb763c98c36afb00968457035e92517f6503d4619f0af973ebb", + "subscription_user_key": { + "present": true, + "prefix": "sk-1", + "suffix": "e237", + "fingerprint": "729466d494b959e3e40deb27de81cafde8a11df2ba045c879011dd4c52003ecb" + }, + "managed_probe_key": { + "present": true, + "prefix": "sk-r", + "suffix": "31da", + "fingerprint": "75b239cf52e25232b04eeff4bbf50590adb34a541e7f127e142028e168bf7804" + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/01a-create-host.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/01a-create-host.json new file mode 100644 index 00000000..0695f09a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/01a-create-host.json @@ -0,0 +1 @@ +{"host_id":"remote43-kimi-patched-remotecrm-18139","base_url":"http://127.0.0.1:18139","host_version":"0.1.126","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/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/02-import.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/02-import.headers.txt new file mode 100644 index 00000000..fc10644d --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/02-import.headers.txt @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 25 May 2026 10:01:54 GMT +Content-Length: 1019 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/03-import.body.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/03-import.body.json new file mode 100644 index 00000000..8451f224 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/03-import.body.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"access_status":"subscription_ready","accounts_count":1,"batch_id":1,"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_d5c894c7-b17b-47a9-93f1-d0b083c3ea9c\",\"model\":\"kimi-k2.6\",\"object\":\"chat.completion\",\"created\":1779703314,\"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":"3","name":"Kimi A7M 默认分组-subscription"},"host_overlays":[],"plan":{"id":"1","name":"Kimi A7M 默认套餐-subscription"},"provider_status":"active"} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/04-batch-detail-initial.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/04-batch-detail-initial.json new file mode 100644 index 00000000..7492f88b --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/04-batch-detail-initial.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":1,"BatchID":1,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_d5c894c7-b17b-47a9-93f1-d0b083c3ea9c\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779703314,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:75b239cf52e25232b04eeff4bbf50590adb34a541e7f127e142028e168bf7804\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"requested_probe_api_key\":\"sk-1779702240-b324e237\",\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"2\"]}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":1,"id":1,"mode":"partial","pack_id":1,"provider_id":1},"items":[{"account_status":"warning","batch_id":1,"id":1,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"1\",\"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":4,"managed_resources":[{"ID":1,"BatchID":1,"HostID":1,"ResourceType":"group","HostResourceID":"3","ResourceName":"Kimi A7M 默认分组-subscription"},{"ID":2,"BatchID":1,"HostID":1,"ResourceType":"channel","HostResourceID":"1","ResourceName":"Kimi A7M 默认渠道-subscription"},{"ID":3,"BatchID":1,"HostID":1,"ResourceType":"plan","HostResourceID":"1","ResourceName":"Kimi A7M 默认套餐-subscription"},{"ID":4,"BatchID":1,"HostID":1,"ResourceType":"account","HostResourceID":"1","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/05-subscription-access-prep.summary.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/05-subscription-access-prep.summary.json new file mode 100644 index 00000000..50419b15 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/05-subscription-access-prep.summary.json @@ -0,0 +1,12 @@ +{ + "subscription_user_id_hash": "d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35", + "subscription_group_id": 3, + "min_balance": 10, + "subscription_days": 30, + "api_key": { + "present": true, + "prefix": "sk-1", + "suffix": "e237", + "fingerprint": "729466d494b959e3e40deb27de81cafde8a11df2ba045c879011dd4c52003ecb" + } +} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/06-subscription-access-prep.psql.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/06-subscription-access-prep.psql.txt new file mode 100644 index 00000000..894b2e3a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/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/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/07-redis-targeted-invalidation.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/07-redis-targeted-invalidation.json new file mode 100644 index 00000000..85a0578a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/07-redis-targeted-invalidation.json @@ -0,0 +1,6 @@ +{ + "auth_cache_invalidated": true, + "balance_cache_invalidated": true, + "subscription_cache_invalidated": true, + "redis_del_exit_code": 0 +} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/08-subscription-group-state.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/08-subscription-group-state.json new file mode 100644 index 00000000..602e5843 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/08-subscription-group-state.json @@ -0,0 +1,28 @@ +{ + "group_id": 3, + "group": { + "id": 3, + "name": "Kimi A7M 默认分组-subscription", + "type": null, + "subscription_type": "subscription" + }, + "subscription": { + "id": 1, + "user_id_hash": "4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce", + "group_id": 3, + "status": "active", + "starts_at": "2026-05-25T10:01:49.952117+00:00", + "expires_at": "2026-06-24T10:01:49.952117+00:00" + }, + "key": { + "id": 2, + "group_id": 3, + "status": "active", + "redacted": { + "present": true, + "prefix": "sk-r", + "suffix": "31da", + "fingerprint": "75b239cf52e25232b04eeff4bbf50590adb34a541e7f127e142028e168bf7804" + } + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/09-models.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/09-models.headers.txt new file mode 100644 index 00000000..935b999a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/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: aa1a1260-c101-41e9-8fd8-48eca2dd76d8 +Date: Mon, 25 May 2026 10:02:00 GMT +Content-Length: 123 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/10-models.body.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/10-models.body.json new file mode 100644 index 00000000..b9de987c --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/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/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/11-chat.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/11-chat.headers.txt new file mode 100644 index 00000000..81c4e371 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/11-chat.headers.txt @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 25 May 2026 10:02:04 GMT +Referrer-Policy: strict-origin-when-cross-origin +Vary: Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-Request-Id: 5686a075-4d35-4c28-921f-9947fff37603 +Content-Length: 611 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/12-chat.body.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/12-chat.body.json new file mode 100644 index 00000000..e80db07f --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/12-chat.body.json @@ -0,0 +1 @@ +{"id":"msg_71e88e35-93a4-4e1f-aa17-c70adaf3b1b0","model":"kimi-k2.6","object":"chat.completion","created":1779703324,"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/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/13-provider-status.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/13-provider-status.json new file mode 100644 index 00000000..c8613e42 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/13-provider-status.json @@ -0,0 +1 @@ +{"access_closures_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","id":1,"mode":"partial"},"host":{"base_url":"http://127.0.0.1:18139","host_id":"remote43-kimi-patched-remotecrm-18139","host_version":"0.1.126"},"latest_access_status":"subscription_ready","latest_reconcile_status":"not_run","latest_reconcile_summary":{},"managed_resources_count":4,"pack":{"pack_id":"openai-cn-pack","version":"1.1.3"},"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/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/14-access-status.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/14-access-status.json new file mode 100644 index 00000000..5465ac09 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/14-access-status.json @@ -0,0 +1 @@ +{"batch_access_status":"subscription_ready","batch_id":1,"closures_count":1,"latest_access_status":"subscription_ready","latest_closure":{"closure_type":"subscription","details_json":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_d5c894c7-b17b-47a9-93f1-d0b083c3ea9c\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779703314,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:75b239cf52e25232b04eeff4bbf50590adb34a541e7f127e142028e168bf7804\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"requested_probe_api_key\":\"sk-1779702240-b324e237\",\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"2\"]}","id":1,"status":"subscription_ready"},"pack_id":"openai-cn-pack","provider_id":"kimi-a7m"} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/15-access-preview.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/15-access-preview.json new file mode 100644 index 00000000..56e78371 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/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/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/16-batch-detail-final.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/16-batch-detail-final.json new file mode 100644 index 00000000..7492f88b --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/16-batch-detail-final.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":1,"BatchID":1,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_d5c894c7-b17b-47a9-93f1-d0b083c3ea9c\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779703314,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:75b239cf52e25232b04eeff4bbf50590adb34a541e7f127e142028e168bf7804\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"requested_probe_api_key\":\"sk-1779702240-b324e237\",\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"2\"]}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":1,"id":1,"mode":"partial","pack_id":1,"provider_id":1},"items":[{"account_status":"warning","batch_id":1,"id":1,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"1\",\"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":4,"managed_resources":[{"ID":1,"BatchID":1,"HostID":1,"ResourceType":"group","HostResourceID":"3","ResourceName":"Kimi A7M 默认分组-subscription"},{"ID":2,"BatchID":1,"HostID":1,"ResourceType":"channel","HostResourceID":"1","ResourceName":"Kimi A7M 默认渠道-subscription"},{"ID":3,"BatchID":1,"HostID":1,"ResourceType":"plan","HostResourceID":"1","ResourceName":"Kimi A7M 默认套餐-subscription"},{"ID":4,"BatchID":1,"HostID":1,"ResourceType":"account","HostResourceID":"1","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/17-upstream-models.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/17-upstream-models.headers.txt new file mode 100644 index 00000000..d447f30b --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/17-upstream-models.headers.txt @@ -0,0 +1,9 @@ +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json; charset=utf-8 +date: Mon, 25 May 2026 10:02:07 GMT +via: 1.1 Caddy +x-new-api-version: v0.0.0 +x-oneapi-request-id: 20260525100207102733615i5evCBDa +content-length: 168 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/18-upstream-models.body.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/18-upstream-models.body.json new file mode 100644 index 00000000..8369a64f --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/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/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/19-upstream-chat.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/19-upstream-chat.headers.txt new file mode 100644 index 00000000..8b06b3d0 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/19-upstream-chat.headers.txt @@ -0,0 +1,14 @@ +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json +date: Mon, 25 May 2026 10:02:10 GMT +req-arrive-time: 1779703330162 +req-cost-time: 1398 +resp-start-time: 1779703331560 +server: istio-envoy +via: 1.1 Caddy +x-envoy-upstream-service-time: 1394 +x-new-api-version: v0.0.0 +x-oneapi-request-id: 20260525100210124345475D8wajYwv +content-length: 611 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/20-upstream-chat.body.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/20-upstream-chat.body.txt new file mode 100644 index 00000000..3d9bc437 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/20-upstream-chat.body.txt @@ -0,0 +1 @@ +{"id":"msg_8fbb284f-6c5a-4867-941c-533d902fbff9","model":"kimi-k2.6","object":"chat.completion","created":1779703331,"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/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/21-summary.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/21-summary.json new file mode 100644 index 00000000..f4e5e611 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/21-summary.json @@ -0,0 +1 @@ +{"artifact_dir": "/home/long/project/sub2api-cn-relay-manager/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm", "provider_id": "kimi-a7m", "batch_id": 1, "batch_status": "succeeded", "access_status_from_import": "subscription_ready", "provider_status_from_import": "active", "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": ["kimi-k2.6"], "upstream_models_has_expected_model": true, "upstream_chat_status": 200, "completion_classification": "unknown", "latest_access_status": "subscription_ready", "preview_available": true, "accepted_keys_count": 1, "subscription_group_id": "3", "import_group_id": "3"} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/00-local-key-source.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/00-local-key-source.json new file mode 100644 index 00000000..ebb15fa9 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/00-local-key-source.json @@ -0,0 +1,10 @@ +{ + "source": "file:/tmp/a7m_kimi_remote43_scripted.key", + "provider_id": "kimi-a7m", + "redacted": { + "present": true, + "prefix": "sk-F", + "suffix": "rqf2", + "fingerprint": "100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe" + } +} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/01-runtime-context.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/01-runtime-context.json new file mode 100644 index 00000000..d3641593 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/01-runtime-context.json @@ -0,0 +1,25 @@ +{ + "crm_base": "http://127.0.0.1:18173", + "host_base": "http://127.0.0.1:18169", + "crm_host_base": "http://127.0.0.1:18169", + "remote_host_base": "http://127.0.0.1:18169", + "provider_id": "kimi-a7m", + "subscription_group_id": "2", + "import_group_id": null, + "subscription_user_id_hash": "d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35", + "managed_user_id_hash": "4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce", + "admin_user_id_hash": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b", + "managed_user_email_hash": "ac10dfb7b18bd7397dc5cfb877667cd306c50f7aac2625c3ace9bb18faefbc5d", + "subscription_user_key": { + "present": true, + "prefix": "sk-1", + "suffix": "a131", + "fingerprint": "b89d31de2cb1531540d147312d693fd97fe56686ea093e68d2e91268ff581708" + }, + "managed_probe_key": { + "present": true, + "prefix": "sk-r", + "suffix": "84ee", + "fingerprint": "cd65766a8ecd833a5eb4cba4d21b89d319ee6028c63787b67c3c828c0c1ad597" + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/01a-create-host.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/01a-create-host.json new file mode 100644 index 00000000..a2f5076e --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/01a-create-host.json @@ -0,0 +1 @@ +{"host_id":"remote43-kimi-patched-auto2-18169","base_url":"http://127.0.0.1:18169","host_version":"0.1.126","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/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/02-import.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/02-import.headers.txt new file mode 100644 index 00000000..9ef9a292 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/02-import.headers.txt @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 25 May 2026 11:18:43 GMT +Content-Length: 1001 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/03-import.body.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/03-import.body.json new file mode 100644 index 00000000..3ee8065a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/03-import.body.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"access_status":"subscription_ready","accounts_count":1,"batch_id":27,"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_79dd04f9-bd9f-4d72-926f-754985adf3d1\",\"model\":\"kimi-k2.6\",\"object\":\"chat.completion\",\"created\":1779707923,\"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/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/04-batch-detail-initial.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/04-batch-detail-initial.json new file mode 100644 index 00000000..f82a899a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/04-batch-detail-initial.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":28,"BatchID":27,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_79dd04f9-bd9f-4d72-926f-754985adf3d1\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779707923,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:cd65766a8ecd833a5eb4cba4d21b89d319ee6028c63787b67c3c828c0c1ad597\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"requested_probe_api_key\":\"sk-1779707918-9648a131\",\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"2\"]}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":3,"id":27,"mode":"partial","pack_id":1,"provider_id":30},"items":[{"account_status":"warning","batch_id":27,"id":23,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"1\",\"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":4,"managed_resources":[{"ID":52,"BatchID":27,"HostID":3,"ResourceType":"group","HostResourceID":"2","ResourceName":"Kimi A7M 默认分组-subscription"},{"ID":53,"BatchID":27,"HostID":3,"ResourceType":"channel","HostResourceID":"1","ResourceName":"Kimi A7M 默认渠道-subscription"},{"ID":54,"BatchID":27,"HostID":3,"ResourceType":"plan","HostResourceID":"1","ResourceName":"Kimi A7M 默认套餐-subscription"},{"ID":55,"BatchID":27,"HostID":3,"ResourceType":"account","HostResourceID":"1","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/05-subscription-access-prep.summary.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/05-subscription-access-prep.summary.json new file mode 100644 index 00000000..cdf5b3bd --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/05-subscription-access-prep.summary.json @@ -0,0 +1,12 @@ +{ + "subscription_user_id_hash": "d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35", + "subscription_group_id": 2, + "min_balance": 10, + "subscription_days": 30, + "api_key": { + "present": true, + "prefix": "sk-1", + "suffix": "a131", + "fingerprint": "b89d31de2cb1531540d147312d693fd97fe56686ea093e68d2e91268ff581708" + } +} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/06-subscription-access-prep.psql.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/06-subscription-access-prep.psql.txt new file mode 100644 index 00000000..894b2e3a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/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/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/07-redis-targeted-invalidation.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/07-redis-targeted-invalidation.json new file mode 100644 index 00000000..85a0578a --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/07-redis-targeted-invalidation.json @@ -0,0 +1,6 @@ +{ + "auth_cache_invalidated": true, + "balance_cache_invalidated": true, + "subscription_cache_invalidated": true, + "redis_del_exit_code": 0 +} diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/08-subscription-group-state.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/08-subscription-group-state.json new file mode 100644 index 00000000..56ecc995 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/08-subscription-group-state.json @@ -0,0 +1,28 @@ +{ + "group_id": 2, + "group": { + "id": 2, + "name": "Kimi A7M 默认分组-subscription", + "type": null, + "subscription_type": "subscription" + }, + "subscription": { + "id": 1, + "user_id_hash": "4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce", + "group_id": 2, + "status": "active", + "starts_at": "2026-05-25T11:18:41.260181+00:00", + "expires_at": "2026-06-24T11:18:41.260181+00:00" + }, + "key": { + "id": 2, + "group_id": 2, + "status": "active", + "redacted": { + "present": true, + "prefix": "sk-r", + "suffix": "84ee", + "fingerprint": "cd65766a8ecd833a5eb4cba4d21b89d319ee6028c63787b67c3c828c0c1ad597" + } + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/09-models.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/09-models.headers.txt new file mode 100644 index 00000000..dfeb1872 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/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: 6c6486e3-57d7-422f-b38b-70234787b84d +Date: Mon, 25 May 2026 11:18:48 GMT +Content-Length: 123 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/10-models.body.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/10-models.body.json new file mode 100644 index 00000000..b9de987c --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/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/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/11-chat.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/11-chat.headers.txt new file mode 100644 index 00000000..9ada2812 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/11-chat.headers.txt @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 25 May 2026 11:18:52 GMT +Referrer-Policy: strict-origin-when-cross-origin +Vary: Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-Request-Id: 76138e76-5788-4397-9374-20441808e3d1 +Content-Length: 611 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/12-chat.body.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/12-chat.body.json new file mode 100644 index 00000000..2f44a0f8 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/12-chat.body.json @@ -0,0 +1 @@ +{"id":"msg_b18c66b4-fec6-4913-b6c9-cda11d867439","model":"kimi-k2.6","object":"chat.completion","created":1779707932,"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/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/13-provider-status.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/13-provider-status.json new file mode 100644 index 00000000..e69de29b diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/14-access-status.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/14-access-status.json new file mode 100644 index 00000000..e69de29b diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/17-upstream-models.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/17-upstream-models.headers.txt new file mode 100644 index 00000000..696c4dad --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/17-upstream-models.headers.txt @@ -0,0 +1,9 @@ +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json; charset=utf-8 +date: Mon, 25 May 2026 11:18:55 GMT +via: 1.1 Caddy +x-new-api-version: v0.0.0 +x-oneapi-request-id: 20260525111855869676966cFyctFEq +content-length: 168 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/18-upstream-models.body.json b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/18-upstream-models.body.json new file mode 100644 index 00000000..8369a64f --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/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/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/19-upstream-chat.headers.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/19-upstream-chat.headers.txt new file mode 100644 index 00000000..2222c8b9 --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/19-upstream-chat.headers.txt @@ -0,0 +1,14 @@ +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json +date: Mon, 25 May 2026 11:18:59 GMT +req-arrive-time: 1779707938835 +req-cost-time: 688 +resp-start-time: 1779707939524 +server: istio-envoy +via: 1.1 Caddy +x-envoy-upstream-service-time: 685 +x-new-api-version: v0.0.0 +x-oneapi-request-id: 202605251118587990475820LV4Uoie +content-length: 611 + diff --git a/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/20-upstream-chat.body.txt b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/20-upstream-chat.body.txt new file mode 100644 index 00000000..096dce3c --- /dev/null +++ b/artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/20-upstream-chat.body.txt @@ -0,0 +1 @@ +{"id":"msg_12fb41a1-774e-4f8c-9e5d-4681b2767795","model":"kimi-k2.6","object":"chat.completion","created":1779707939,"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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/00-local-key-source.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/00-local-key-source.json new file mode 100644 index 00000000..4cec7c4c --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/00-local-key-source.json @@ -0,0 +1,10 @@ +{ + "source": "file:/tmp/a7m_kimi_remote43_scripted_rerun.key", + "provider_id": "kimi-a7m", + "redacted": { + "present": true, + "prefix": "sk-F", + "suffix": "rqf2", + "fingerprint": "100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe" + } +} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/01-runtime-context.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/01-runtime-context.json new file mode 100644 index 00000000..9a49e3d1 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/01-runtime-context.json @@ -0,0 +1,25 @@ +{ + "crm_base": "http://127.0.0.1:18173", + "host_base": "http://127.0.0.1:18169", + "crm_host_base": "http://127.0.0.1:18169", + "remote_host_base": "http://127.0.0.1:18169", + "provider_id": "kimi-a7m", + "subscription_group_id": "2", + "import_group_id": null, + "subscription_user_id_hash": "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a", + "managed_user_id_hash": "ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d", + "admin_user_id_hash": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b", + "managed_user_email_hash": "d97102cbbf653fa4a9fbfadea21bbab5e669abed8909b3cf3f4839b88cb563fb", + "subscription_user_key": { + "present": true, + "prefix": "sk-1", + "suffix": "1a70", + "fingerprint": "f7d9206cedb8512acb4a879b9d7e06b8edd0d8d04fdb5d6999a1a73a54d3339f" + }, + "managed_probe_key": { + "present": true, + "prefix": "sk-r", + "suffix": "ef7f", + "fingerprint": "006bae7b24b9c44d5a506a19e770ac101448db74dffc6f7b07d8407d65c56d61" + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/01a-create-host.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/01a-create-host.json new file mode 100644 index 00000000..f1ac25b2 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/01a-create-host.json @@ -0,0 +1 @@ +{"host_id": "remote43-kimi-patched-auto2-18169", "base_url": "http://127.0.0.1:18169", "host_version": "0.1.126", "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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/02-import.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/02-import.headers.txt new file mode 100644 index 00000000..cda37f2c --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/02-import.headers.txt @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 25 May 2026 23:25:05 GMT +Content-Length: 1001 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/03-import.body.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/03-import.body.json new file mode 100644 index 00000000..07615661 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/03-import.body.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"access_status":"subscription_ready","accounts_count":1,"batch_id":28,"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_fd0da5fc-a4df-4025-94a6-3a2d9eb11322\",\"model\":\"kimi-k2.6\",\"object\":\"chat.completion\",\"created\":1779751505,\"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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/04-batch-detail-initial.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/04-batch-detail-initial.json new file mode 100644 index 00000000..d64d85c4 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/04-batch-detail-initial.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":29,"BatchID":28,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_fd0da5fc-a4df-4025-94a6-3a2d9eb11322\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779751505,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:006bae7b24b9c44d5a506a19e770ac101448db74dffc6f7b07d8407d65c56d61\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"requested_probe_api_key\":\"sk-1779751469-76741a70\",\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"4\"]}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":3,"id":28,"mode":"partial","pack_id":1,"provider_id":30},"items":[{"account_status":"warning","batch_id":28,"id":24,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"2\",\"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":56,"BatchID":28,"HostID":3,"ResourceType":"account","HostResourceID":"2","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/05-subscription-access-prep.summary.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/05-subscription-access-prep.summary.json new file mode 100644 index 00000000..2884ac31 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/05-subscription-access-prep.summary.json @@ -0,0 +1,12 @@ +{ + "subscription_user_id_hash": "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a", + "subscription_group_id": 2, + "min_balance": 10, + "subscription_days": 30, + "api_key": { + "present": true, + "prefix": "sk-1", + "suffix": "1a70", + "fingerprint": "f7d9206cedb8512acb4a879b9d7e06b8edd0d8d04fdb5d6999a1a73a54d3339f" + } +} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/06-subscription-access-prep.psql.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/06-subscription-access-prep.psql.txt new file mode 100644 index 00000000..894b2e3a --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/07-redis-targeted-invalidation.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/07-redis-targeted-invalidation.json new file mode 100644 index 00000000..85a0578a --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/07-redis-targeted-invalidation.json @@ -0,0 +1,6 @@ +{ + "auth_cache_invalidated": true, + "balance_cache_invalidated": true, + "subscription_cache_invalidated": true, + "redis_del_exit_code": 0 +} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/08-subscription-group-state.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/08-subscription-group-state.json new file mode 100644 index 00000000..863a529b --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/08-subscription-group-state.json @@ -0,0 +1,28 @@ +{ + "group_id": 2, + "group": { + "id": 2, + "name": "Kimi A7M 默认分组-subscription", + "type": null, + "subscription_type": "subscription" + }, + "subscription": { + "id": 3, + "user_id_hash": "ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d", + "group_id": 2, + "status": "active", + "starts_at": "2026-05-25T23:25:04.620653+00:00", + "expires_at": "2026-06-24T23:25:04.620653+00:00" + }, + "key": { + "id": 4, + "group_id": 2, + "status": "active", + "redacted": { + "present": true, + "prefix": "sk-r", + "suffix": "ef7f", + "fingerprint": "006bae7b24b9c44d5a506a19e770ac101448db74dffc6f7b07d8407d65c56d61" + } + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/09-models.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/09-models.headers.txt new file mode 100644 index 00000000..3ef4d9d6 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/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: dfa954a6-5680-407b-8cb8-a57508b1265d +Date: Mon, 25 May 2026 23:25:11 GMT +Content-Length: 123 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/10-models.body.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/10-models.body.json new file mode 100644 index 00000000..b9de987c --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/11-chat.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/11-chat.headers.txt new file mode 100644 index 00000000..979c51cb --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/11-chat.headers.txt @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 25 May 2026 23:25:15 GMT +Referrer-Policy: strict-origin-when-cross-origin +Vary: Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-Request-Id: 3cf25033-5f37-4453-8a41-0df4b2f287f9 +Content-Length: 611 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/12-chat.body.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/12-chat.body.json new file mode 100644 index 00000000..df201e4d --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/12-chat.body.json @@ -0,0 +1 @@ +{"id":"msg_d648c0f0-0b54-4417-9cc2-da1ef8e3b324","model":"kimi-k2.6","object":"chat.completion","created":1779751515,"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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/13-provider-status.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/13-provider-status.json new file mode 100644 index 00000000..e69de29b diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/17-upstream-models.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/17-upstream-models.headers.txt new file mode 100644 index 00000000..144892c7 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/17-upstream-models.headers.txt @@ -0,0 +1,9 @@ +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json; charset=utf-8 +date: Mon, 25 May 2026 23:25:18 GMT +via: 1.1 Caddy +x-new-api-version: v0.0.0 +x-oneapi-request-id: 20260525232518480884749pxkavNM2 +content-length: 168 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/18-upstream-models.body.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/18-upstream-models.body.json new file mode 100644 index 00000000..8369a64f --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/19-upstream-chat.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/19-upstream-chat.headers.txt new file mode 100644 index 00000000..3cc6ccaf --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/19-upstream-chat.headers.txt @@ -0,0 +1,14 @@ +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json +date: Mon, 25 May 2026 23:25:21 GMT +req-arrive-time: 1779751521759 +req-cost-time: 640 +resp-start-time: 1779751522399 +server: istio-envoy +via: 1.1 Caddy +x-envoy-upstream-service-time: 636 +x-new-api-version: v0.0.0 +x-oneapi-request-id: 20260525232521486387931DDTbPGBS +content-length: 611 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/20-upstream-chat.body.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/20-upstream-chat.body.txt new file mode 100644 index 00000000..e916da79 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun/20-upstream-chat.body.txt @@ -0,0 +1 @@ +{"id":"msg_a545f3f2-6fdd-4a9b-af41-14a7d2d73ac9","model":"kimi-k2.6","object":"chat.completion","created":1779751522,"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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/00-local-key-source.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/00-local-key-source.json new file mode 100644 index 00000000..4cec7c4c --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/00-local-key-source.json @@ -0,0 +1,10 @@ +{ + "source": "file:/tmp/a7m_kimi_remote43_scripted_rerun.key", + "provider_id": "kimi-a7m", + "redacted": { + "present": true, + "prefix": "sk-F", + "suffix": "rqf2", + "fingerprint": "100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe" + } +} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/01-runtime-context.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/01-runtime-context.json new file mode 100644 index 00000000..35a5f51a --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/01-runtime-context.json @@ -0,0 +1,25 @@ +{ + "crm_base": "http://127.0.0.1:18173", + "host_base": "http://127.0.0.1:18169", + "crm_host_base": "http://127.0.0.1:18169", + "remote_host_base": "http://127.0.0.1:18169", + "provider_id": "kimi-a7m", + "subscription_group_id": "2", + "import_group_id": null, + "subscription_user_id_hash": "e7f6c011776e8db7cd330b54174fd76f7d0216b612387a5ffcfb81e6f0919683", + "managed_user_id_hash": "7902699be42c8a8e46fbbb4501726517e86b22c56a189f7625a6da49081b2451", + "admin_user_id_hash": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b", + "managed_user_email_hash": "75bd03af61ec28411e993a158d9a7bfbdf8e2ac3eb62c31ee30471516b71677a", + "subscription_user_key": { + "present": true, + "prefix": "sk-1", + "suffix": "8d43", + "fingerprint": "33696e41d107008a47111253a112828cf3e39612517d19697c62a2f69f3ee42e" + }, + "managed_probe_key": { + "present": true, + "prefix": "sk-r", + "suffix": "aff1", + "fingerprint": "f3c2e7f29ada8dc1dcf71a118933998a1d01e1190038dccacbb8ead3c114fadb" + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/01a-create-host.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/01a-create-host.json new file mode 100644 index 00000000..f1ac25b2 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/01a-create-host.json @@ -0,0 +1 @@ +{"host_id": "remote43-kimi-patched-auto2-18169", "base_url": "http://127.0.0.1:18169", "host_version": "0.1.126", "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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/02-import.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/02-import.headers.txt new file mode 100644 index 00000000..e0ca1737 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/02-import.headers.txt @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 25 May 2026 23:27:15 GMT +Content-Length: 1001 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/03-import.body.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/03-import.body.json new file mode 100644 index 00000000..1505d010 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/03-import.body.json @@ -0,0 +1 @@ +{"accepted_keys_count":1,"access_status":"subscription_ready","accounts_count":1,"batch_id":29,"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_2d1845cb-3ae7-462c-a9a0-acbc658ae650\",\"model\":\"kimi-k2.6\",\"object\":\"chat.completion\",\"created\":1779751635,\"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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/04-batch-detail-initial.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/04-batch-detail-initial.json new file mode 100644 index 00000000..70a05659 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/04-batch-detail-initial.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":30,"BatchID":29,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_2d1845cb-3ae7-462c-a9a0-acbc658ae650\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779751635,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:f3c2e7f29ada8dc1dcf71a118933998a1d01e1190038dccacbb8ead3c114fadb\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"requested_probe_api_key\":\"sk-1779751632-d7788d43\",\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"6\"]}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":3,"id":29,"mode":"partial","pack_id":1,"provider_id":30},"items":[{"account_status":"warning","batch_id":29,"id":25,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"3\",\"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":57,"BatchID":29,"HostID":3,"ResourceType":"account","HostResourceID":"3","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/05-subscription-access-prep.summary.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/05-subscription-access-prep.summary.json new file mode 100644 index 00000000..e9676b99 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/05-subscription-access-prep.summary.json @@ -0,0 +1,12 @@ +{ + "subscription_user_id_hash": "e7f6c011776e8db7cd330b54174fd76f7d0216b612387a5ffcfb81e6f0919683", + "subscription_group_id": 2, + "min_balance": 10, + "subscription_days": 30, + "api_key": { + "present": true, + "prefix": "sk-1", + "suffix": "8d43", + "fingerprint": "33696e41d107008a47111253a112828cf3e39612517d19697c62a2f69f3ee42e" + } +} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/06-subscription-access-prep.psql.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/06-subscription-access-prep.psql.txt new file mode 100644 index 00000000..894b2e3a --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/07-redis-targeted-invalidation.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/07-redis-targeted-invalidation.json new file mode 100644 index 00000000..85a0578a --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/07-redis-targeted-invalidation.json @@ -0,0 +1,6 @@ +{ + "auth_cache_invalidated": true, + "balance_cache_invalidated": true, + "subscription_cache_invalidated": true, + "redis_del_exit_code": 0 +} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/08-subscription-group-state.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/08-subscription-group-state.json new file mode 100644 index 00000000..5af1c412 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/08-subscription-group-state.json @@ -0,0 +1,28 @@ +{ + "group_id": 2, + "group": { + "id": 2, + "name": "Kimi A7M 默认分组-subscription", + "type": null, + "subscription_type": "subscription" + }, + "subscription": { + "id": 5, + "user_id_hash": "7902699be42c8a8e46fbbb4501726517e86b22c56a189f7625a6da49081b2451", + "group_id": 2, + "status": "active", + "starts_at": "2026-05-25T23:27:14.343291+00:00", + "expires_at": "2026-06-24T23:27:14.343291+00:00" + }, + "key": { + "id": 6, + "group_id": 2, + "status": "active", + "redacted": { + "present": true, + "prefix": "sk-r", + "suffix": "aff1", + "fingerprint": "f3c2e7f29ada8dc1dcf71a118933998a1d01e1190038dccacbb8ead3c114fadb" + } + } +} \ No newline at end of file diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/09-models.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/09-models.headers.txt new file mode 100644 index 00000000..5f7aee99 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/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: 9cb065cc-d37b-466a-a244-d45a3ca6479b +Date: Mon, 25 May 2026 23:27:21 GMT +Content-Length: 123 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/10-models.body.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/10-models.body.json new file mode 100644 index 00000000..b9de987c --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/11-chat.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/11-chat.headers.txt new file mode 100644 index 00000000..9133f082 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/11-chat.headers.txt @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 25 May 2026 23:27:24 GMT +Referrer-Policy: strict-origin-when-cross-origin +Vary: Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-Request-Id: 9d184a57-3c7c-4f08-a508-7bff17bb2ba9 +Content-Length: 611 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/12-chat.body.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/12-chat.body.json new file mode 100644 index 00000000..7ac28cfb --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/12-chat.body.json @@ -0,0 +1 @@ +{"id":"msg_cc822589-2448-4c07-b5ff-20c748a2212c","model":"kimi-k2.6","object":"chat.completion","created":1779751645,"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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/13-provider-status.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/13-provider-status.json new file mode 100644 index 00000000..c825b01f --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/13-provider-status.json @@ -0,0 +1 @@ +{"access_closures_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","id":29,"mode":"partial"},"host":{"base_url":"http://127.0.0.1:18169","host_id":"remote43-kimi-patched-auto2-18169","host_version":"0.1.126"},"latest_access_status":"subscription_ready","latest_reconcile_status":"not_run","latest_reconcile_summary":{},"managed_resources_count":1,"pack":{"pack_id":"openai-cn-pack","version":"1.1.3"},"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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/14-access-status.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/14-access-status.json new file mode 100644 index 00000000..82c88e33 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/14-access-status.json @@ -0,0 +1 @@ +{"batch_access_status":"subscription_ready","batch_id":29,"closures_count":1,"latest_access_status":"subscription_ready","latest_closure":{"closure_type":"subscription","details_json":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_2d1845cb-3ae7-462c-a9a0-acbc658ae650\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779751635,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:f3c2e7f29ada8dc1dcf71a118933998a1d01e1190038dccacbb8ead3c114fadb\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"requested_probe_api_key\":\"sk-1779751632-d7788d43\",\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"6\"]}","id":30,"status":"subscription_ready"},"pack_id":"openai-cn-pack","provider_id":"kimi-a7m"} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/15-access-preview.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/15-access-preview.json new file mode 100644 index 00000000..56e78371 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/16-batch-detail-final.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/16-batch-detail-final.json new file mode 100644 index 00000000..70a05659 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/16-batch-detail-final.json @@ -0,0 +1 @@ +{"access_closures":[{"ID":30,"BatchID":29,"ClosureType":"subscription","Status":"subscription_ready","DetailsJSON":"{\"completion_ok\":true,\"completion_preview\":\"{\\\"id\\\":\\\"msg_2d1845cb-3ae7-462c-a9a0-acbc658ae650\\\",\\\"model\\\":\\\"kimi-k2.6\\\",\\\"object\\\":\\\"chat.completion\\\",\\\"created\\\":1779751635,\\\"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\",\"effective_probe_key_fingerprint\":\"sha256:f3c2e7f29ada8dc1dcf71a118933998a1d01e1190038dccacbb8ead3c114fadb\",\"effective_probe_key_source\":\"managed_subscription\",\"has_expected_model\":true,\"models\":[\"kimi-k2.6\"],\"ok\":true,\"requested_probe_api_key\":\"sk-1779751632-d7788d43\",\"status_code\":200,\"subscription_days\":30,\"subscription_users\":[\"6\"]}"}],"access_count":1,"batch":{"access_status":"subscription_ready","batch_status":"succeeded","host_id":3,"id":29,"mode":"partial","pack_id":1,"provider_id":30},"items":[{"account_status":"warning","batch_id":29,"id":25,"key_fingerprint":"sha256:100830605ca92c766278d12cea58d6fc5d5eb27902a491d6c8e4fe13900a3bbe","probe_summary_json":"{\"account_id\":\"3\",\"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":57,"BatchID":29,"HostID":3,"ResourceType":"account","HostResourceID":"3","ResourceName":"kimi-a7m-01"}],"reconcile_count":0,"reconcile_runs":[]} diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/17-upstream-models.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/17-upstream-models.headers.txt new file mode 100644 index 00000000..7c5704d4 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/17-upstream-models.headers.txt @@ -0,0 +1,9 @@ +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json; charset=utf-8 +date: Mon, 25 May 2026 23:27:28 GMT +via: 1.1 Caddy +x-new-api-version: v0.0.0 +x-oneapi-request-id: 2026052523272879753567GywKwNDN +content-length: 168 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/18-upstream-models.body.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/18-upstream-models.body.json new file mode 100644 index 00000000..8369a64f --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/19-upstream-chat.headers.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/19-upstream-chat.headers.txt new file mode 100644 index 00000000..fe8b3966 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/19-upstream-chat.headers.txt @@ -0,0 +1,14 @@ +HTTP/2 200 +alt-svc: h3=":443"; ma=2592000 +content-type: application/json +date: Mon, 25 May 2026 23:27:31 GMT +req-arrive-time: 1779751651247 +req-cost-time: 523 +resp-start-time: 1779751651770 +server: istio-envoy +via: 1.1 Caddy +x-envoy-upstream-service-time: 519 +x-new-api-version: v0.0.0 +x-oneapi-request-id: 20260525232731212275335pxEWNfIr +content-length: 611 + diff --git a/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/20-upstream-chat.body.txt b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/20-upstream-chat.body.txt new file mode 100644 index 00000000..08e4c052 --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/20-upstream-chat.body.txt @@ -0,0 +1 @@ +{"id":"msg_913e5187-b418-41d0-a04c-9b01a5411659","model":"kimi-k2.6","object":"chat.completion","created":1779751651,"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/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/21-summary.json b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/21-summary.json new file mode 100644 index 00000000..f509167b --- /dev/null +++ b/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/21-summary.json @@ -0,0 +1 @@ +{"artifact_dir": "/home/long/project/sub2api-cn-relay-manager/artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2", "provider_id": "kimi-a7m", "batch_id": 29, "batch_status": "succeeded", "access_status_from_import": "subscription_ready", "provider_status_from_import": "active", "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": ["kimi-k2.6"], "upstream_models_has_expected_model": true, "upstream_chat_status": 200, "completion_classification": "unknown", "latest_access_status": "subscription_ready", "preview_available": true, "accepted_keys_count": 1, "subscription_group_id": "2", "import_group_id": "2"} \ No newline at end of file diff --git a/cmd/cli/apply_host_overlay.go b/cmd/cli/apply_host_overlay.go new file mode 100644 index 00000000..2a21c08e --- /dev/null +++ b/cmd/cli/apply_host_overlay.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "strings" + + "sub2api-cn-relay-manager/internal/overlay" + "sub2api-cn-relay-manager/internal/pack" +) + +type applyHostOverlayFunc func(context.Context, applyHostOverlayCLIRequest) (overlay.ApplyResult, error) + +type applyHostOverlayCLIRequest struct { + PackDir string + ProviderID string + HostVersion string + SourceDir string + OutputDir string + TargetHost string + OverlayID string +} + +func parseApplyHostOverlayCLIArgs(args []string) (applyHostOverlayCLIRequest, error) { + fs := flag.NewFlagSet("apply-host-overlay", flag.ContinueOnError) + fs.SetOutput(io.Discard) + + var req applyHostOverlayCLIRequest + fs.StringVar(&req.PackDir, "pack-dir", "", "") + fs.StringVar(&req.ProviderID, "provider-id", "", "") + fs.StringVar(&req.HostVersion, "host-version", "", "") + fs.StringVar(&req.SourceDir, "source-dir", "", "") + fs.StringVar(&req.OutputDir, "output-dir", "", "") + fs.StringVar(&req.TargetHost, "target-host", "", "") + fs.StringVar(&req.OverlayID, "overlay-id", "", "") + if err := fs.Parse(args); err != nil { + return applyHostOverlayCLIRequest{}, err + } + + switch { + case strings.TrimSpace(req.PackDir) == "": + return applyHostOverlayCLIRequest{}, fmt.Errorf("--pack-dir is required") + case strings.TrimSpace(req.ProviderID) == "": + return applyHostOverlayCLIRequest{}, fmt.Errorf("--provider-id is required") + case strings.TrimSpace(req.HostVersion) == "": + return applyHostOverlayCLIRequest{}, fmt.Errorf("--host-version is required") + case strings.TrimSpace(req.SourceDir) == "": + return applyHostOverlayCLIRequest{}, fmt.Errorf("--source-dir is required") + } + return req, nil +} + +func runApplyHostOverlay(ctx context.Context, req applyHostOverlayCLIRequest) (overlay.ApplyResult, error) { + loadedPack, err := pack.LoadDir(req.PackDir) + if err != nil { + return overlay.ApplyResult{}, err + } + providerManifest, err := findProvider(loadedPack, req.ProviderID) + if err != nil { + return overlay.ApplyResult{}, err + } + + targetHost := strings.TrimSpace(req.TargetHost) + if targetHost == "" { + targetHost = loadedPack.Manifest.TargetHost + } + resolvedOverlays, err := pack.ResolveApplicableHostOverlays(providerManifest, targetHost, req.HostVersion) + if err != nil { + return overlay.ApplyResult{}, err + } + if len(resolvedOverlays) == 0 { + return overlay.ApplyResult{}, fmt.Errorf("no host overlays matched provider %q for host %q version %q", req.ProviderID, targetHost, req.HostVersion) + } + filteredOverlays, err := overlay.FilterOverlays(resolvedOverlays, req.OverlayID) + if err != nil { + return overlay.ApplyResult{}, err + } + + return overlay.Apply(ctx, overlay.ApplyRequest{ + PackDir: loadedPack.Dir, + SourceDir: req.SourceDir, + OutputDir: req.OutputDir, + Overlays: filteredOverlays, + }) +} diff --git a/cmd/cli/apply_host_overlay_test.go b/cmd/cli/apply_host_overlay_test.go new file mode 100644 index 00000000..6b9c7b51 --- /dev/null +++ b/cmd/cli/apply_host_overlay_test.go @@ -0,0 +1,115 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRunApplyHostOverlayAppliesResolvedPackOverlay(t *testing.T) { + sourceDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + if err := os.WriteFile(filepath.Join(sourceDir, "backend", "hello.txt"), []byte("hello\n"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + packDir := createApplyHostOverlayPackFixture(t) + result, err := runApplyHostOverlay(context.Background(), applyHostOverlayCLIRequest{ + PackDir: packDir, + ProviderID: "kimi-a7m", + HostVersion: "0.1.129", + SourceDir: sourceDir, + }) + if err != nil { + t.Fatalf("runApplyHostOverlay() error = %v", err) + } + + body, err := os.ReadFile(filepath.Join(result.OutputDir, "backend", "hello.txt")) + if err != nil { + t.Fatalf("ReadFile() error = %v", err) + } + if string(body) != "patched\n" { + t.Fatalf("patched file = %q, want %q", string(body), "patched\n") + } + if _, err := os.Stat(result.MetadataFilePath); err != nil { + t.Fatalf("Stat(metadata) error = %v", err) + } +} + +func createApplyHostOverlayPackFixture(t *testing.T) string { + t.Helper() + + packDir := t.TempDir() + files := map[string]string{ + "pack.json": `{ + "pack_id": "openai-cn-pack", + "version": "1.1.3", + "vendor": "YourTeam", + "target_host": "sub2api", + "min_host_version": "0.1.126", + "max_host_version": "0.2.x", + "providers_dir": "providers", + "checksum_file": "checksums.txt" +}`, + "providers/kimi-a7m.json": `{ + "provider_id": "kimi-a7m", + "display_name": "Kimi A7M OpenAI Compatible", + "base_url": "https://kimi.a7m.com.cn/v1", + "platform": "openai", + "account_type": "apikey", + "default_models": ["kimi-k2.6"], + "smoke_test_model": "kimi-k2.6", + "host_overlays": [ + { + "overlay_id": "sub2api-stock-v0129-kimi-a7m", + "display_name": "sub2api stock v0.1.129 Kimi A7M overlay", + "target_host": "sub2api", + "min_host_version": "0.1.129", + "max_host_version": "0.1.129", + "apply_mode": "patch", + "patch_path": "overlays/kimi-a7m-sub2api-v0.1.129.patch", + "notes_path": "overlays/kimi-a7m-sub2api-v0.1.129.md", + "reason": "stock host still routes chat traffic into unsupported Responses path" + } + ], + "group_template": {"name": "g", "rate_multiplier": 1}, + "channel_template": {"name": "c", "model_mapping": {"kimi-k2.6": "kimi-k2.6"}}, + "plan_template": {"name": "p", "price": 1, "validity_days": 30, "validity_unit": "day"}, + "import": {"supports_multi_key": true, "supports_strict": true, "supports_partial": true} +}`, + "overlays/kimi-a7m-sub2api-v0.1.129.patch": strings.Join([]string{ + "diff --git a/backend/hello.txt b/backend/hello.txt", + "--- a/backend/hello.txt", + "+++ b/backend/hello.txt", + "@@ -1 +1 @@", + "-hello", + "+patched", + "", + }, "\n"), + "overlays/kimi-a7m-sub2api-v0.1.129.md": "# overlay\n", + } + + checksumLines := make([]string, 0, len(files)) + for relativePath, body := range files { + absolutePath := filepath.Join(packDir, relativePath) + if err := os.MkdirAll(filepath.Dir(absolutePath), 0o755); err != nil { + t.Fatalf("MkdirAll(%q) error = %v", absolutePath, err) + } + if err := os.WriteFile(absolutePath, []byte(body), 0o644); err != nil { + t.Fatalf("WriteFile(%q) error = %v", absolutePath, err) + } + sum := sha256.Sum256([]byte(body)) + checksumLines = append(checksumLines, hex.EncodeToString(sum[:])+" "+relativePath) + } + if err := os.WriteFile(filepath.Join(packDir, "checksums.txt"), []byte(strings.Join(checksumLines, "\n")+"\n"), 0o644); err != nil { + t.Fatalf("WriteFile(checksums.txt) error = %v", err) + } + + return packDir +} diff --git a/cmd/cli/batch_import_test.go b/cmd/cli/batch_import_test.go index 9f791326..549d5d74 100644 --- a/cmd/cli/batch_import_test.go +++ b/cmd/cli/batch_import_test.go @@ -93,7 +93,7 @@ func TestBatchImportCLI(t *testing.T) { "--access-mode", "self_service", "--probe-api-key", "gateway-key", "--confirm-timeout", "15s", - }, nil, nil, nil, nil, nil, nil, func(_ context.Context, req batchImportCLIRequest) (batchImportCLIResult, error) { + }, nil, nil, nil, nil, nil, nil, nil, func(_ context.Context, req batchImportCLIRequest) (batchImportCLIResult, error) { batchImportCalled = true if req.HostID != "host-1" { t.Fatalf("HostID = %q, want host-1", req.HostID) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index f0779328..63f311f3 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -81,7 +81,7 @@ type rollbackSummary struct { func main() { if err := execute(context.Background(), log.Writer(), os.Args[1:], func(context.Context) (config.StartupConfig, error) { return config.LoadStartupFromEnv() - }, runInstallPack, runImportProvider, runPreviewProvider, runRollbackProvider, runReconcileProvider, runBatchImport); err != nil { + }, runInstallPack, runImportProvider, runPreviewProvider, runRollbackProvider, runReconcileProvider, runApplyHostOverlay, runBatchImport); err != nil { log.Fatalf("run cli: %v", err) } } @@ -96,6 +96,7 @@ func execute( previewProvider previewProviderFunc, rollbackProvider rollbackProviderFunc, reconcileProvider reconcileProviderFunc, + applyHostOverlay applyHostOverlayFunc, batchImport batchImportFunc, ) error { if len(args) > 0 && args[0] == "batch-import" { @@ -171,6 +172,22 @@ func execute( _, err = fmt.Fprintf(output, "status=%s\nmissing_count=%d\nextra_count=%d\nprobe_failures=%d\naccess_status=%s\n", result.Status, result.MissingCount, result.ExtraCount, result.ProbeFailureCount, result.AccessStatus) return err } + if len(args) > 0 && args[0] == "apply-host-overlay" { + req, err := parseApplyHostOverlayCLIArgs(args[1:]) + if err != nil { + return err + } + result, err := applyHostOverlay(ctx, req) + if err != nil { + return err + } + overlayIDs := make([]string, 0, len(result.AppliedOverlays)) + for _, hostOverlay := range result.AppliedOverlays { + overlayIDs = append(overlayIDs, hostOverlay.OverlayID) + } + _, err = fmt.Fprintf(output, "output_dir=%s\napplied_overlays=%d\noverlay_ids=%s\nmetadata_file=%s\n", result.OutputDir, len(result.AppliedOverlays), strings.Join(overlayIDs, ","), result.MetadataFilePath) + return err + } cfg, err := loadConfig(ctx) if err != nil { @@ -410,10 +427,16 @@ func runPreviewProvider(ctx context.Context, req previewCLIRequest) (provision.P } service := provision.NewPreviewService(client) + hostVersion, err := client.GetHostVersion(ctx) + if err != nil { + return provision.PreviewReport{}, err + } return service.PreviewImport(ctx, provision.PreviewRequest{ - Provider: providerManifest, - Mode: req.Mode, - Keys: req.Keys, + TargetHost: loadedPack.Manifest.TargetHost, + HostVersion: hostVersion, + Provider: providerManifest, + Mode: req.Mode, + Keys: req.Keys, }) } diff --git a/cmd/cli/main_test.go b/cmd/cli/main_test.go index 91845e2d..21fcf23f 100644 --- a/cmd/cli/main_test.go +++ b/cmd/cli/main_test.go @@ -8,6 +8,8 @@ import ( "testing" "sub2api-cn-relay-manager/internal/config" + "sub2api-cn-relay-manager/internal/overlay" + "sub2api-cn-relay-manager/internal/pack" "sub2api-cn-relay-manager/internal/provision" "sub2api-cn-relay-manager/internal/reconcile" "sub2api-cn-relay-manager/internal/store/sqlite" @@ -33,7 +35,7 @@ func TestExecuteWritesConfigSummaryAfterBootstrap(t *testing.T) { SQLiteDSN: "file:test.db?_foreign_keys=on", }, }, nil - }, nil, nil, nil, nil, nil, nil) + }, nil, nil, nil, nil, nil, nil, nil) if err != nil { t.Fatalf("execute() returned error: %v", err) } @@ -61,7 +63,7 @@ func TestExecuteReturnsBootstrapError(t *testing.T) { err := execute(context.Background(), &bytes.Buffer{}, nil, func(context.Context) (config.StartupConfig, error) { return config.StartupConfig{}, wantErr - }, nil, nil, nil, nil, nil, nil) + }, nil, nil, nil, nil, nil, nil, nil) if !errors.Is(err, wantErr) { t.Fatalf("execute() error = %v, want %v", err, wantErr) } @@ -75,7 +77,7 @@ func TestExecuteReturnsWriteError(t *testing.T) { Server: config.ServerConfig{ListenAddr: ":9292"}, Database: config.DatabaseConfig{SQLiteDSN: "file:test.db"}, }, nil - }, nil, nil, nil, nil, nil, nil) + }, nil, nil, nil, nil, nil, nil, nil) if !errors.Is(err, wantErr) { t.Fatalf("execute() error = %v, want %v", err, wantErr) } @@ -99,7 +101,7 @@ func TestExecuteInstallPackWritesSummary(t *testing.T) { HostVersion: "0.1.126", Providers: []sqlite.Provider{{ProviderID: "deepseek"}}, }, nil - }, nil, nil, nil, nil, nil) + }, nil, nil, nil, nil, nil, nil) if err != nil { t.Fatalf("execute() install-pack error = %v", err) } @@ -134,7 +136,7 @@ func TestExecuteImportProviderWritesSummary(t *testing.T) { AccessStatus: provision.AccessStatusSelfServiceReady, Accounts: []provision.AccountImportResult{{}, {}}, }, nil - }, nil, nil, nil, nil) + }, nil, nil, nil, nil, nil) if err != nil { t.Fatalf("execute() import error = %v", err) } @@ -171,7 +173,7 @@ func TestExecutePreviewProviderWritesSummary(t *testing.T) { "plan": {Action: provision.PreviewActionConflict}, }, }, nil - }, nil, nil, nil) + }, nil, nil, nil, nil) if err != nil { t.Fatalf("execute() preview error = %v", err) } @@ -199,7 +201,7 @@ func TestExecuteRollbackProviderWritesSummary(t *testing.T) { t.Fatalf("unexpected rollback request: %+v", req) } return rollbackSummary{Accounts: 2, Plans: 1, Channels: 1, Groups: 1}, nil - }, nil, nil) + }, nil, nil, nil) if err != nil { t.Fatalf("execute() rollback error = %v", err) } @@ -228,7 +230,7 @@ func TestExecuteReconcileProviderWritesSummary(t *testing.T) { t.Fatalf("unexpected reconcile request: %+v", req) } return reconcile.Result{Status: "drifted", MissingCount: 1, ExtraCount: 2, ProbeFailureCount: 1, AccessStatus: provision.AccessStatusBroken}, nil - }, nil) + }, nil, nil) if err != nil { t.Fatalf("execute() reconcile error = %v", err) } @@ -241,6 +243,48 @@ func TestExecuteReconcileProviderWritesSummary(t *testing.T) { } } +func TestExecuteApplyHostOverlayWritesSummary(t *testing.T) { + var output bytes.Buffer + applyCalled := false + + err := execute(context.Background(), &output, []string{ + "apply-host-overlay", + "--pack-dir", "/tmp/pack", + "--provider-id", "kimi-a7m", + "--host-version", "0.1.129", + "--source-dir", "/tmp/sub2api-src", + }, nil, nil, nil, nil, nil, nil, func(_ context.Context, req applyHostOverlayCLIRequest) (overlay.ApplyResult, error) { + applyCalled = true + if req.ProviderID != "kimi-a7m" || req.HostVersion != "0.1.129" { + t.Fatalf("unexpected apply-host-overlay request: %+v", req) + } + return overlay.ApplyResult{ + OutputDir: "/tmp/sub2api-src-patched-sub2api-stock-v0129-kimi-a7m", + AppliedOverlays: []pack.HostOverlay{{ + OverlayID: "sub2api-stock-v0129-kimi-a7m", + }}, + MetadataFilePath: "/tmp/sub2api-src-patched-sub2api-stock-v0129-kimi-a7m/.sub2api-cn-relay-manager-overlay.json", + }, nil + }, nil) + if err != nil { + t.Fatalf("execute() apply-host-overlay error = %v", err) + } + if !applyCalled { + t.Fatal("execute() did not invoke applyHostOverlay") + } + got := output.String() + if !strings.Contains(got, "applied_overlays=1") || !strings.Contains(got, "overlay_ids=sub2api-stock-v0129-kimi-a7m") { + t.Fatalf("execute() apply-host-overlay output = %q, want overlay summary", got) + } +} + +func TestParseApplyHostOverlayCLIArgsRequiresSourceDir(t *testing.T) { + _, err := parseApplyHostOverlayCLIArgs([]string{"--pack-dir", "/tmp/pack", "--provider-id", "kimi-a7m", "--host-version", "0.1.129"}) + if err == nil { + t.Fatal("parseApplyHostOverlayCLIArgs() error = nil, want validation error") + } +} + func TestParseInstallPackCLIArgsRequiresHostBaseURL(t *testing.T) { _, err := parseInstallPackCLIArgs([]string{"--pack-path", "/tmp/openai-pack.zip"}) if err == nil { diff --git a/docs/EXECUTION_BOARD.md b/docs/EXECUTION_BOARD.md index 34c8dbc4..ebd21c2d 100644 --- a/docs/EXECUTION_BOARD.md +++ b/docs/EXECUTION_BOARD.md @@ -17,12 +17,57 @@ - `artifacts/real-host-acceptance/20260521_210403/07-access-status.json` - latest-head relay-manager 已新增宿主 capability 自愈: - 当第三方 OpenAI-compatible upstream 因宿主把 `openai_responses_supported` 误判成 `true` 而导致 host `/v1/chat/completions` 返回 `502 upstream_error` 时,access closure 与后台 reconcile 会自动把相关 account 修正到 raw `/chat/completions` 路径后再重试 - - 该修正现在不再依赖宿主长期保留补丁,宿主升级后只要下次 import/access/reconcile 触发,就能重新收敛到正确 capability + - 但这条控制面自愈当前仍不足以单独收敛本地 stock `weishaw/sub2api:0.1.129` + `kimi-a7m` 场景;`artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/21-summary.json` 已再次证明:在不改宿主源码的前提下,managed `/v1/models` 虽然命中 `kimi-k2.6`,`/v1/chat/completions` 仍会落到 `502 upstream_error`,所以该 case 仍需宿主运行时兼容补丁或 shim - 2026-05-23 remote43 线上验收脚本已继续收口: - `scripts/import_remote43_provider.sh` 现已明确拆分 `CRM_HOST_BASE` 与 `REMOTE_HOST_BASE` - 远端 Postgres / Redis 容器已改成按目标宿主端口自动解析,不再硬编码落到 `sub2api-relaymgr-pg/redis` - 远端 managed probe `/v1/models` 与 `/v1/chat/completions` 已改成只走 `REMOTE_HOST_BASE` - provider status / access status / access preview 末尾查询已补 `host_id`,避免本地 CRM 有多宿主历史时被 `provider exists on multiple hosts` 截断 +- 2026-05-25 已把 Hermes 里可复用的 `a7m-kimi` 正式并入主 pack: + - 新增 `packs/openai-cn-pack/providers/kimi-a7m.json` + - `openai-cn-pack` 版本现为 `1.1.3` + - 当前主仓不再需要依赖历史临时 pack `openai-cn-pack-kimi-a7m` + - `kimi-a7m` provider manifest 现在也开始承载 `host_overlays` 元数据;本地已把 `sub2api v0.1.129` 的 Kimi A7M runtime overlay 说明与 `.patch` 资产纳入 `packs/openai-cn-pack/overlays/` + - 新增 `go run ./cmd/cli apply-host-overlay` 最小执行器;当前 pack 内命中的 overlay 已可直接生成 patched 宿主构建目录,不再只是 preview/import 阶段的提示信息 + - 2026-05-25 已继续把路线 A 推进到运行态层面: + - 从 `/tmp/sub2api-clean` 的 clean worktree `HEAD` 导出 stock 源码,再用 `go run ./cmd/cli apply-host-overlay --provider-id kimi-a7m --host-version 0.1.129` 生成全新 patched 源码树 + - 基于该 patched 源码树重建 `localhost/sub2api:patched-overlay-20260525-clean`,并在独立 Podman 网络里启动新的 Postgres / Redis / App fresh-host + - `artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_patched_overlay_image_freshhost_clean/21-summary.json` 已确认:`import_batch_status=succeeded`、`provider_status=active`、`latest_access_status=subscription_ready`、`completion_ok=true`、`completion_status=200` + - 同目录 `07-access-status.json` 与 patched host 运行日志已共同证明 managed subscription key 真实打通 `/v1/models` 与 `POST /v1/chat/completions` + - 注意:该 fresh-host 使用的镜像基底仍是 `weishaw/sub2api:0.1.129`,但宿主管理 API 当前自报 `host_version=0.1.126`;后续读 artifact 时应以日期和证据链为准,不要只依赖版本字段 + - 2026-05-25 已把同一条 patched overlay 路线放到 remote43 做线上验收: + - remote43 侧单独拉起了 `sub2api-kimi-patched-20260525-{app,pg,redis}`,patched host 暴露 `127.0.0.1:18139` + - 临时 CRM 也切到 remote43 本机运行,再通过 SSH 隧道映射回本地 `127.0.0.1:18143`,避免“本地 CRM 透过隧道探远端 host”导致的 `get host version` 超时噪音 + - `artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_freshhost_remotecrm/21-summary.json` 已确认:`batch_status=succeeded`、`access_status_from_import=subscription_ready`、`provider_status_from_import=active`、`direct_models_http200=true`、`direct_chat_http200=true`、`upstream_chat_status=200` + - 同目录 `14-access-status.json` 已确认 `effective_probe_key_source=managed_subscription` 且 `completion_status=200` + - remote43 宿主日志也已落到真实 `GET /v1/models = 200`、`POST /v1/chat/completions = 200`,说明这条 patched overlay 路线不只在本地 fresh-host 成功,也已在远端真实环境收敛到 ready + - 这轮还顺手修掉了 `scripts/import_remote43_provider.sh` 的一个真实脚本缺陷:查找未分配 `relay-sub-*` 用户时,`NOT EXISTS` 子查询错误引用了无 alias 的 `users.id`,在 PostgreSQL 上会报 `invalid reference to FROM-clause entry for table "users"` + - 2026-05-25 继续把这套 remote43 patched-host / remote CRM 的启动流程脚本化: + - 新增 `scripts/setup_remote43_patched_stack.sh`,把 pack 镜像、二进制上传、remote43 上的 PG/Redis/patched host/临时 CRM 拉起、以及本地 operator env / SSH 隧道提示收口为一个固定入口 + - 新增 `scripts/remote43_patched_stack_lib.sh`,把远端 host env / CRM env / bootstrap script 渲染逻辑抽成可测试 helper + - `scripts/test_real_host_scripts.sh` 已新增对应回归,避免以后再回退到手工 `/tmp/*.sh` 拼装 + - 脚本首轮真实演练暴露出一个运行态细节:patched `sub2api` 二进制实际监听 `8080`,不能沿用旧临时脚本里的 `127.0.0.1:$HOST_PORT:3000` 端口映射;当前 `setup_remote43_patched_stack.sh` 已新增 `HOST_CONTAINER_PORT=8080` 默认值并完成 remote43 二次实跑验证 + - 用修复后的固定脚本在 remote43 新起的 `sub2api-kimi-patched-auto2-20260525` 栈上,`kimi-a7m` 再次完成真实导入主链路:`artifacts/real-host-acceptance/20260525_remote43_kimi_a7m_patched_overlay_scripted_stack/03-import.body.json` 已确认 `batch_status=succeeded`、`access_status=subscription_ready`、`provider_status=active`,同目录 `10-models.body.json` / `12-chat.body.json` / `18-upstream-models.body.json` / `20-upstream-chat.body.txt` 也已再次证明 managed 与 upstream 双侧都回到 `HTTP 200` + - 2026-05-26 继续把 scripted stack 的末尾状态查询收口为稳定契约:`scripts/import_remote43_provider.sh` 末尾不再只传 `host_id`,而是显式拼上 `pack_id=openai-cn-pack&host_id=`;修复原因是 remote43 上同一个 provider 可能存在多个 pack 版本,缺 `pack_id` 时 `/api/providers/{providerID}/status` 会返回 `400 provider exists in multiple packs; pack_id is required` + - 修复后,`artifacts/real-host-acceptance/20260526_remote43_kimi_a7m_patched_overlay_scripted_stack_rerun2/13-provider-status.json`、`14-access-status.json`、`21-summary.json` 已全部自动补齐;其中 `21-summary.json` 已再次确认 `batch_status=succeeded`、`provider_status_from_import=active`、`latest_access_status=subscription_ready`、`direct_chat_status=200`、`upstream_chat_status=200` +- `artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/21-summary.json` 已证明: + - Hermes 本机 `A7M_KIMI_API_KEY` 直探 upstream `/v1/models` 与 `/v1/chat/completions` 均为 `200` + - latest-head relay-manager + 本地 `weishaw/sub2api:0.1.129` fresh-host 下,import-time gateway `/v1/models` 命中 `kimi-k2.6` + - 但 completion 仍落到 `502 upstream_error`,手工 managed key 再探 `/v1/chat/completions` 也返回 `503` + - 管理员 account 视角 `/api/v1/admin/accounts/1/models` 正确,但手工 managed key `/v1/models` 仍会回到 GPT 默认集合,当前应继续归类为宿主运行时 gap / drift,而不是 Hermes key 失效 +- `artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/22-patched-host-validation.json` 已证明: + - 问题根因是宿主把 Kimi A7M 这类 custom upstream 误走到 `Responses` 路径,而不是 Hermes key 或 relay-manager pack 失效 + - 在 `/tmp/sub2api-clean` 的宿主补丁下,旁路容器 `sub2api-patched` 已恢复 `managed key /v1/models=200`、`managed key /v1/chat/completions=200`、`admin accounts/:id/test=true` + - fallback 日志与账号 `extra.openai_responses_supported=false` 持久化已同时出现,说明这条链路已经从 stock host 的 `partially_succeeded / broken` 收敛到 patched host 的 `ready` +- `artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/21-summary.json` 已证明: + - 仅启用 relay-manager 侧方案 C(预先 `force_disable_openai_responses_api` + import/access/reconcile capability repair),但保持宿主仍是未打补丁的 stock `weishaw/sub2api:0.1.129` + - import-time gateway `/v1/models` 仍能命中 `kimi-k2.6` + - 但 import-time gateway `/v1/chat/completions` 依旧返回 `502 upstream_error`,`access_status` 仍是 `broken`,`provider_status_latest` 仍是 `partially_succeeded` + - 因此当前最新真相不是“方案 C 已替代宿主补丁”,而是“方案 C 缩小了控制面误判范围,但这条 Kimi A7M / stock v0.1.129 链路仍需要宿主运行时兼容修复” +- 2026-05-25 已继续补齐方案 C(控制面侧 capability repair): + - `internal/host/sub2api` 新增 `ClearTempUnschedulable` + - access / reconcile 的 capability repair 现在会同时写 `extra.openai_responses_supported=false` 并清理账号 `temp_unschedulable` + - `packs/openai-cn-pack/providers/kimi-a7m.json` 新增 `force_disable_openai_responses_api=true`,导入后会在 gateway closure 前预先把该账号切到 raw `/v1/chat/completions` - `artifacts/real-host-acceptance/20260523_144937_remote43_kimi-a7m_key_import` 已证明: - 这轮线上 `kimi-a7m` 不再复现“错库取 key 导致统一 401”或“模型列表串成 GPT 默认集合” - import 已返回 `gateway.status_code=200`、`models=["kimi-k2.6"]`、`has_expected_model=true` @@ -149,6 +194,11 @@ - 已证明脚本层的“错库取 key / 错地址 / 多 host 历史查询”问题被收掉 - 仍保留的真实阻塞是宿主 completion 路径 `502/503` +9. `artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes` + - 当前主 pack `1.1.1` 正式纳入 `kimi-a7m` 后的本地 fresh-host 验收样本 + - `21-summary.json` 保留了 stock host `v0.1.129` 的原始失败快照,证明 Hermes A7M upstream key 当前在线可用,阻塞不在 key 本身 + - `22-patched-host-validation.json` 与 `23-sub2api-host-patch-notes.md` 已补齐 patched host 的真实收敛证据:问题是宿主 runtime 的 `Responses -> raw chat` 兼容缺口,补丁后链路已回到 ready + ## 剩余项(P2 / 运营前置,不阻塞按 PRD 首版范围上线) 1. 运营前置 diff --git a/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md b/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md index f08e1cde..c0ee2b07 100644 --- a/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md +++ b/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md @@ -67,7 +67,41 @@ scripts/build_local_image.sh - 二进制:`bin/sub2api-cn-relay-manager` - 镜像:`sub2api-cn-relay-manager:local` -### 2. 先 dry-run 检查真实验收参数 +### 2. remote43 patched host / CRM 固定启动脚本 + +当目标是复现 2026-05-25 那条 `remote43 + patched overlay + remote CRM` 验收链路时,优先先跑固定脚本,不要再手工拼 `/tmp/*.sh`: + +```bash +cd /path/to/sub2api-cn-relay-manager + +HOST_BINARY=/path/to/sub2api-patched \ +CRM_BINARY=./server \ +bash ./scripts/setup_remote43_patched_stack.sh +``` + +脚本会: +- 把本地 pack 镜像到 `/tmp/openai-cn-pack-` 并同步到 remote43 同路径 +- 上传 patched 宿主二进制与当前 CRM server 二进制 +- 在 remote43 拉起新的 Postgres / Redis / patched host +- 在 remote43 启动独立 SQLite 的临时 CRM +- 生成两个本地辅助文件: + - `local operator env file` + - `local tunnel script` + +后续按脚本输出执行: + +```bash +set -a; source /tmp/remote43-patched-stack-18139.env; set +a +bash /tmp/remote43-patched-stack-18139.tunnel.sh +``` + +然后再跑: + +```bash +bash ./scripts/import_remote43_provider.sh kimi-a7m kimi-k2.6 A7M_KIMI_API_KEY /path/to/keyfile +``` + +### 3. 先 dry-run 检查真实验收参数 ```bash CRM_BASE_URL=http://127.0.0.1:8080 \ @@ -81,10 +115,10 @@ KEYS=sk-live-1,sk-live-2 \ ACCESS_MODE=self_service \ ACCESS_API_KEY=user-gateway-key \ DRY_RUN=1 \ -scripts/real_host_acceptance.sh +bash ./scripts/real_host_acceptance.sh ``` -### 3. 执行真实验收 +### 4. 执行真实验收 ```bash CRM_BASE_URL=http://127.0.0.1:8080 \ @@ -97,10 +131,10 @@ PROVIDER_ID=deepseek \ KEYS=sk-live-1,sk-live-2 \ ACCESS_MODE=self_service \ ACCESS_API_KEY=user-gateway-key \ -scripts/real_host_acceptance.sh +bash ./scripts/real_host_acceptance.sh ``` -### 4. 订阅模式示例 +### 5. 订阅模式示例 ```bash CRM_BASE_URL=http://127.0.0.1:8080 \ @@ -114,10 +148,10 @@ KEYS=sk-live-1 \ ACCESS_MODE=subscription \ SUBSCRIPTION_USERS=user-a,user-b \ SUBSCRIPTION_DAYS=30 \ -scripts/real_host_acceptance.sh +bash ./scripts/real_host_acceptance.sh ``` -### 5. 导入后自动补 access 前置(可选) +### 6. 导入后自动补 access 前置(可选) 当真实宿主需要额外完成“普通用户余额 / key-group 绑定 / 订阅写入 / 缓存失效”等宿主侧动作时,可在 import 完成后插入自定义 hook: diff --git a/internal/access/closure_test.go b/internal/access/closure_test.go index adac2f3c..2d93899f 100644 --- a/internal/access/closure_test.go +++ b/internal/access/closure_test.go @@ -187,23 +187,31 @@ func TestServiceCloseRepairsOpenAIResponsesCapabilityMismatch(t *testing.T) { if len(host.disabledResponsesAccountIDs) != 1 || host.disabledResponsesAccountIDs[0] != "account-1" { t.Fatalf("disabled responses account ids = %v, want [account-1]", host.disabledResponsesAccountIDs) } + if host.clearTempUnschedulableCalls != 1 { + t.Fatalf("clear temp unschedulable calls = %d, want 1", host.clearTempUnschedulableCalls) + } + if len(host.clearedTempUnschedulableAccountIDs) != 1 || host.clearedTempUnschedulableAccountIDs[0] != "account-1" { + t.Fatalf("cleared temp unschedulable account ids = %v, want [account-1]", host.clearedTempUnschedulableAccountIDs) + } } type fakeClosureHost struct { - 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 - completionAfterRepair *sub2api.GatewayCompletionResult - completionErr error - disableResponsesCalls int - disabledResponsesAccountIDs []string + 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 + completionAfterRepair *sub2api.GatewayCompletionResult + completionErr error + disableResponsesCalls int + disabledResponsesAccountIDs []string + clearTempUnschedulableCalls int + clearedTempUnschedulableAccountIDs []string } func (f *fakeClosureHost) EnsureSubscriptionAccess(_ context.Context, req sub2api.EnsureSubscriptionAccessRequest) (sub2api.SubscriptionAccessRef, error) { @@ -253,3 +261,9 @@ func (f *fakeClosureHost) DisableOpenAIResponsesAPI(_ context.Context, accountID f.disabledResponsesAccountIDs = append([]string(nil), accountIDs...) return nil } + +func (f *fakeClosureHost) ClearTempUnschedulable(_ context.Context, accountIDs []string) error { + f.clearTempUnschedulableCalls++ + f.clearedTempUnschedulableAccountIDs = append([]string(nil), accountIDs...) + return nil +} diff --git a/internal/access/openai_responses_repair.go b/internal/access/openai_responses_repair.go index 44f73ad9..22b8dbff 100644 --- a/internal/access/openai_responses_repair.go +++ b/internal/access/openai_responses_repair.go @@ -45,12 +45,29 @@ func (s *Service) maybeRepairOpenAIResponsesCapability(ctx context.Context, req if len(accountIDs) == 0 { return completion, nil } - if err := s.host.DisableOpenAIResponsesAPI(ctx, accountIDs); err != nil { + if err := RepairOpenAIResponsesCapability(ctx, s.host, accountIDs); err != nil { return completion, nil } return s.checkGatewayCompletionWithRetry(ctx, completionReq) } +func RepairOpenAIResponsesCapability(ctx context.Context, host Host, accountIDs []string) error { + if host == nil { + return nil + } + normalized := normalizedAccountIDs(accountIDs) + if len(normalized) == 0 { + return nil + } + if err := host.DisableOpenAIResponsesAPI(ctx, normalized); err != nil { + return err + } + if err := host.ClearTempUnschedulable(ctx, normalized); err != nil { + return nil + } + return nil +} + func normalizedAccountIDs(accountIDs []string) []string { seen := map[string]struct{}{} values := make([]string, 0, len(accountIDs)) diff --git a/internal/access/openai_responses_repair_test.go b/internal/access/openai_responses_repair_test.go index af18a02d..31b7d7da 100644 --- a/internal/access/openai_responses_repair_test.go +++ b/internal/access/openai_responses_repair_test.go @@ -181,10 +181,45 @@ func TestMaybeRepairOpenAIResponsesCapabilitySwallowsDisableError(t *testing.T) } } +func TestMaybeRepairOpenAIResponsesCapabilityClearsTempUnschedulableBeforeRetry(t *testing.T) { + t.Parallel() + + host := &fakeRepairHost{} + service := NewService(host) + got, err := service.maybeRepairOpenAIResponsesCapability(context.Background(), ClosureRequest{ + AccountIDs: []string{"account-1", "account-1"}, + ResponsesCapabilitySuspect: true, + }, sub2api.GatewayCompletionCheckRequest{APIKey: "user-key", Model: "kimi-k2.6"}, sub2api.GatewayCompletionResult{ + StatusCode: 503, + BodyPreview: `{"error":{"message":"No available accounts"}}`, + }) + if err != nil { + t.Fatalf("maybeRepairOpenAIResponsesCapability() error = %v", err) + } + if !got.OK || got.StatusCode != 200 { + t.Fatalf("maybeRepairOpenAIResponsesCapability() = %+v, want repaired completion success", got) + } + if host.disableCalls != 1 { + t.Fatalf("disableCalls = %d, want 1", host.disableCalls) + } + if host.clearTempUnschedulableCalls != 1 { + t.Fatalf("clearTempUnschedulableCalls = %d, want 1", host.clearTempUnschedulableCalls) + } + if len(host.clearedTempUnschedulableAccountIDs) != 1 || host.clearedTempUnschedulableAccountIDs[0] != "account-1" { + t.Fatalf("clearedTempUnschedulableAccountIDs = %v, want [account-1]", host.clearedTempUnschedulableAccountIDs) + } + if host.completionCalls != 1 { + t.Fatalf("completionCalls = %d, want 1", host.completionCalls) + } +} + type fakeRepairHost struct { - disableErr error - disableCalls int - completionCalls int + disableErr error + clearTempUnschedulableErr error + disableCalls int + clearTempUnschedulableCalls int + completionCalls int + clearedTempUnschedulableAccountIDs []string } func (f *fakeRepairHost) EnsureSubscriptionAccess(_ context.Context, _ sub2api.EnsureSubscriptionAccessRequest) (sub2api.SubscriptionAccessRef, error) { @@ -208,3 +243,9 @@ func (f *fakeRepairHost) DisableOpenAIResponsesAPI(_ context.Context, _ []string f.disableCalls++ return f.disableErr } + +func (f *fakeRepairHost) ClearTempUnschedulable(_ context.Context, accountIDs []string) error { + f.clearTempUnschedulableCalls++ + f.clearedTempUnschedulableAccountIDs = append([]string(nil), accountIDs...) + return f.clearTempUnschedulableErr +} diff --git a/internal/access/types.go b/internal/access/types.go index fbdaafd9..d837357a 100644 --- a/internal/access/types.go +++ b/internal/access/types.go @@ -41,6 +41,7 @@ type Host interface { CheckGatewayAccess(ctx context.Context, req sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error) CheckGatewayCompletion(ctx context.Context, req sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error) DisableOpenAIResponsesAPI(ctx context.Context, accountIDs []string) error + ClearTempUnschedulable(ctx context.Context, accountIDs []string) error } type Service struct { diff --git a/internal/app/http_api.go b/internal/app/http_api.go index 6a9530c7..fa1f3ffc 100644 --- a/internal/app/http_api.go +++ b/internal/app/http_api.go @@ -92,9 +92,10 @@ type PackInfo struct { } type PackProviderInfo struct { - ProviderID string `json:"provider_id"` - DisplayName string `json:"display_name"` - Platform string `json:"platform,omitempty"` + ProviderID string `json:"provider_id"` + DisplayName string `json:"display_name"` + Platform string `json:"platform,omitempty"` + HostOverlays int `json:"host_overlays,omitempty"` } type AssignAccessSubscriptionsRequest struct { @@ -612,6 +613,7 @@ func handlePreviewProvider(w http.ResponseWriter, r *http.Request, fn func(conte } writeJSON(w, http.StatusOK, map[string]any{ "accepted_keys_count": len(result.AcceptedKeys), + "host_overlays": result.HostOverlays, "names": result.Names, "decisions": result.Decisions, }) @@ -637,6 +639,7 @@ func handleImportProvider(w http.ResponseWriter, r *http.Request, fn func(contex "access_status": result.Report.AccessStatus, "accepted_keys_count": len(result.Report.AcceptedKeys), "accounts_count": len(result.Report.Accounts), + "host_overlays": result.Report.HostOverlays, "gateway": result.Report.Gateway, "error": classifyError(err), } @@ -654,6 +657,7 @@ func handleImportProvider(w http.ResponseWriter, r *http.Request, fn func(contex "access_status": result.Report.AccessStatus, "accepted_keys_count": len(result.Report.AcceptedKeys), "accounts_count": len(result.Report.Accounts), + "host_overlays": result.Report.HostOverlays, "group": result.Report.Group, "channel": result.Report.Channel, "plan": result.Report.Plan, @@ -1017,8 +1021,18 @@ func NewActionSet(sqliteDSN string) ActionSet { if err != nil { return provision.PreviewReport{}, err } + hostVersion, err := client.GetHostVersion(ctx) + if err != nil { + return provision.PreviewReport{}, err + } service := provision.NewPreviewService(client) - return service.PreviewImport(ctx, provision.PreviewRequest{Provider: providerManifest, Mode: req.Mode, Keys: req.Keys}) + return service.PreviewImport(ctx, provision.PreviewRequest{ + TargetHost: loadedPack.Manifest.TargetHost, + HostVersion: hostVersion, + Provider: providerManifest, + Mode: req.Mode, + Keys: req.Keys, + }) }, ImportProvider: func(ctx context.Context, req ImportProviderRequest) (provision.RuntimeImportResult, error) { loadedPack, err := pack.LoadPath(req.PackPath) @@ -1307,20 +1321,29 @@ func NewActionSet(sqliteDSN string) ActionSet { return nil, err } defer store.Close() - pack, err := store.Packs().GetByPackID(ctx, packID) + packRow, err := store.Packs().GetByPackID(ctx, packID) if err != nil { return nil, err } - providers, err := store.Providers().ListByPackID(ctx, pack.ID) + providers, err := store.Providers().ListByPackID(ctx, packRow.ID) if err != nil { return nil, err } result := make([]PackProviderInfo, 0, len(providers)) for _, p := range providers { + hostOverlays := 0 + if strings.TrimSpace(p.ManifestJSON) != "" { + var providerManifest pack.ProviderManifest + if err := json.Unmarshal([]byte(p.ManifestJSON), &providerManifest); err != nil { + return nil, fmt.Errorf("decode stored provider manifest: %w", err) + } + hostOverlays = len(providerManifest.HostOverlays) + } result = append(result, PackProviderInfo{ - ProviderID: p.ProviderID, - DisplayName: p.DisplayName, - Platform: p.Platform, + ProviderID: p.ProviderID, + DisplayName: p.DisplayName, + Platform: p.Platform, + HostOverlays: hostOverlays, }) } return result, nil diff --git a/internal/host/sub2api/account_capability_repair.go b/internal/host/sub2api/account_capability_repair.go index df5b8520..be78db7b 100644 --- a/internal/host/sub2api/account_capability_repair.go +++ b/internal/host/sub2api/account_capability_repair.go @@ -34,3 +34,27 @@ func (c *Client) DisableOpenAIResponsesAPI(ctx context.Context, accountIDs []str } return nil } + +func (c *Client) ClearTempUnschedulable(ctx context.Context, accountIDs []string) error { + seen := map[string]struct{}{} + for _, rawID := range accountIDs { + accountID := strings.TrimSpace(rawID) + if accountID == "" { + continue + } + if _, ok := seen[accountID]; ok { + continue + } + seen[accountID] = struct{}{} + + path := "/api/v1/admin/accounts/" + accountID + "/temp-unschedulable" + statusCode, _, body, err := c.perform(ctx, http.MethodDelete, path, nil) + if err != nil { + return err + } + if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { + return newHTTPError(http.MethodDelete, path, statusCode, body) + } + } + return nil +} diff --git a/internal/host/sub2api/account_capability_repair_test.go b/internal/host/sub2api/account_capability_repair_test.go index 648f811e..add37faf 100644 --- a/internal/host/sub2api/account_capability_repair_test.go +++ b/internal/host/sub2api/account_capability_repair_test.go @@ -60,6 +60,58 @@ func TestDisableOpenAIResponsesAPIReturnsHTTPError(t *testing.T) { } } +func TestClearTempUnschedulableSkipsEmptyAccountIDs(t *testing.T) { + t.Parallel() + + client, err := NewClient("https://sub2api.example.com", WithHTTPClient(&http.Client{ + Transport: roundTripperFunc(func(*http.Request) (*http.Response, error) { + t.Fatal("unexpected HTTP request for empty account ids") + return nil, nil + }), + })) + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + + if err := client.ClearTempUnschedulable(context.Background(), []string{" ", ""}); err != nil { + t.Fatalf("ClearTempUnschedulable() error = %v", err) + } +} + +func TestClearTempUnschedulableReturnsHTTPError(t *testing.T) { + t.Parallel() + + var gotPath string + client, err := NewClient("https://sub2api.example.com", WithHTTPClient(&http.Client{ + Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) { + gotPath = req.URL.Path + return &http.Response{ + StatusCode: http.StatusForbidden, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(`{"error":"forbidden"}`)), + }, nil + }), + })) + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + + err = client.ClearTempUnschedulable(context.Background(), []string{"account-1"}) + if err == nil { + t.Fatal("ClearTempUnschedulable() error = nil, want HTTP error") + } + httpErr, ok := err.(*HTTPError) + if !ok { + t.Fatalf("ClearTempUnschedulable() error type = %T, want *HTTPError", err) + } + if gotPath != "/api/v1/admin/accounts/account-1/temp-unschedulable" { + t.Fatalf("request path = %q, want /api/v1/admin/accounts/account-1/temp-unschedulable", gotPath) + } + if httpErr.StatusCode != http.StatusForbidden { + t.Fatalf("HTTPError.StatusCode = %d, want %d", httpErr.StatusCode, http.StatusForbidden) + } +} + type roundTripperFunc func(*http.Request) (*http.Response, error) func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { diff --git a/internal/host/sub2api/client.go b/internal/host/sub2api/client.go index add5a226..f0d385da 100644 --- a/internal/host/sub2api/client.go +++ b/internal/host/sub2api/client.go @@ -32,6 +32,7 @@ type HostAdapter interface { CheckGatewayAccess(ctx context.Context, req GatewayAccessCheckRequest) (GatewayAccessResult, error) CheckGatewayCompletion(ctx context.Context, req GatewayCompletionCheckRequest) (GatewayCompletionResult, error) DisableOpenAIResponsesAPI(ctx context.Context, accountIDs []string) error + ClearTempUnschedulable(ctx context.Context, accountIDs []string) error ListManagedResources(ctx context.Context, req ListManagedResourcesRequest) (ManagedResourceSnapshot, error) } diff --git a/internal/overlay/executor.go b/internal/overlay/executor.go new file mode 100644 index 00000000..e42ba6bc --- /dev/null +++ b/internal/overlay/executor.go @@ -0,0 +1,257 @@ +package overlay + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" + + "sub2api-cn-relay-manager/internal/pack" +) + +const metadataFileName = ".sub2api-cn-relay-manager-overlay.json" + +type ApplyRequest struct { + PackDir string + SourceDir string + OutputDir string + Overlays []pack.HostOverlay +} + +type ApplyResult struct { + OutputDir string + AppliedOverlays []pack.HostOverlay + MetadataFilePath string +} + +func Apply(ctx context.Context, req ApplyRequest) (_ ApplyResult, err error) { + packDir := strings.TrimSpace(req.PackDir) + sourceDir := strings.TrimSpace(req.SourceDir) + if packDir == "" { + return ApplyResult{}, fmt.Errorf("pack dir is required") + } + if sourceDir == "" { + return ApplyResult{}, fmt.Errorf("source dir is required") + } + if len(req.Overlays) == 0 { + return ApplyResult{}, fmt.Errorf("at least one host overlay is required") + } + + packAbs, err := filepath.Abs(packDir) + if err != nil { + return ApplyResult{}, fmt.Errorf("resolve pack dir: %w", err) + } + sourceAbs, err := filepath.Abs(sourceDir) + if err != nil { + return ApplyResult{}, fmt.Errorf("resolve source dir: %w", err) + } + sourceInfo, err := os.Stat(sourceAbs) + if err != nil { + return ApplyResult{}, fmt.Errorf("stat source dir: %w", err) + } + if !sourceInfo.IsDir() { + return ApplyResult{}, fmt.Errorf("source dir %q must be a directory", sourceAbs) + } + + outputDir := strings.TrimSpace(req.OutputDir) + if outputDir == "" { + outputDir = defaultOutputDir(sourceAbs, req.Overlays) + } + outputAbs, err := filepath.Abs(outputDir) + if err != nil { + return ApplyResult{}, fmt.Errorf("resolve output dir: %w", err) + } + if outputAbs == sourceAbs { + return ApplyResult{}, fmt.Errorf("output dir must differ from source dir") + } + if isPathWithin(outputAbs, sourceAbs) { + return ApplyResult{}, fmt.Errorf("output dir %q must not be nested inside source dir %q", outputAbs, sourceAbs) + } + if _, err := os.Stat(outputAbs); err == nil { + return ApplyResult{}, fmt.Errorf("output dir %q already exists", outputAbs) + } else if !os.IsNotExist(err) { + return ApplyResult{}, fmt.Errorf("stat output dir: %w", err) + } + + if err := copyTree(sourceAbs, outputAbs); err != nil { + return ApplyResult{}, fmt.Errorf("copy source dir: %w", err) + } + cleanupOutput := true + defer func() { + if cleanupOutput { + _ = os.RemoveAll(outputAbs) + } + }() + + for _, hostOverlay := range req.Overlays { + patchPath := strings.TrimSpace(hostOverlay.PatchPath) + if patchPath == "" { + return ApplyResult{}, fmt.Errorf("overlay %q does not define patch_path", hostOverlay.OverlayID) + } + patchAbs := filepath.Join(packAbs, patchPath) + if err := applyPatchFile(ctx, outputAbs, patchAbs); err != nil { + return ApplyResult{}, fmt.Errorf("apply overlay %q: %w", hostOverlay.OverlayID, err) + } + } + + metadataPath := filepath.Join(outputAbs, metadataFileName) + if err := writeMetadata(metadataPath, sourceAbs, req.Overlays); err != nil { + return ApplyResult{}, fmt.Errorf("write overlay metadata: %w", err) + } + + cleanupOutput = false + return ApplyResult{ + OutputDir: outputAbs, + AppliedOverlays: append([]pack.HostOverlay(nil), req.Overlays...), + MetadataFilePath: metadataPath, + }, nil +} + +func FilterOverlays(overlays []pack.HostOverlay, overlayID string) ([]pack.HostOverlay, error) { + trimmedOverlayID := strings.TrimSpace(overlayID) + if trimmedOverlayID == "" { + return append([]pack.HostOverlay(nil), overlays...), nil + } + filtered := make([]pack.HostOverlay, 0, len(overlays)) + for _, hostOverlay := range overlays { + if strings.TrimSpace(hostOverlay.OverlayID) == trimmedOverlayID { + filtered = append(filtered, hostOverlay) + } + } + if len(filtered) == 0 { + return nil, fmt.Errorf("overlay %q did not match any resolved host overlays", trimmedOverlayID) + } + return filtered, nil +} + +func applyPatchFile(ctx context.Context, outputDir string, patchPath string) error { + if _, err := os.Stat(patchPath); err != nil { + return fmt.Errorf("stat patch file %q: %w", patchPath, err) + } + cmd := exec.CommandContext(ctx, "patch", "-p1", "-i", patchPath, "-d", outputDir) + output, err := cmd.CombinedOutput() + if err != nil { + message := strings.TrimSpace(string(output)) + if message == "" { + message = err.Error() + } + return fmt.Errorf("%s", message) + } + return nil +} + +func writeMetadata(path string, sourceDir string, overlays []pack.HostOverlay) error { + body, err := json.MarshalIndent(map[string]any{ + "source_dir": sourceDir, + "applied_overlays": overlays, + }, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, append(body, '\n'), 0o644) +} + +func defaultOutputDir(sourceDir string, overlays []pack.HostOverlay) string { + baseName := filepath.Base(sourceDir) + overlaySuffix := "overlay" + if len(overlays) > 0 { + overlaySuffix = sanitizePathToken(overlays[0].OverlayID) + if overlaySuffix == "" { + overlaySuffix = "overlay" + } + } + return filepath.Join(filepath.Dir(sourceDir), baseName+"-patched-"+overlaySuffix) +} + +func sanitizePathToken(value string) string { + value = strings.ToLower(strings.TrimSpace(value)) + var b strings.Builder + lastDash := false + for _, r := range value { + switch { + case r >= 'a' && r <= 'z', r >= '0' && r <= '9': + b.WriteRune(r) + lastDash = false + case !lastDash: + b.WriteByte('-') + lastDash = true + } + } + return strings.Trim(b.String(), "-") +} + +func isPathWithin(target string, root string) bool { + rel, err := filepath.Rel(root, target) + if err != nil { + return false + } + return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))) +} + +func copyTree(sourceDir string, outputDir string) error { + if err := os.MkdirAll(outputDir, 0o755); err != nil { + return err + } + return filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + rel, err := filepath.Rel(sourceDir, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + if rel == ".git" || strings.HasPrefix(rel, ".git"+string(filepath.Separator)) { + if d.IsDir() { + return filepath.SkipDir + } + return nil + } + targetPath := filepath.Join(outputDir, rel) + info, err := d.Info() + if err != nil { + return err + } + switch { + case d.IsDir(): + return os.MkdirAll(targetPath, info.Mode().Perm()) + case info.Mode()&os.ModeSymlink != 0: + linkTarget, err := os.Readlink(path) + if err != nil { + return err + } + return os.Symlink(linkTarget, targetPath) + default: + return copyFile(path, targetPath, info.Mode().Perm()) + } + }) +} + +func copyFile(sourcePath string, targetPath string, perm fs.FileMode) error { + if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { + return err + } + sourceFile, err := os.Open(sourcePath) + if err != nil { + return err + } + defer sourceFile.Close() + + targetFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm) + if err != nil { + return err + } + defer targetFile.Close() + + if _, err := io.Copy(targetFile, sourceFile); err != nil { + return err + } + return nil +} diff --git a/internal/overlay/executor_test.go b/internal/overlay/executor_test.go new file mode 100644 index 00000000..ff1f1c59 --- /dev/null +++ b/internal/overlay/executor_test.go @@ -0,0 +1,127 @@ +package overlay + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "sub2api-cn-relay-manager/internal/pack" +) + +func TestApplyCopiesSourceAndAppliesPatch(t *testing.T) { + sourceDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + if err := os.WriteFile(filepath.Join(sourceDir, "backend", "hello.txt"), []byte("hello\n"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + packDir := t.TempDir() + patchBody := strings.Join([]string{ + "diff --git a/backend/hello.txt b/backend/hello.txt", + "--- a/backend/hello.txt", + "+++ b/backend/hello.txt", + "@@ -1 +1 @@", + "-hello", + "+patched", + "", + }, "\n") + patchPath := filepath.Join(packDir, "overlays", "sample.patch") + if err := os.MkdirAll(filepath.Dir(patchPath), 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + if err := os.WriteFile(patchPath, []byte(patchBody), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + result, err := Apply(context.Background(), ApplyRequest{ + PackDir: packDir, + SourceDir: sourceDir, + Overlays: []pack.HostOverlay{{ + OverlayID: "sample", + PatchPath: "overlays/sample.patch", + }}, + }) + if err != nil { + t.Fatalf("Apply() error = %v", err) + } + body, err := os.ReadFile(filepath.Join(result.OutputDir, "backend", "hello.txt")) + if err != nil { + t.Fatalf("ReadFile() error = %v", err) + } + if string(body) != "patched\n" { + t.Fatalf("patched file = %q, want %q", string(body), "patched\n") + } + if _, err := os.Stat(result.MetadataFilePath); err != nil { + t.Fatalf("Stat(metadata) error = %v", err) + } +} + +func TestApplySupportsRelativePackDir(t *testing.T) { + workspaceDir := t.TempDir() + originalWD, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd() error = %v", err) + } + if err := os.Chdir(workspaceDir); err != nil { + t.Fatalf("Chdir() error = %v", err) + } + t.Cleanup(func() { + _ = os.Chdir(originalWD) + }) + + sourceDir := filepath.Join(workspaceDir, "source") + if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + if err := os.WriteFile(filepath.Join(sourceDir, "backend", "hello.txt"), []byte("hello\n"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + packDir := filepath.Join(workspaceDir, "pack") + patchPath := filepath.Join(packDir, "overlays", "sample.patch") + if err := os.MkdirAll(filepath.Dir(patchPath), 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + patchBody := strings.Join([]string{ + "diff --git a/backend/hello.txt b/backend/hello.txt", + "--- a/backend/hello.txt", + "+++ b/backend/hello.txt", + "@@ -1 +1 @@", + "-hello", + "+patched", + "", + }, "\n") + if err := os.WriteFile(patchPath, []byte(patchBody), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + result, err := Apply(context.Background(), ApplyRequest{ + PackDir: "pack", + SourceDir: sourceDir, + Overlays: []pack.HostOverlay{{ + OverlayID: "sample", + PatchPath: "overlays/sample.patch", + }}, + }) + if err != nil { + t.Fatalf("Apply() with relative pack dir error = %v", err) + } + body, err := os.ReadFile(filepath.Join(result.OutputDir, "backend", "hello.txt")) + if err != nil { + t.Fatalf("ReadFile() error = %v", err) + } + if string(body) != "patched\n" { + t.Fatalf("patched file = %q, want %q", string(body), "patched\n") + } +} + +func TestFilterOverlaysRejectsMissingOverlayID(t *testing.T) { + _, err := FilterOverlays([]pack.HostOverlay{{OverlayID: "sample"}}, "missing") + if err == nil || !strings.Contains(err.Error(), `overlay "missing" did not match`) { + t.Fatalf("FilterOverlays() error = %v, want missing overlay detail", err) + } +} diff --git a/internal/pack/extra_test.go b/internal/pack/extra_test.go index 5f0194af..b8b28cc7 100644 --- a/internal/pack/extra_test.go +++ b/internal/pack/extra_test.go @@ -142,7 +142,7 @@ func TestValidateProvidersRejectsInvalidProviderFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateProviders(tt.providers) + err := validateProviders(t.TempDir(), tt.providers) if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("validateProviders() error = %v, want substring %q", err, tt.wantErr) } diff --git a/internal/pack/host_overlay.go b/internal/pack/host_overlay.go new file mode 100644 index 00000000..d46f9739 --- /dev/null +++ b/internal/pack/host_overlay.go @@ -0,0 +1,49 @@ +package pack + +import "strings" + +func ResolveApplicableHostOverlays(provider ProviderManifest, targetHost string, hostVersion string) ([]HostOverlay, error) { + trimmedTargetHost := strings.TrimSpace(targetHost) + trimmedHostVersion := strings.TrimSpace(hostVersion) + if trimmedTargetHost == "" || trimmedHostVersion == "" || len(provider.HostOverlays) == 0 { + return nil, nil + } + result := make([]HostOverlay, 0, len(provider.HostOverlays)) + for _, overlay := range provider.HostOverlays { + if !strings.EqualFold(strings.TrimSpace(overlay.TargetHost), trimmedTargetHost) { + continue + } + ok, err := hostOverlayMatchesVersion(overlay, trimmedHostVersion) + if err != nil { + return nil, err + } + if ok { + result = append(result, overlay) + } + } + return result, nil +} + +func hostOverlayMatchesVersion(overlay HostOverlay, hostVersion string) (bool, error) { + minVersion := strings.TrimSpace(overlay.MinHostVersion) + if minVersion != "" { + cmp, err := compareVersions(hostVersion, minVersion) + if err != nil { + return false, err + } + if cmp < 0 { + return false, nil + } + } + maxVersion := strings.TrimSpace(overlay.MaxHostVersion) + if maxVersion != "" { + ok, err := matchesMaxConstraint(hostVersion, maxVersion) + if err != nil { + return false, err + } + if !ok { + return false, nil + } + } + return true, nil +} diff --git a/internal/pack/host_overlay_test.go b/internal/pack/host_overlay_test.go new file mode 100644 index 00000000..1735e702 --- /dev/null +++ b/internal/pack/host_overlay_test.go @@ -0,0 +1,35 @@ +package pack + +import "testing" + +func TestResolveApplicableHostOverlays(t *testing.T) { + provider := ProviderManifest{ + ProviderID: "kimi-a7m", + HostOverlays: []HostOverlay{ + { + OverlayID: "sub2api-stock-v0129-kimi-a7m", + DisplayName: "sub2api stock v0.1.129 Kimi A7M overlay", + TargetHost: "sub2api", + MinHostVersion: "0.1.129", + MaxHostVersion: "0.1.129", + Reason: "stock host still routes chat traffic into unsupported Responses path", + }, + { + OverlayID: "other-version", + DisplayName: "other version", + TargetHost: "sub2api", + MinHostVersion: "0.1.130", + MaxHostVersion: "0.1.130", + Reason: "not this version", + }, + }, + } + + overlays, err := ResolveApplicableHostOverlays(provider, "sub2api", "0.1.129") + if err != nil { + t.Fatalf("ResolveApplicableHostOverlays() error = %v", err) + } + if len(overlays) != 1 || overlays[0].OverlayID != "sub2api-stock-v0129-kimi-a7m" { + t.Fatalf("ResolveApplicableHostOverlays() = %+v, want matching v0.1.129 overlay", overlays) + } +} diff --git a/internal/pack/loader.go b/internal/pack/loader.go index adae9095..3cca936a 100644 --- a/internal/pack/loader.go +++ b/internal/pack/loader.go @@ -26,17 +26,31 @@ type Manifest struct { } type ProviderManifest struct { - ProviderID string `json:"provider_id"` - DisplayName string `json:"display_name"` - BaseURL string `json:"base_url"` - Platform string `json:"platform"` - AccountType string `json:"account_type"` - DefaultModels []string `json:"default_models"` - SmokeTestModel string `json:"smoke_test_model"` - GroupTemplate GroupTemplate `json:"group_template"` - ChannelTemplate ChannelTemplate `json:"channel_template"` - PlanTemplate PlanTemplate `json:"plan_template"` - Import ImportOptions `json:"import"` + ProviderID string `json:"provider_id"` + DisplayName string `json:"display_name"` + BaseURL string `json:"base_url"` + Platform string `json:"platform"` + AccountType string `json:"account_type"` + ForceDisableOpenAIResponsesAPI bool `json:"force_disable_openai_responses_api,omitempty"` + HostOverlays []HostOverlay `json:"host_overlays,omitempty"` + DefaultModels []string `json:"default_models"` + SmokeTestModel string `json:"smoke_test_model"` + GroupTemplate GroupTemplate `json:"group_template"` + ChannelTemplate ChannelTemplate `json:"channel_template"` + PlanTemplate PlanTemplate `json:"plan_template"` + Import ImportOptions `json:"import"` +} + +type HostOverlay struct { + OverlayID string `json:"overlay_id"` + DisplayName string `json:"display_name"` + TargetHost string `json:"target_host"` + MinHostVersion string `json:"min_host_version,omitempty"` + MaxHostVersion string `json:"max_host_version,omitempty"` + ApplyMode string `json:"apply_mode,omitempty"` + PatchPath string `json:"patch_path,omitempty"` + NotesPath string `json:"notes_path,omitempty"` + Reason string `json:"reason"` } type GroupTemplate struct { @@ -100,7 +114,7 @@ func LoadDir(dir string) (LoadedPack, error) { if len(providers) == 0 { return LoadedPack{}, fmt.Errorf("providers dir %q does not contain provider manifests", manifest.ProvidersDir) } - if err := validateProviders(providers); err != nil { + if err := validateProviders(root, providers); err != nil { return LoadedPack{}, err } @@ -155,7 +169,7 @@ func loadProviders(root string, providersDir string) ([]ProviderManifest, error) return providers, nil } -func validateProviders(providers []ProviderManifest) error { +func validateProviders(root string, providers []ProviderManifest) error { seen := make(map[string]struct{}, len(providers)) allowInsecureBaseURL := strings.EqualFold(strings.TrimSpace(os.Getenv(envAllowInsecureProviderBaseURL)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envAllowInsecureProviderBaseURL)), "true") @@ -197,11 +211,91 @@ func validateProviders(providers []ProviderManifest) error { if _, ok := seen[providerID]; ok { return fmt.Errorf("duplicate provider_id %q", providerID) } + if err := validateHostOverlays(root, provider); err != nil { + return err + } seen[providerID] = struct{}{} } return nil } +func validateHostOverlays(root string, provider ProviderManifest) error { + if len(provider.HostOverlays) == 0 { + return nil + } + seen := make(map[string]struct{}, len(provider.HostOverlays)) + for _, overlay := range provider.HostOverlays { + overlayID := strings.TrimSpace(overlay.OverlayID) + switch { + case overlayID == "": + return fmt.Errorf("provider %q: host_overlays.overlay_id is required", provider.ProviderID) + case strings.TrimSpace(overlay.DisplayName) == "": + return fmt.Errorf("provider %q overlay %q: display_name is required", provider.ProviderID, overlayID) + case strings.TrimSpace(overlay.TargetHost) == "": + return fmt.Errorf("provider %q overlay %q: target_host is required", provider.ProviderID, overlayID) + case strings.TrimSpace(overlay.Reason) == "": + return fmt.Errorf("provider %q overlay %q: reason is required", provider.ProviderID, overlayID) + } + if _, ok := seen[overlayID]; ok { + return fmt.Errorf("provider %q: duplicate host overlay %q", provider.ProviderID, overlayID) + } + if err := validateHostOverlayVersionRange(overlay); err != nil { + return fmt.Errorf("provider %q overlay %q: %w", provider.ProviderID, overlayID, err) + } + if err := validateOverlayFileRef(root, overlay.PatchPath); err != nil { + return fmt.Errorf("provider %q overlay %q: %w", provider.ProviderID, overlayID, err) + } + if err := validateOverlayFileRef(root, overlay.NotesPath); err != nil { + return fmt.Errorf("provider %q overlay %q: %w", provider.ProviderID, overlayID, err) + } + seen[overlayID] = struct{}{} + } + return nil +} + +func validateHostOverlayVersionRange(overlay HostOverlay) error { + minVersion := strings.TrimSpace(overlay.MinHostVersion) + if minVersion != "" { + if _, err := parseVersion(minVersion); err != nil { + return fmt.Errorf("parse min_host_version: %w", err) + } + } + maxVersion := strings.TrimSpace(overlay.MaxHostVersion) + if maxVersion != "" { + if strings.HasSuffix(normalizeVersion(maxVersion), ".x") { + if _, err := matchesMaxConstraint("0.0.0", maxVersion); err != nil { + return fmt.Errorf("parse max_host_version: %w", err) + } + } else if _, err := parseVersion(maxVersion); err != nil { + return fmt.Errorf("parse max_host_version: %w", err) + } + } + if minVersion != "" && maxVersion != "" && !strings.HasSuffix(normalizeVersion(maxVersion), ".x") { + cmp, err := compareVersions(minVersion, maxVersion) + if err != nil { + return fmt.Errorf("compare version range: %w", err) + } + if cmp > 0 { + return fmt.Errorf("min_host_version %q is above max_host_version %q", minVersion, maxVersion) + } + } + return nil +} + +func validateOverlayFileRef(root string, relativePath string) error { + trimmed := strings.TrimSpace(relativePath) + if trimmed == "" { + return nil + } + if filepath.IsAbs(trimmed) { + return fmt.Errorf("file reference %q must be relative to pack root", trimmed) + } + if _, err := os.Stat(filepath.Join(root, trimmed)); err != nil { + return fmt.Errorf("read file reference %q: %w", trimmed, err) + } + return nil +} + func hasAllowedProviderBaseURL(baseURL string, allowInsecureBaseURL bool) bool { if strings.HasPrefix(baseURL, "https://") { return true diff --git a/internal/pack/loader_test.go b/internal/pack/loader_test.go index 525bb453..4808a590 100644 --- a/internal/pack/loader_test.go +++ b/internal/pack/loader_test.go @@ -129,6 +129,34 @@ func TestLoadDirRejectsDefaultModelsMissingFromChannelMapping(t *testing.T) { } } +func TestLoadDirParsesProviderHostOverlays(t *testing.T) { + packDir := createPackFixture(t, map[string]string{ + "pack.json": `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`, + "providers/kimi-a7m.json": `{"provider_id":"kimi-a7m","display_name":"Kimi A7M","base_url":"https://kimi.a7m.com.cn/v1","platform":"openai","account_type":"apikey","default_models":["kimi-k2.6"],"smoke_test_model":"kimi-k2.6","host_overlays":[{"overlay_id":"sub2api-stock-v0129-kimi-a7m","display_name":"sub2api stock v0.1.129 Kimi A7M overlay","target_host":"sub2api","min_host_version":"0.1.129","max_host_version":"0.1.129","apply_mode":"manual_overlay","patch_path":"overlays/kimi-a7m-sub2api-v0.1.129.md","notes_path":"overlays/kimi-a7m-sub2api-v0.1.129.md","reason":"stock host still routes chat traffic into unsupported Responses path"}],"group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"kimi-k2.6":"kimi-k2.6"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`, + "overlays/kimi-a7m-sub2api-v0.1.129.md": "# overlay\n", + }) + + loaded, err := LoadDir(packDir) + if err != nil { + t.Fatalf("LoadDir() error = %v", err) + } + if len(loaded.Providers) != 1 || len(loaded.Providers[0].HostOverlays) != 1 { + t.Fatalf("loaded provider overlays = %+v, want one overlay", loaded.Providers) + } +} + +func TestLoadDirRejectsMissingHostOverlayFile(t *testing.T) { + packDir := createPackFixture(t, map[string]string{ + "pack.json": `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`, + "providers/kimi-a7m.json": `{"provider_id":"kimi-a7m","display_name":"Kimi A7M","base_url":"https://kimi.a7m.com.cn/v1","platform":"openai","account_type":"apikey","default_models":["kimi-k2.6"],"smoke_test_model":"kimi-k2.6","host_overlays":[{"overlay_id":"sub2api-stock-v0129-kimi-a7m","display_name":"sub2api stock v0.1.129 Kimi A7M overlay","target_host":"sub2api","min_host_version":"0.1.129","max_host_version":"0.1.129","patch_path":"overlays/missing.patch","reason":"stock host still routes chat traffic into unsupported Responses path"}],"group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"kimi-k2.6":"kimi-k2.6"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`, + }) + + _, err := LoadDir(packDir) + if err == nil || !strings.Contains(err.Error(), `read file reference "overlays/missing.patch"`) { + t.Fatalf("LoadDir() error = %v, want missing overlay file detail", err) + } +} + func createPackFixture(t *testing.T, files map[string]string) string { t.Helper() diff --git a/internal/provision/import_service.go b/internal/provision/import_service.go index 8009469d..4ec635a2 100644 --- a/internal/provision/import_service.go +++ b/internal/provision/import_service.go @@ -60,6 +60,7 @@ type ImportReport struct { ProviderStatus string AccessStatus string AcceptedKeys []string + HostOverlays []pack.HostOverlay Group sub2api.GroupRef Channel sub2api.ChannelRef Plan *sub2api.PlanRef @@ -255,6 +256,11 @@ func (s *ImportService) Import(ctx context.Context, req ImportRequest) (report I report.AccessStatus = AccessStatusBroken return report, fmt.Errorf("strict import failed: %d account(s) did not pass smoke validation", failedAccounts) } + if shouldPreemptivelyDisableOpenAIResponses(req.Provider) { + if err := access.RepairOpenAIResponsesCapability(ctx, s.host, importedAccountIDs(report.Accounts)); err != nil { + return failOrDegrade(report, req.Mode, fmt.Errorf("preemptively repair openai responses capability: %w", err)) + } + } closureService := access.NewService(s.host) gateway, err := closureService.Close(ctx, access.ClosureRequest{ @@ -350,6 +356,12 @@ func importedAccountsSuspectResponsesCapabilityMismatch(accounts []AccountImport return false } +func shouldPreemptivelyDisableOpenAIResponses(provider pack.ProviderManifest) bool { + return strings.EqualFold(strings.TrimSpace(provider.Platform), "openai") && + strings.EqualFold(strings.TrimSpace(provider.AccountType), "apikey") && + provider.ForceDisableOpenAIResponsesAPI +} + func ensureGroup(ctx context.Context, host hostAdapter, existing []sub2api.NamedResource, provider pack.ProviderManifest, accessMode, groupName string) (sub2api.GroupRef, bool, error) { switch len(existing) { case 0: diff --git a/internal/provision/import_service_test.go b/internal/provision/import_service_test.go index 96efb90b..75f71057 100644 --- a/internal/provision/import_service_test.go +++ b/internal/provision/import_service_test.go @@ -385,6 +385,57 @@ func TestImportServiceRepairsOpenAIResponsesCapabilityMismatchAfterInstall(t *te if len(host.disabledResponsesAccountIDs) != 1 || host.disabledResponsesAccountIDs[0] != "account_1" { t.Fatalf("disabled responses account ids = %v, want [account_1]", host.disabledResponsesAccountIDs) } + if host.clearTempUnschedulableCalls != 1 { + t.Fatalf("clear temp unschedulable calls = %d, want 1", host.clearTempUnschedulableCalls) + } + if len(host.clearedTempUnschedulableAccountIDs) != 1 || host.clearedTempUnschedulableAccountIDs[0] != "account_1" { + t.Fatalf("cleared temp unschedulable account ids = %v, want [account_1]", host.clearedTempUnschedulableAccountIDs) + } +} + +func TestImportServicePreemptivelyDisablesResponsesForChatOnlyProvider(t *testing.T) { + host := &fakeHostAdapter{ + batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}}, + testResults: map[string]sub2api.ProbeResult{ + "account_1": {OK: true, Status: "passed"}, + }, + models: map[string][]sub2api.AccountModel{ + "account_1": {{ID: "kimi-k2.6"}}, + }, + gatewayResult: sub2api.GatewayAccessResult{ + OK: true, + StatusCode: 200, + HasExpectedModel: true, + Models: []string{"kimi-k2.6"}, + CompletionOK: true, + CompletionStatus: 200, + }, + } + + report, err := NewImportService(host).Import(context.Background(), ImportRequest{ + Provider: sampleChatOnlyProviderManifest(), + 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 host.disableResponsesCalls != 1 { + t.Fatalf("disable responses calls = %d, want 1", host.disableResponsesCalls) + } + if len(host.disabledResponsesAccountIDs) != 1 || host.disabledResponsesAccountIDs[0] != "account_1" { + t.Fatalf("disabled responses account ids = %v, want [account_1]", host.disabledResponsesAccountIDs) + } + if host.clearTempUnschedulableCalls != 1 { + t.Fatalf("clear temp unschedulable calls = %d, want 1", host.clearTempUnschedulableCalls) + } + if got := host.callSequence; len(got) == 0 || got[0] != "disable_responses" { + t.Fatalf("callSequence = %v, want preemptive capability repair before gateway validation", got) + } } func TestImportServiceStrictModeRollsBackCreatedResources(t *testing.T) { @@ -531,6 +582,20 @@ func sampleProviderManifest() pack.ProviderManifest { } } +func sampleChatOnlyProviderManifest() pack.ProviderManifest { + provider := sampleProviderManifest() + provider.ProviderID = "kimi-a7m" + provider.DisplayName = "Kimi A7M OpenAI Compatible" + provider.BaseURL = "https://kimi.a7m.com.cn/v1" + provider.DefaultModels = []string{"kimi-k2.6"} + provider.SmokeTestModel = "kimi-k2.6" + provider.ChannelTemplate = pack.ChannelTemplate{Name: "Kimi A7M 默认渠道", ModelMapping: map[string]string{"kimi-k2.6": "kimi-k2.6"}} + provider.GroupTemplate = pack.GroupTemplate{Name: "Kimi A7M 默认分组", RateMultiplier: 1} + provider.PlanTemplate = pack.PlanTemplate{Name: "Kimi A7M 默认套餐", Price: 19.9, ValidityDays: 30, ValidityUnit: "day"} + provider.ForceDisableOpenAIResponsesAPI = true + return provider +} + func TestImportReconcilesExistingChannelConfiguration(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}}, @@ -639,39 +704,41 @@ func TestImportKeepsExistingAccountsWhenReplacementValidationFails(t *testing.T) } type fakeHostAdapter struct { - batchAccounts []sub2api.AccountRef - batchCreateReq sub2api.BatchCreateAccountsRequest - testResults map[string]sub2api.ProbeResult - models map[string][]sub2api.AccountModel - gatewayResult sub2api.GatewayAccessResult - batchCreateErr error - assignErr error - gatewayErr error - hostVersion string - assignedSubscriptions []sub2api.AssignSubscriptionRequest - gatewayProbe sub2api.GatewayAccessCheckRequest - completionProbe sub2api.GatewayCompletionCheckRequest - deletedResources []string - managedSnapshot sub2api.ManagedResourceSnapshot - listManagedReq sub2api.ListManagedResourcesRequest - createGroupCalls int - createChannelCalls int - updateChannelCalls int - createPlanCalls int - createGroupReq sub2api.CreateGroupRequest - createChannelReq sub2api.CreateChannelRequest - updateChannelID string - updateChannelReq sub2api.CreateChannelRequest - callSequence []string - completionCalls int - completionResults []sub2api.GatewayCompletionResult - completionResult sub2api.GatewayCompletionResult - completionAfterRepair *sub2api.GatewayCompletionResult - completionErr error - testedModels map[string]string - disableResponsesCalls int - disabledResponsesAccountIDs []string - deleteErrors map[string]error + batchAccounts []sub2api.AccountRef + batchCreateReq sub2api.BatchCreateAccountsRequest + testResults map[string]sub2api.ProbeResult + models map[string][]sub2api.AccountModel + gatewayResult sub2api.GatewayAccessResult + batchCreateErr error + assignErr error + gatewayErr error + hostVersion string + assignedSubscriptions []sub2api.AssignSubscriptionRequest + gatewayProbe sub2api.GatewayAccessCheckRequest + completionProbe sub2api.GatewayCompletionCheckRequest + deletedResources []string + managedSnapshot sub2api.ManagedResourceSnapshot + listManagedReq sub2api.ListManagedResourcesRequest + createGroupCalls int + createChannelCalls int + updateChannelCalls int + createPlanCalls int + createGroupReq sub2api.CreateGroupRequest + createChannelReq sub2api.CreateChannelRequest + updateChannelID string + updateChannelReq sub2api.CreateChannelRequest + callSequence []string + completionCalls int + completionResults []sub2api.GatewayCompletionResult + completionResult sub2api.GatewayCompletionResult + completionAfterRepair *sub2api.GatewayCompletionResult + completionErr error + testedModels map[string]string + disableResponsesCalls int + disabledResponsesAccountIDs []string + clearTempUnschedulableCalls int + clearedTempUnschedulableAccountIDs []string + deleteErrors map[string]error } func (f *fakeHostAdapter) GetHostVersion(context.Context) (string, error) { @@ -801,10 +868,18 @@ func (f *fakeHostAdapter) CheckGatewayCompletion(_ context.Context, req sub2api. return f.completionResult, nil } func (f *fakeHostAdapter) DisableOpenAIResponsesAPI(_ context.Context, accountIDs []string) error { + f.callSequence = append(f.callSequence, "disable_responses") f.disableResponsesCalls++ f.disabledResponsesAccountIDs = append([]string(nil), accountIDs...) return nil } + +func (f *fakeHostAdapter) ClearTempUnschedulable(_ context.Context, accountIDs []string) error { + f.callSequence = append(f.callSequence, "clear_temp_unschedulable") + f.clearTempUnschedulableCalls++ + f.clearedTempUnschedulableAccountIDs = append([]string(nil), accountIDs...) + return nil +} func (f *fakeHostAdapter) ListManagedResources(_ context.Context, req sub2api.ListManagedResourcesRequest) (sub2api.ManagedResourceSnapshot, error) { f.listManagedReq = req return sub2api.ManagedResourceSnapshot{ diff --git a/internal/provision/preview_service.go b/internal/provision/preview_service.go index e4544884..91c3c9d0 100644 --- a/internal/provision/preview_service.go +++ b/internal/provision/preview_service.go @@ -19,9 +19,11 @@ type previewHost interface { } type PreviewRequest struct { - Provider pack.ProviderManifest - Mode string - Keys []string + TargetHost string + HostVersion string + Provider pack.ProviderManifest + Mode string + Keys []string } type PreviewDecision struct { @@ -33,6 +35,7 @@ type PreviewDecision struct { type PreviewReport struct { AcceptedKeys []string + HostOverlays []pack.HostOverlay Names ResourceNames Decisions map[string]PreviewDecision } @@ -56,6 +59,10 @@ func (s *PreviewService) PreviewImport(ctx context.Context, req PreviewRequest) if s.host == nil { return PreviewReport{}, fmt.Errorf("preview host is required") } + hostOverlays, err := pack.ResolveApplicableHostOverlays(req.Provider, req.TargetHost, req.HostVersion) + if err != nil { + return PreviewReport{}, fmt.Errorf("resolve host overlays: %w", err) + } names := SuggestResourceNamesForMode(req.Provider, req.Mode) snapshot, err := s.host.ListManagedResources(ctx, sub2api.ListManagedResourcesRequest{ @@ -70,6 +77,7 @@ func (s *PreviewService) PreviewImport(ctx context.Context, req PreviewRequest) return PreviewReport{ AcceptedKeys: acceptedKeys, + HostOverlays: hostOverlays, Names: names, Decisions: map[string]PreviewDecision{ "group": decideResource(names.Group, snapshot.Groups), diff --git a/internal/provision/preview_service_test.go b/internal/provision/preview_service_test.go index 6bb40775..96166bae 100644 --- a/internal/provision/preview_service_test.go +++ b/internal/provision/preview_service_test.go @@ -6,6 +6,7 @@ import ( "testing" "sub2api-cn-relay-manager/internal/host/sub2api" + "sub2api-cn-relay-manager/internal/pack" ) func TestSuggestResourceNames(t *testing.T) { @@ -67,6 +68,35 @@ func TestPreviewServiceReportsCreateActionsWhenHostHasNoResources(t *testing.T) } } +func TestPreviewServiceIncludesMatchingHostOverlays(t *testing.T) { + host := &fakePreviewHost{} + svc := NewPreviewService(host) + + provider := sampleProviderManifest() + provider.HostOverlays = []pack.HostOverlay{{ + OverlayID: "sub2api-stock-v0129-kimi-a7m", + DisplayName: "sub2api stock v0.1.129 Kimi A7M overlay", + TargetHost: "sub2api", + MinHostVersion: "0.1.129", + MaxHostVersion: "0.1.129", + Reason: "stock host still routes chat traffic into unsupported Responses path", + }} + + report, err := svc.PreviewImport(context.Background(), PreviewRequest{ + TargetHost: "sub2api", + HostVersion: "0.1.129", + Provider: provider, + Mode: ImportModePartial, + Keys: []string{"key-1"}, + }) + if err != nil { + t.Fatalf("PreviewImport() error = %v", err) + } + if len(report.HostOverlays) != 1 || report.HostOverlays[0].OverlayID != "sub2api-stock-v0129-kimi-a7m" { + t.Fatalf("HostOverlays = %+v, want matching overlay", report.HostOverlays) + } +} + func TestPreviewServiceReportsReuseAndConflict(t *testing.T) { host := &fakePreviewHost{snapshot: sub2api.ManagedResourceSnapshot{ Groups: []sub2api.NamedResource{{ID: "group_1", Name: "DeepSeek 默认分组"}}, diff --git a/internal/provision/reconcile_service_test.go b/internal/provision/reconcile_service_test.go index cf14fd23..179bbd72 100644 --- a/internal/provision/reconcile_service_test.go +++ b/internal/provision/reconcile_service_test.go @@ -209,6 +209,9 @@ func TestReconcileServiceRepairsOpenAIResponsesCapabilityMismatch(t *testing.T) if len(host.disabledResponsesAccountIDs) != 2 { t.Fatalf("disabled responses account ids = %v, want both accounts", host.disabledResponsesAccountIDs) } + if host.clearTempUnschedulableCalls != 1 { + t.Fatalf("clear temp unschedulable calls = %d, want 1", host.clearTempUnschedulableCalls) + } } func TestReconcileServiceReturnsDriftedWhenManagedResourceMissing(t *testing.T) { diff --git a/internal/provision/runtime_import_service.go b/internal/provision/runtime_import_service.go index 7606e065..4ddd666b 100644 --- a/internal/provision/runtime_import_service.go +++ b/internal/provision/runtime_import_service.go @@ -102,6 +102,11 @@ func (s *RuntimeImportService) Import(ctx context.Context, req RuntimeImportRequ Access: req.Access, Keys: req.Keys, }) + hostOverlays, overlayErr := pack.ResolveApplicableHostOverlays(req.Provider, req.Pack.Manifest.TargetHost, hostVersion) + if overlayErr != nil { + return RuntimeImportResult{}, fmt.Errorf("resolve host overlays: %w", overlayErr) + } + report.HostOverlays = hostOverlays if report.BatchStatus == "" { report.BatchStatus = BatchStatusFailed } diff --git a/internal/provision/runtime_import_service_test.go b/internal/provision/runtime_import_service_test.go index e1599594..2fd72ca0 100644 --- a/internal/provision/runtime_import_service_test.go +++ b/internal/provision/runtime_import_service_test.go @@ -59,6 +59,9 @@ func TestRuntimeImportServicePersistsOperationalState(t *testing.T) { if result.Report.BatchStatus != BatchStatusSucceeded { t.Fatalf("BatchStatus = %q, want %q", result.Report.BatchStatus, BatchStatusSucceeded) } + if len(result.Report.HostOverlays) != 0 { + t.Fatalf("HostOverlays = %+v, want none for sample provider", result.Report.HostOverlays) + } if got := queryCount(t, store.SQLDB(), "hosts"); got != 1 { t.Fatalf("hosts row count = %d, want 1", got) @@ -110,6 +113,57 @@ func TestRuntimeImportServicePersistsOperationalState(t *testing.T) { } } +func TestRuntimeImportServiceIncludesMatchingHostOverlaysInReport(t *testing.T) { + store := openProvisionTestStore(t) + defer closeProvisionTestStore(t, store) + + seedProvisionHost(t, store, "host-1", "https://sub2api.example.com") + + host := &fakeHostAdapter{ + hostVersion: "0.1.129", + batchAccounts: []sub2api.AccountRef{{ID: "account_1"}}, + testResults: map[string]sub2api.ProbeResult{"account_1": {OK: true, Status: "passed"}}, + models: map[string][]sub2api.AccountModel{"account_1": {{ID: "kimi-k2.6"}}}, + gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"kimi-k2.6"}, CompletionOK: true, CompletionStatus: 200}, + } + + provider := sampleProviderManifest() + provider.ProviderID = "kimi-a7m" + provider.DefaultModels = []string{"kimi-k2.6"} + provider.SmokeTestModel = "kimi-k2.6" + provider.ChannelTemplate.ModelMapping = map[string]string{"kimi-k2.6": "kimi-k2.6"} + provider.HostOverlays = []pack.HostOverlay{{ + OverlayID: "sub2api-stock-v0129-kimi-a7m", + DisplayName: "sub2api stock v0.1.129 Kimi A7M overlay", + TargetHost: "sub2api", + MinHostVersion: "0.1.129", + MaxHostVersion: "0.1.129", + Reason: "stock host still routes chat traffic into unsupported Responses path", + }} + + result, err := NewRuntimeImportService(store, host).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: provider, + Mode: ImportModePartial, + Keys: []string{"key-1"}, + Access: AccessRequest{ + Mode: AccessModeSubscription, + Subscriptions: []SubscriptionTarget{{UserID: "crm-user", DurationDays: 30}}, + }, + }) + if err != nil { + t.Fatalf("RuntimeImportService.Import() error = %v", err) + } + if len(result.Report.HostOverlays) != 1 || result.Report.HostOverlays[0].OverlayID != "sub2api-stock-v0129-kimi-a7m" { + t.Fatalf("HostOverlays = %+v, want matching overlay", result.Report.HostOverlays) + } +} + func TestRuntimeImportServicePersistsFailedBatchAfterStrictRollback(t *testing.T) { store := openProvisionTestStore(t) defer closeProvisionTestStore(t, store) diff --git a/internal/reconcile/service.go b/internal/reconcile/service.go index 8b5c5410..44aec0a8 100644 --- a/internal/reconcile/service.go +++ b/internal/reconcile/service.go @@ -259,7 +259,7 @@ func (s *Service) rerunAccessClosure(ctx context.Context, batchID int64, accessC return "", false, fmt.Errorf("re-check gateway completion: %w", err) } if access.ShouldAttemptOpenAIResponsesCapabilityRepair(suspectResponsesCapabilityMismatch, completion) { - if err := s.host.DisableOpenAIResponsesAPI(ctx, accountIDs); err == nil { + if err := access.RepairOpenAIResponsesCapability(ctx, s.host, accountIDs); err == nil { completion, err = s.host.CheckGatewayCompletion(ctx, completionReq) if err != nil { return "", false, fmt.Errorf("re-check gateway completion after capability repair: %w", err) diff --git a/internal/reconcile/service_runtime_test.go b/internal/reconcile/service_runtime_test.go index f22bd9fa..1c4d85d0 100644 --- a/internal/reconcile/service_runtime_test.go +++ b/internal/reconcile/service_runtime_test.go @@ -290,6 +290,9 @@ func TestRerunAccessClosureReturnsErrorWhenCompletionAfterRepairFails(t *testing if err == nil || err.Error() != "re-check gateway completion after capability repair: still failing" { t.Fatalf("rerunAccessClosure() error = %v, want post-repair completion failure", err) } + if host.clearTempUnschedulableCalls != 1 { + t.Fatalf("clearTempUnschedulableCalls = %d, want 1", host.clearTempUnschedulableCalls) + } } func TestRerunAccessClosurePersistsBrokenRecordWhenRepairCannotRun(t *testing.T) { @@ -325,6 +328,9 @@ func TestRerunAccessClosurePersistsBrokenRecordWhenRepairCannotRun(t *testing.T) if host.disableResponsesCalls != 1 { t.Fatalf("disableResponsesCalls = %d, want 1", host.disableResponsesCalls) } + if host.clearTempUnschedulableCalls != 0 { + t.Fatalf("clearTempUnschedulableCalls = %d, want 0 after disable failure", host.clearTempUnschedulableCalls) + } if host.completionCalls != 1 { t.Fatalf("completionCalls = %d, want 1 without retry after disable failure", host.completionCalls) } @@ -636,20 +642,22 @@ func mustExecReconcileSQL(t *testing.T, store *sqlite.DB, query string, args ... } type reconcileHostStub struct { - disableResponsesErr error - gatewayErr error - gatewayResult sub2api.GatewayAccessResult - testResults map[string]sub2api.ProbeResult - testErrors map[string]error - models map[string][]sub2api.AccountModel - modelErrors map[string]error - completionResults []sub2api.GatewayCompletionResult - completionErrs []error - completionCalls int - disableResponsesCalls int - disabledResponsesAccounts []string - managedResourceSnapshot sub2api.ManagedResourceSnapshot - listManagedResourcesErr error + disableResponsesErr error + gatewayErr error + gatewayResult sub2api.GatewayAccessResult + testResults map[string]sub2api.ProbeResult + testErrors map[string]error + models map[string][]sub2api.AccountModel + modelErrors map[string]error + completionResults []sub2api.GatewayCompletionResult + completionErrs []error + completionCalls int + disableResponsesCalls int + disabledResponsesAccounts []string + clearTempUnschedulableCalls int + clearedTempUnschedulableAccounts []string + managedResourceSnapshot sub2api.ManagedResourceSnapshot + listManagedResourcesErr error } func (h *reconcileHostStub) GetHostVersion(context.Context) (string, error) { @@ -756,6 +764,12 @@ func (h *reconcileHostStub) DisableOpenAIResponsesAPI(_ context.Context, account return h.disableResponsesErr } +func (h *reconcileHostStub) ClearTempUnschedulable(_ context.Context, accountIDs []string) error { + h.clearTempUnschedulableCalls++ + h.clearedTempUnschedulableAccounts = append([]string(nil), accountIDs...) + return nil +} + func (h *reconcileHostStub) ListManagedResources(context.Context, sub2api.ListManagedResourcesRequest) (sub2api.ManagedResourceSnapshot, error) { if h.listManagedResourcesErr != nil { return sub2api.ManagedResourceSnapshot{}, h.listManagedResourcesErr diff --git a/packs/openai-cn-pack/README.md b/packs/openai-cn-pack/README.md index 65db2396..e98e45fe 100644 --- a/packs/openai-cn-pack/README.md +++ b/packs/openai-cn-pack/README.md @@ -4,6 +4,9 @@ 它不是宿主原生插件,而是一个可被控制面读取的 `model_pack`,用于描述国产模型 provider 的默认接入模板、默认模型映射、默认套餐和导入约束。 +当前 pack 也可以承载 provider 级 `host_overlays` 元数据。 +这类 overlay 不是立即在线改宿主代码,而是把“某个 provider 在某个宿主版本上需要额外运行时补丁/兼容层”的工程信息纳入 pack 管理,供 preview/import 阶段直接暴露,并可由 CLI 执行器生成 patched 宿主构建目录。 + 当前目录现在同时包含: - 真实可校验包:`pack.json`、`providers/deepseek.json`、`checksums.txt` @@ -60,6 +63,18 @@ go run ./cmd/cli import-provider \ 如果你要导入的不是这 10 个模板之一,而是一个全新的官方 provider,那么仍然需要先补一个新的 provider manifest,再做一键导入。 +对已经声明 `host_overlays` 的 provider,也可以直接用最小执行器生成 patched 宿主源码目录。例如对 `kimi-a7m` 命中的 `sub2api v0.1.129` overlay: + +```bash +go run ./cmd/cli apply-host-overlay \ + --pack-dir ./packs/openai-cn-pack \ + --provider-id kimi-a7m \ + --host-version 0.1.129 \ + --source-dir /tmp/sub2api-clean +``` + +命令会解析 provider manifest 中命中的 overlay,复制 `--source-dir` 到新的输出目录,应用 `patch_path` 指向的补丁,并在输出目录写入 `.sub2api-cn-relay-manager-overlay.json` 元数据文件。 + 后续真实交付时,还可以继续扩展更多 provider: - `kimi.json` diff --git a/packs/openai-cn-pack/checksums.txt b/packs/openai-cn-pack/checksums.txt index cdf3b56e..50ba5fdb 100644 --- a/packs/openai-cn-pack/checksums.txt +++ b/packs/openai-cn-pack/checksums.txt @@ -7,7 +7,10 @@ 5dcc402daddacce6dcaceb1501020342f1b1121fbffe9097ede4d5aae072f84e providers/minimax.json 65e3a1a5e56889ddb0474a3b55294aceb6920fa72dcf6f2c56d3199462daa4cf providers/kimi-k2-thinking-official.json a39de44fa68fcb5ee9c3ef38ed3bd5c30acd23cacd2f618d670de0bf9e7096e3 providers/deepseek-reasoner-official.json -e3da0745a14cb76f7275bfef90b40a6f652f4dce2efd95e44c27fe2e81f4eea5 pack.json +2db47989a9715464a34b00f7e322ceb1396f96617ddcfa7dee5bd3e7b262c17d providers/kimi-a7m.json +584991c1a5a3973bda9701ad15bb1c1b167038baa513cb67985708a65bdf6ca6 overlays/kimi-a7m-sub2api-v0.1.129.md +2b2597694ab03409360bf73de43a5cfcea0e26369c4ab18cc8552ff3278729aa overlays/kimi-a7m-sub2api-v0.1.129.patch +4d0069e7bb014b886d4b21fa9a2144fcf65e835f158eda1bb98e092efebd93f3 pack.json eda16afc83e12055d3a41b5e37fd0923d3741b66da5af780bcea53ff34fa130e providers/step-3-5-flash-official.json fa486a449407f38de8b180ff301568deccef5177ca0436158b1d5b0e6d9328b2 providers/openai-zhongzhuan.json fdf7fa2e1ff4aa4f5dcd3f3ec2f55db11d6197625a467d6d0afa8a554a6ba6e6 providers/deepseek-chat-official.json diff --git a/packs/openai-cn-pack/overlays/kimi-a7m-sub2api-v0.1.129.md b/packs/openai-cn-pack/overlays/kimi-a7m-sub2api-v0.1.129.md new file mode 100644 index 00000000..171839cb --- /dev/null +++ b/packs/openai-cn-pack/overlays/kimi-a7m-sub2api-v0.1.129.md @@ -0,0 +1,32 @@ +# Kimi A7M / sub2api v0.1.129 Overlay + +`overlay_id`: `sub2api-stock-v0129-kimi-a7m` + +适用范围: + +- 宿主:`sub2api` +- 版本:`0.1.129` +- provider:`kimi-a7m` + +触发背景: + +- stock `weishaw/sub2api:0.1.129` 面对 `https://kimi.a7m.com.cn/v1` 时,`/v1/models` 可以命中 `kimi-k2.6` +- 但运行时 `/v1/chat/completions` 仍会误走到不兼容的 `Responses` 路径,最终返回 `502 upstream_error` +- 仅靠 relay-manager 控制面侧方案 C,当前还不能把这条链路收敛到 `ready` + +已验证的宿主补丁落点: + +- `backend/internal/service/openai_apikey_responses_probe.go` +- `backend/internal/service/openai_gateway_chat_completions.go` +- `backend/internal/service/account_test_service.go` + +参考证据: + +- `artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/22-patched-host-validation.json` +- `artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_from_hermes/23-sub2api-host-patch-notes.md` +- `artifacts/real-host-acceptance/20260525_local_v0129_kimi_a7m_scheme_c_stockhost_rerun/21-summary.json` + +当前用途: + +- 由 pack/provider manifest 暴露给控制面,作为“该 provider 在该宿主版本上需要 overlay”的正式插件元数据 +- 这一步只负责纳管,不直接修改宿主源码 diff --git a/packs/openai-cn-pack/overlays/kimi-a7m-sub2api-v0.1.129.patch b/packs/openai-cn-pack/overlays/kimi-a7m-sub2api-v0.1.129.patch new file mode 100644 index 00000000..5296d4d3 --- /dev/null +++ b/packs/openai-cn-pack/overlays/kimi-a7m-sub2api-v0.1.129.patch @@ -0,0 +1,356 @@ +diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go +index b9cd698a..3a58e022 100644 +--- a/backend/internal/service/account_test_service.go ++++ b/backend/internal/service/account_test_service.go +@@ -555,14 +555,8 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account + if err != nil { + return s.sendErrorAndEnd(c, fmt.Sprintf("Invalid base URL: %s", err.Error())) + } +- // 账号已被探测为不支持 Responses(如 DeepSeek/Kimi 等)时,丢出明确提示。 +- // 账号本身可用(网关会走 CC 直转),仅测试入口需要补齐 CC SSE 处理逻辑。 +- // TODO:实现 CC 格式的账号测试路径(需专门的 CC SSE handler)。 + if !openai_compat.ShouldUseResponsesAPI(account.Extra) { +- return s.sendErrorAndEnd(c, +- "账号已被探测为不支持 OpenAI Responses API(如 DeepSeek/Kimi 等三方兼容上游),"+ +- "账号本身可正常使用,但当前测试接口仅支持 Responses API 路径。请直接通过实际 API 调用验证。", +- ) ++ return s.testOpenAIRawChatCompletionsConnection(c, ctx, account, testModelID, prompt, normalizedBaseURL, authToken) + } + apiURL = buildOpenAIResponsesURL(normalizedBaseURL) + } else { +@@ -1321,6 +1315,133 @@ func (s *AccountTestService) processOpenAIStream(c *gin.Context, body io.Reader) + } + } + ++func (s *AccountTestService) testOpenAIRawChatCompletionsConnection( ++ c *gin.Context, ++ ctx context.Context, ++ account *Account, ++ modelID string, ++ prompt string, ++ normalizedBaseURL string, ++ authToken string, ++) error { ++ apiURL := buildOpenAIChatCompletionsURL(normalizedBaseURL) ++ ++ c.Writer.Header().Set("Content-Type", "text/event-stream") ++ c.Writer.Header().Set("Cache-Control", "no-cache") ++ c.Writer.Header().Set("Connection", "keep-alive") ++ c.Writer.Header().Set("X-Accel-Buffering", "no") ++ c.Writer.Flush() ++ ++ payloadPrompt := strings.TrimSpace(prompt) ++ if payloadPrompt == "" { ++ payloadPrompt = "hi" ++ } ++ payloadBytes, _ := json.Marshal(map[string]any{ ++ "model": modelID, ++ "messages": []map[string]any{ ++ { ++ "role": "user", ++ "content": payloadPrompt, ++ }, ++ }, ++ "stream": true, ++ "stream_options": map[string]any{ ++ "include_usage": true, ++ }, ++ }) ++ ++ s.sendEvent(c, TestEvent{Type: "test_start", Model: modelID}) ++ ++ req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(payloadBytes)) ++ if err != nil { ++ return s.sendErrorAndEnd(c, "Failed to create request") ++ } ++ req.Header.Set("Content-Type", "application/json") ++ req.Header.Set("Accept", "text/event-stream") ++ req.Header.Set("Authorization", "Bearer "+authToken) ++ if customUA := strings.TrimSpace(account.GetOpenAIUserAgent()); customUA != "" { ++ req.Header.Set("User-Agent", customUA) ++ } ++ ++ proxyURL := "" ++ if account.ProxyID != nil && account.Proxy != nil { ++ proxyURL = account.Proxy.URL() ++ } ++ ++ resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) ++ if err != nil { ++ return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error())) ++ } ++ defer func() { _ = resp.Body.Close() }() ++ ++ if resp.StatusCode != http.StatusOK { ++ body, _ := io.ReadAll(resp.Body) ++ return s.sendErrorAndEnd(c, fmt.Sprintf("API returned %d: %s", resp.StatusCode, string(body))) ++ } ++ ++ return s.processOpenAIChatCompletionsStream(c, resp.Body) ++} ++ ++func (s *AccountTestService) processOpenAIChatCompletionsStream(c *gin.Context, body io.Reader) error { ++ reader := bufio.NewReader(body) ++ seenEvent := false ++ ++ for { ++ line, err := reader.ReadString('\n') ++ if err != nil { ++ if err == io.EOF && seenEvent { ++ s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) ++ return nil ++ } ++ if err == io.EOF { ++ return s.sendErrorAndEnd(c, "Stream ended before any chat completion event was received") ++ } ++ return s.sendErrorAndEnd(c, fmt.Sprintf("Stream read error: %s", err.Error())) ++ } ++ ++ line = strings.TrimSpace(line) ++ if line == "" || !sseDataPrefix.MatchString(line) { ++ continue ++ } ++ ++ jsonStr := sseDataPrefix.ReplaceAllString(line, "") ++ if jsonStr == "[DONE]" { ++ s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) ++ return nil ++ } ++ ++ seenEvent = true ++ ++ var data map[string]any ++ if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { ++ continue ++ } ++ ++ if errData, ok := data["error"].(map[string]any); ok { ++ if msg, ok := errData["message"].(string); ok && msg != "" { ++ return s.sendErrorAndEnd(c, msg) ++ } ++ return s.sendErrorAndEnd(c, "Unknown error") ++ } ++ ++ choices, ok := data["choices"].([]any) ++ if !ok || len(choices) == 0 { ++ continue ++ } ++ firstChoice, ok := choices[0].(map[string]any) ++ if !ok { ++ continue ++ } ++ delta, ok := firstChoice["delta"].(map[string]any) ++ if !ok { ++ continue ++ } ++ if text, ok := delta["content"].(string); ok && text != "" { ++ s.sendEvent(c, TestEvent{Type: "content", Text: text}) ++ } ++ } ++} ++ + // testOpenAIImageAPIKey tests OpenAI image generation using an API Key account. + func (s *AccountTestService) testOpenAIImageAPIKey(c *gin.Context, ctx context.Context, account *Account, modelID, prompt string) error { + authToken := account.GetOpenAIApiKey() +diff --git a/backend/internal/service/openai_apikey_responses_probe.go b/backend/internal/service/openai_apikey_responses_probe.go +index a4eb9252..6935fa6e 100644 +--- a/backend/internal/service/openai_apikey_responses_probe.go ++++ b/backend/internal/service/openai_apikey_responses_probe.go +@@ -44,6 +44,28 @@ func openaiResponsesProbePayload(modelID string) []byte { + return body + } + ++// openaiChatCompletionsProbePayload 是用于交叉验证的最小 Chat Completions 请求体。 ++// ++// 当 /v1/responses 返回 403 这类模糊信号时,我们再探一次 ++// /v1/chat/completions:若 chat 端点可达,则说明该上游更可能是 ++// chat-only OpenAI-compatible,而不是完整支持 Responses 的实现。 ++func openaiChatCompletionsProbePayload(modelID string) []byte { ++ if strings.TrimSpace(modelID) == "" { ++ modelID = openai.DefaultTestModel ++ } ++ body, _ := json.Marshal(map[string]any{ ++ "model": modelID, ++ "messages": []map[string]any{ ++ { ++ "role": "user", ++ "content": "hi", ++ }, ++ }, ++ "stream": false, ++ }) ++ return body ++} ++ + // ProbeOpenAIAPIKeyResponsesSupport 探测 OpenAI APIKey 账号上游是否支持 + // /v1/responses 端点,并将结果持久化到 accounts.extra.openai_responses_supported。 + // +@@ -51,6 +73,9 @@ func openaiResponsesProbePayload(modelID string) []byte { + // + // 探测策略(参见包文档 internal/pkg/openai_compat): + // - 上游 404 / 405 → 不支持,写 false ++// - 上游 403 → 继续交叉探测 /v1/chat/completions: ++// - - chat 端点可达(非 404/405)→ 视为 chat-only upstream,写 false ++// - - chat 端点不可确认 / 探测失败 → 保持旧语义,写 true + // - 上游 2xx / 其他 4xx(401/422/400 等)/ 5xx → 支持,写 true + // - 网络层失败(连接错误、超时)→ 不写标记,保持 unknown + // (后续请求仍按"现状即证据"默认走 Responses) +@@ -116,6 +141,28 @@ func (s *AccountTestService) ProbeOpenAIAPIKeyResponsesSupport(ctx context.Conte + }() + + supported := isResponsesEndpointSupportedByStatus(resp.StatusCode) ++ if resp.StatusCode == http.StatusForbidden { ++ chatStatus, chatErr := s.probeOpenAIAPIKeyChatCompletionsStatus(ctx, account) ++ if chatErr != nil { ++ logger.LegacyPrintf( ++ "service.openai_probe", ++ "probe_chat_crosscheck_failed: account_id=%d base_url=%s err=%v", ++ accountID, ++ normalizedBaseURL, ++ chatErr, ++ ) ++ } else if isChatCompletionsEndpointSupportedByStatus(chatStatus) { ++ supported = false ++ logger.LegacyPrintf( ++ "service.openai_probe", ++ "probe_chat_crosscheck_chat_only: account_id=%d base_url=%s responses_status=%d chat_status=%d", ++ accountID, ++ normalizedBaseURL, ++ resp.StatusCode, ++ chatStatus, ++ ) ++ } ++ } + + if err := s.accountRepo.UpdateExtra(ctx, accountID, map[string]any{ + openai_compat.ExtraKeyResponsesSupported: supported, +@@ -147,3 +194,59 @@ func isResponsesEndpointSupportedByStatus(status int) bool { + } + return true + } ++ ++func isChatCompletionsEndpointSupportedByStatus(status int) bool { ++ switch status { ++ case http.StatusNotFound, http.StatusMethodNotAllowed: ++ return false ++ default: ++ return true ++ } ++} ++ ++func (s *AccountTestService) probeOpenAIAPIKeyChatCompletionsStatus(ctx context.Context, account *Account) (int, error) { ++ if account == nil { ++ return 0, http.ErrNoLocation ++ } ++ ++ apiKey := account.GetOpenAIApiKey() ++ if apiKey == "" { ++ return 0, http.ErrNoCookie ++ } ++ baseURL := account.GetOpenAIBaseURL() ++ if baseURL == "" { ++ baseURL = "https://api.openai.com" ++ } ++ normalizedBaseURL, err := s.validateUpstreamBaseURL(baseURL) ++ if err != nil { ++ return 0, err ++ } ++ ++ probeURL := buildOpenAIChatCompletionsURL(normalizedBaseURL) ++ probeCtx, cancel := context.WithTimeout(ctx, openaiResponsesProbeTimeout) ++ defer cancel() ++ ++ req, err := http.NewRequestWithContext(probeCtx, http.MethodPost, probeURL, bytes.NewReader(openaiChatCompletionsProbePayload(""))) ++ if err != nil { ++ return 0, err ++ } ++ req.Header.Set("Content-Type", "application/json") ++ req.Header.Set("Authorization", "Bearer "+apiKey) ++ req.Header.Set("Accept", "application/json") ++ ++ proxyURL := "" ++ if account.ProxyID != nil && account.Proxy != nil { ++ proxyURL = account.Proxy.URL() ++ } ++ ++ resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) ++ if err != nil { ++ return 0, err ++ } ++ defer func() { ++ _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20)) ++ _ = resp.Body.Close() ++ }() ++ ++ return resp.StatusCode, nil ++} +diff --git a/backend/internal/service/openai_gateway_chat_completions.go b/backend/internal/service/openai_gateway_chat_completions.go +index 84d85c74..fe6ff300 100644 +--- a/backend/internal/service/openai_gateway_chat_completions.go ++++ b/backend/internal/service/openai_gateway_chat_completions.go +@@ -247,6 +247,17 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions( + + upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody)) + upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg) ++ if shouldFallbackResponsesCompatToRawChat(account, resp.StatusCode) { ++ s.markOpenAIResponsesUnsupported(ctx, account) ++ logger.L().Info("openai chat_completions: fallback responses->raw chat after custom upstream incompatibility signal", ++ zap.Int64("account_id", account.ID), ++ zap.String("account_name", account.Name), ++ zap.String("base_url", account.GetOpenAIBaseURL()), ++ zap.Int("responses_status", resp.StatusCode), ++ zap.String("upstream_message", upstreamMsg), ++ ) ++ return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel) ++ } + if s.shouldFailoverOpenAIUpstreamResponse(resp.StatusCode, upstreamMsg, respBody) { + upstreamDetail := "" + if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody { +@@ -316,6 +327,47 @@ func normalizeResponsesRequestServiceTier(req *apicompat.ResponsesRequest) { + req.ServiceTier = normalizedOpenAIServiceTierValue(req.ServiceTier) + } + ++func shouldFallbackResponsesCompatToRawChat(account *Account, status int) bool { ++ if account == nil || account.Type != AccountTypeAPIKey { ++ return false ++ } ++ if strings.TrimSpace(account.GetOpenAIBaseURL()) == "" { ++ return false ++ } ++ switch status { ++ case http.StatusForbidden, http.StatusNotFound, http.StatusMethodNotAllowed: ++ return true ++ default: ++ return false ++ } ++} ++ ++func (s *OpenAIGatewayService) markOpenAIResponsesUnsupported(ctx context.Context, account *Account) { ++ if account == nil || account.Type != AccountTypeAPIKey { ++ return ++ } ++ ++ if account.Extra == nil { ++ account.Extra = map[string]any{} ++ } ++ if supported, ok := account.Extra[openai_compat.ExtraKeyResponsesSupported].(bool); ok && !supported { ++ return ++ } ++ account.Extra[openai_compat.ExtraKeyResponsesSupported] = false ++ ++ if s == nil || s.accountRepo == nil { ++ return ++ } ++ if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{ ++ openai_compat.ExtraKeyResponsesSupported: false, ++ }); err != nil { ++ logger.L().Warn("openai chat_completions: persist responses unsupported flag failed", ++ zap.Int64("account_id", account.ID), ++ zap.Error(err), ++ ) ++ } ++} ++ + func normalizeResponsesBodyServiceTier(body []byte) ([]byte, string, error) { + if len(body) == 0 { + return body, "", nil diff --git a/packs/openai-cn-pack/pack.json b/packs/openai-cn-pack/pack.json index 78154ded..952a37ca 100644 --- a/packs/openai-cn-pack/pack.json +++ b/packs/openai-cn-pack/pack.json @@ -1,6 +1,6 @@ { "pack_id": "openai-cn-pack", - "version": "1.1.0", + "version": "1.1.3", "vendor": "YourTeam", "target_host": "sub2api", "min_host_version": "0.1.126", diff --git a/packs/openai-cn-pack/providers/kimi-a7m.json b/packs/openai-cn-pack/providers/kimi-a7m.json new file mode 100644 index 00000000..9c9810b7 --- /dev/null +++ b/packs/openai-cn-pack/providers/kimi-a7m.json @@ -0,0 +1,44 @@ +{ + "provider_id": "kimi-a7m", + "display_name": "Kimi A7M OpenAI Compatible", + "base_url": "https://kimi.a7m.com.cn/v1", + "platform": "openai", + "account_type": "apikey", + "force_disable_openai_responses_api": true, + "host_overlays": [ + { + "overlay_id": "sub2api-stock-v0129-kimi-a7m", + "display_name": "sub2api stock v0.1.129 Kimi A7M overlay", + "target_host": "sub2api", + "min_host_version": "0.1.129", + "max_host_version": "0.1.129", + "apply_mode": "patch", + "patch_path": "overlays/kimi-a7m-sub2api-v0.1.129.patch", + "notes_path": "overlays/kimi-a7m-sub2api-v0.1.129.md", + "reason": "stock host still routes Kimi A7M chat traffic into an incompatible Responses path; runtime overlay or shim is still required" + } + ], + "default_models": ["kimi-k2.6"], + "smoke_test_model": "kimi-k2.6", + "group_template": { + "name": "Kimi A7M 默认分组", + "rate_multiplier": 1.0 + }, + "channel_template": { + "name": "Kimi A7M 默认渠道", + "model_mapping": { + "kimi-k2.6": "kimi-k2.6" + } + }, + "plan_template": { + "name": "Kimi A7M 默认套餐", + "price": 19.9, + "validity_days": 30, + "validity_unit": "day" + }, + "import": { + "supports_multi_key": true, + "supports_strict": true, + "supports_partial": true + } +} diff --git a/scripts/import_remote43_provider.sh b/scripts/import_remote43_provider.sh index eaea2268..55d145d8 100755 --- a/scripts/import_remote43_provider.sh +++ b/scripts/import_remote43_provider.sh @@ -106,6 +106,19 @@ if [[ -z "$upstream_base_url" ]]; then exit 2 fi +pack_id="$(python3 - "$PACK_PATH" <<'PY' +import json, pathlib, sys +pack_path = pathlib.Path(sys.argv[1]) +pack_file = pack_path / "pack.json" +pack = json.loads(pack_file.read_text(encoding='utf-8')) +print(str(pack.get("pack_id", "")).strip()) +PY +)" +if [[ -z "$pack_id" ]]; then + echo "missing pack_id in $PACK_PATH/pack.json" >&2 + exit 2 +fi + ssh_cmd() { local cmd="$1" ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$cmd" @@ -340,9 +353,9 @@ if [[ -z "$host_bearer_token" ]]; then fi admin_uid="$(ssh_cmd "sudo -n docker exec $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api -Atc \"select id from users where role='admin' order by id asc limit 1;\"")" admin_uid="${admin_uid##*$'\n'}" -sub_uid="$(remote_pg_query "select id from users where email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=users.id and s.deleted_at is null) order by id desc limit 1;")" +sub_uid="$(remote_pg_query "select u.id from users u where u.email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=u.id and s.deleted_at is null) order by u.id desc limit 1;")" sub_uid="${sub_uid##*$'\n'}" -sub_key="$(remote_pg_query "select k.key from users u join api_keys k on k.user_id=u.id where u.email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=users.id and s.deleted_at is null) order by u.id desc limit 1;")" +sub_key="$(remote_pg_query "select k.key from users u join api_keys k on k.user_id=u.id where u.email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=u.id and s.deleted_at is null) order by u.id desc limit 1;")" sub_key="${sub_key##*$'\n'}" if [[ -z "$sub_uid" || -z "$sub_key" ]]; then fresh_seed="$(python3 - <<'PY' @@ -469,6 +482,17 @@ else crm_curl_json POST "/api/hosts" "$create_host_payload" > "$ART/01a-create-host.json" fi +host_id="$(python3 - "$ART/01a-create-host.json" <<'PY' +import json, pathlib, sys +obj = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')) +print(str(obj.get('host_id', '')).strip()) +PY +)" +if [[ -z "$host_id" ]]; then + echo "missing host_id in $ART/01a-create-host.json" >&2 + exit 2 +fi + payload="$(python3 - "$CRM_HOST_BASE" "$host_bearer_token" "$PACK_PATH" "$provider_id" "$upstream_key" "$sub_key" "$sub_uid" "$SUBSCRIPTION_DAYS" <<'PY' import json, sys host_base, host_bearer_token, pack_path, provider_id, upstream_key, sub_key, sub_uid, subscription_days = sys.argv[1:9] @@ -614,10 +638,11 @@ ssh_cmd "cat /tmp/upstream_chat_headers.txt" > "$ART/19-upstream-chat.headers.tx ssh_cmd "cat /tmp/upstream_chat_body.txt" > "$ART/20-upstream-chat.body.txt" sanitize_headers_file "$ART/19-upstream-chat.headers.txt" -provider_query_suffix="?host_id=$(python3 - "$HOST_NAME" <<'PY' +provider_query_suffix="$(python3 - "$pack_id" "$host_id" <<'PY' import sys from urllib.parse import quote -print(quote(sys.argv[1], safe='')) +pack_id, host_id = sys.argv[1:3] +print(f"?pack_id={quote(pack_id, safe='')}&host_id={quote(host_id, safe='')}") PY )" diff --git a/scripts/remote43_patched_stack_lib.sh b/scripts/remote43_patched_stack_lib.sh new file mode 100644 index 00000000..59746735 --- /dev/null +++ b/scripts/remote43_patched_stack_lib.sh @@ -0,0 +1,246 @@ +#!/usr/bin/env bash +set -euo pipefail + +remote43_require_file() { + local path="$1" + local label="$2" + [[ -f "$path" ]] || { + echo "missing $label: $path" >&2 + return 1 + } +} + +remote43_random_hex() { + local bytes="${1:?bytes required}" + python3 - "$bytes" <<'PY' +import secrets +import sys + +print(secrets.token_hex(int(sys.argv[1]))) +PY +} + +remote43_write_env_file() { + local path="$1" + shift + : > "$path" + while [[ $# -gt 0 ]]; do + local key="$1" + local value="$2" + shift 2 + case "$value" in + *$'\n'*) + echo "env value for $key must not contain newlines" >&2 + return 1 + ;; + esac + printf '%s=%s\n' "$key" "$value" >> "$path" + done +} + +render_remote43_host_env() { + local pg_container="$1" + local redis_container="$2" + local db_password="$3" + local db_name="$4" + local admin_email="$5" + local admin_password="$6" + local jwt_secret="$7" + local totp_key="$8" + + cat </dev/null 2>&1 || true + rm -f "\$CRM_PID_FILE" +fi +rm -f "\$CRM_LOG_FILE" + +sudo -n docker rm -f "\$APP_CONTAINER" "\$PG_CONTAINER" "\$REDIS_CONTAINER" >/dev/null 2>&1 || true +sudo -n docker network inspect "\$NETWORK_NAME" >/dev/null 2>&1 || sudo -n docker network create "\$NETWORK_NAME" >/dev/null + +sudo -n docker run -d --name "\$PG_CONTAINER" --network "\$NETWORK_NAME" \\ + -e POSTGRES_USER=sub2api \\ + -e POSTGRES_PASSWORD="\$DB_PASSWORD" \\ + -e POSTGRES_DB="\$DB_NAME" \\ + "\$PG_IMAGE" >/dev/null + +sudo -n docker run -d --name "\$REDIS_CONTAINER" --network "\$NETWORK_NAME" \\ + "\$REDIS_IMAGE" >/dev/null + +sleep 10 + +sudo -n docker run -d --name "\$APP_CONTAINER" --network "\$NETWORK_NAME" \\ + -p "127.0.0.1:\$HOST_PORT:\$HOST_CONTAINER_PORT" \\ + --env-file "\$HOST_ENV_FILE" \\ + -v "\$DATA_DIR:/app/data" \\ + -v "\$HOST_BINARY:/app/sub2api:ro" \\ + "\$HOST_IMAGE" /app/sub2api >/dev/null + +python3 - "\$HOST_ENV_FILE" "\$HOST_PORT" <<'PY' +import json +import pathlib +import subprocess +import sys +import time + +env_path = pathlib.Path(sys.argv[1]) +host_port = sys.argv[2] +values = {} +for line in env_path.read_text(encoding='utf-8').splitlines(): + if '=' not in line: + continue + key, value = line.split('=', 1) + values[key] = value + +payload = json.dumps({ + 'email': values['ADMIN_EMAIL'], + 'password': values['ADMIN_PASSWORD'], + 'turnstile_token': '', +}, ensure_ascii=False) +url = f"http://127.0.0.1:{host_port}/api/v1/auth/login" +for _ in range(60): + result = subprocess.run( + ['curl', '-fsS', '-H', 'Content-Type: application/json', '-X', 'POST', url, '-d', payload], + text=True, + capture_output=True, + ) + if result.returncode == 0 and 'access_token' in result.stdout: + raise SystemExit(0) + time.sleep(2) +raise SystemExit(f'host login did not become ready on {url}') +PY + +nohup bash -lc 'set -a; source "\$1"; set +a; exec "\$2"' _ "\$CRM_ENV_FILE" "\$CRM_BINARY" >"\$CRM_LOG_FILE" 2>&1 & +echo \$! > "\$CRM_PID_FILE" + +python3 - "\$CRM_PORT" <<'PY' +import subprocess +import sys +import time + +url = f"http://127.0.0.1:{sys.argv[1]}/healthz" +for _ in range(30): + result = subprocess.run(['curl', '-fsS', url], text=True, capture_output=True) + if result.returncode == 0 and result.stdout.strip() == 'ok': + raise SystemExit(0) + time.sleep(1) +raise SystemExit(f'crm healthz did not become ready on {url}') +PY + +printf 'host_base=http://127.0.0.1:%s\n' "\$HOST_PORT" +printf 'crm_base=http://127.0.0.1:%s\n' "\$CRM_PORT" +printf 'remote_host_env=%s\n' "\$HOST_ENV_FILE" +printf 'crm_log=%s\n' "\$CRM_LOG_FILE" +EOF +} diff --git a/scripts/setup_remote43_patched_stack.sh b/scripts/setup_remote43_patched_stack.sh new file mode 100755 index 00000000..b66c8ff0 --- /dev/null +++ b/scripts/setup_remote43_patched_stack.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck disable=SC1091 +source "$ROOT_DIR/scripts/remote43_patched_stack_lib.sh" + +KEY="${KEY:-/home/long/下载/zjsea.pem}" +REMOTE="${REMOTE:-ubuntu@43.155.133.187}" +STACK_NAME="${STACK_NAME:-sub2api-patched-$(date +%Y%m%d)}" +HOST_PORT="${HOST_PORT:-18139}" +CRM_PORT="${CRM_PORT:-18143}" +LOCAL_HOST_TUNNEL_PORT="${LOCAL_HOST_TUNNEL_PORT:-$HOST_PORT}" +LOCAL_CRM_TUNNEL_PORT="${LOCAL_CRM_TUNNEL_PORT:-$CRM_PORT}" +HOST_IMAGE="${HOST_IMAGE:-weishaw/sub2api:0.1.129}" +HOST_CONTAINER_PORT="${HOST_CONTAINER_PORT:-8080}" +PG_IMAGE="${PG_IMAGE:-postgres:16-alpine}" +REDIS_IMAGE="${REDIS_IMAGE:-redis:7-alpine}" +DB_NAME="${DB_NAME:-sub2api}" +DB_PASSWORD="${DB_PASSWORD:-$(remote43_random_hex 16)}" +ADMIN_EMAIL="${ADMIN_EMAIL:-admin@sub2api.local}" +ADMIN_PASSWORD="${ADMIN_PASSWORD:-Sub2API-Remote43-Temp-Admin-20260525}" +JWT_SECRET="${JWT_SECRET:-$(remote43_random_hex 24)}" +TOTP_ENCRYPTION_KEY="${TOTP_ENCRYPTION_KEY:-$(remote43_random_hex 32)}" +CRM_ADMIN_TOKEN="${CRM_ADMIN_TOKEN:-$(remote43_random_hex 24)}" +HOST_NAME="${HOST_NAME:-remote43-patched-${HOST_PORT}}" +HOST_BINARY="${HOST_BINARY:-}" +CRM_BINARY="${CRM_BINARY:-$ROOT_DIR/server}" +PACK_DIR="${PACK_DIR:-$ROOT_DIR/packs/openai-cn-pack}" +LOCAL_SHARED_PACK_DIR="${LOCAL_SHARED_PACK_DIR:-/tmp/openai-cn-pack-${STACK_NAME}}" +LOCAL_OPERATOR_ENV_FILE="${LOCAL_OPERATOR_ENV_FILE:-/tmp/remote43-patched-stack-${HOST_PORT}.env}" +LOCAL_TUNNEL_SCRIPT="${LOCAL_TUNNEL_SCRIPT:-/tmp/remote43-patched-stack-${HOST_PORT}.tunnel.sh}" +REMOTE_ROOT="${REMOTE_ROOT:-/home/ubuntu/${STACK_NAME}_${HOST_PORT}}" +REMOTE_PACK_PATH="${REMOTE_PACK_PATH:-$LOCAL_SHARED_PACK_DIR}" +REMOTE_HOST_ENV_FILE="$REMOTE_ROOT/.env.host" +REMOTE_CRM_ENV_FILE="$REMOTE_ROOT/.env.crm" +REMOTE_BOOTSTRAP_FILE="$REMOTE_ROOT/bootstrap.sh" +REMOTE_HOST_BINARY="$REMOTE_ROOT/sub2api-patched" +REMOTE_CRM_BINARY="$REMOTE_ROOT/sub2api-cn-relay-manager-server" +REMOTE_DATA_DIR="$REMOTE_ROOT/data" +REMOTE_CRM_DB_FILE="$REMOTE_ROOT/sub2api-cn-relay-manager.db" +REMOTE_CRM_PID_FILE="$REMOTE_ROOT/crm.pid" +REMOTE_CRM_LOG_FILE="$REMOTE_ROOT/crm.log" +REMOTE_APP_CONTAINER="${REMOTE_APP_CONTAINER:-${STACK_NAME}-app}" +REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-${STACK_NAME}-pg}" +REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-${STACK_NAME}-redis}" +REMOTE_NETWORK="${REMOTE_NETWORK:-${STACK_NAME}-net}" +DRY_RUN="${DRY_RUN:-0}" + +die() { + echo "$*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "missing command: $1" +} + +run_cmd() { + if [[ "$DRY_RUN" == "1" ]]; then + printf 'DRY_RUN:' + printf ' %q' "$@" + printf '\n' + return 0 + fi + "$@" +} + +ssh_remote() { + run_cmd ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$@" +} + +scp_remote() { + run_cmd scp -i "$KEY" -o StrictHostKeyChecking=no "$@" +} + +prepare_local_shared_pack() { + case "$LOCAL_SHARED_PACK_DIR" in + /tmp/*) ;; + *) + die "LOCAL_SHARED_PACK_DIR must stay under /tmp, got: $LOCAL_SHARED_PACK_DIR" + ;; + esac + + mkdir -p "$(dirname "$LOCAL_SHARED_PACK_DIR")" + rm -rf "$LOCAL_SHARED_PACK_DIR" + cp -R "$PACK_DIR" "$LOCAL_SHARED_PACK_DIR" +} + +write_local_tunnel_script() { + cat > "$LOCAL_TUNNEL_SCRIPT" < "$host_env_file" + render_remote43_crm_env \ + "$CRM_PORT" \ + "file:${REMOTE_CRM_DB_FILE}?_foreign_keys=on&_busy_timeout=5000" \ + "$CRM_ADMIN_TOKEN" > "$crm_env_file" + render_remote43_bootstrap_script \ + "$REMOTE_ROOT" \ + "$REMOTE_HOST_ENV_FILE" \ + "$REMOTE_CRM_ENV_FILE" \ + "$(basename "$REMOTE_HOST_BINARY")" \ + "$(basename "$REMOTE_CRM_BINARY")" \ + "$REMOTE_DATA_DIR" \ + "$REMOTE_CRM_DB_FILE" \ + "$REMOTE_CRM_PID_FILE" \ + "$REMOTE_CRM_LOG_FILE" \ + "$REMOTE_APP_CONTAINER" \ + "$REMOTE_PG_CONTAINER" \ + "$REMOTE_REDIS_CONTAINER" \ + "$REMOTE_NETWORK" \ + "$HOST_IMAGE" \ + "$PG_IMAGE" \ + "$REDIS_IMAGE" \ + "$DB_PASSWORD" \ + "$DB_NAME" \ + "$HOST_PORT" \ + "$CRM_PORT" \ + "$HOST_CONTAINER_PORT" > "$bootstrap_file" + chmod +x "$bootstrap_file" + + ssh_remote "mkdir -p $(printf '%q' "$REMOTE_ROOT") $(printf '%q' "$(dirname "$REMOTE_PACK_PATH")") && rm -rf $(printf '%q' "$REMOTE_PACK_PATH")" + scp_remote "$HOST_BINARY" "$REMOTE:$REMOTE_HOST_BINARY" + scp_remote "$CRM_BINARY" "$REMOTE:$REMOTE_CRM_BINARY" + scp_remote "$host_env_file" "$REMOTE:$REMOTE_HOST_ENV_FILE" + scp_remote "$crm_env_file" "$REMOTE:$REMOTE_CRM_ENV_FILE" + scp_remote "$bootstrap_file" "$REMOTE:$REMOTE_BOOTSTRAP_FILE" + scp_remote -r "$LOCAL_SHARED_PACK_DIR" "$REMOTE:$REMOTE_PACK_PATH" + ssh_remote "bash $(printf '%q' "$REMOTE_BOOTSTRAP_FILE")" + + cat < "$pack_dir/pack.json" printf '%s\n' '{"provider_id":"deepseek","base_url":"https://upstream.example.com/v1"}' > "$pack_dir/providers/deepseek.json" cat > "$fakebin/curl" <<'EOF' @@ -334,13 +335,13 @@ case "$url" in */api/import-batches/123) write_body '{"managed_resources":[{"ResourceType":"group","HostResourceID":"7","ResourceName":"DeepSeek 默认分组"}]}' ;; - */api/providers/deepseek/status*) + */api/providers/deepseek/status\?pack_id=openai-cn-pack\&host_id=remote43-current-host) write_body '{"status":"ready"}' ;; - */api/providers/deepseek/access/status*) + */api/providers/deepseek/access/status\?pack_id=openai-cn-pack\&host_id=remote43-current-host) write_body '{"latest_access_status":"subscription_ready"}' ;; - */api/providers/deepseek/access/preview*) + */api/providers/deepseek/access/preview\?pack_id=openai-cn-pack\&host_id=remote43-current-host) write_body '{"available":true}' ;; *) @@ -492,6 +493,7 @@ EOF HOST_BASE="http://127.0.0.1:18087" \ CRM_HOST_BASE="http://127.0.0.1:18093" \ REMOTE_HOST_BASE="http://127.0.0.1:18093" \ + HOST_NAME="human-friendly-host-name" \ ROOT="$artifact_dir/root" \ ART="$artifact_dir/run" \ PACK_PATH="$pack_dir" \ @@ -553,6 +555,14 @@ EOF assert_not_contains "$ssh_contents" "http://127.0.0.1:18087/v1/models" assert_not_contains "$ssh_contents" "http://127.0.0.1:18087/v1/chat/completions" assert_not_contains "$ssh_contents" "user-key" + + local provider_status + provider_status="$(cat "$artifact_dir/run/13-provider-status.json")" + assert_contains "$provider_status" '"status":"ready"' + + local access_status + access_status="$(cat "$artifact_dir/run/14-access-status.json")" + assert_contains "$access_status" '"latest_access_status":"subscription_ready"' } run_test_migrate_historical_artifacts() { @@ -642,10 +652,111 @@ EOF [[ -f "$sensitive_root/20260522_foo/05-subscription-access-prep.sql" ]] || fail "sql file was not moved to sensitive mirror" } +run_test_remote43_patched_stack_renderers() { + # shellcheck disable=SC1091 + source "$ROOT_DIR/scripts/remote43_patched_stack_lib.sh" + + local host_env crm_env bootstrap + host_env="$(render_remote43_host_env "stack-pg" "stack-redis" "db-pass" "sub2api" "admin@sub2api.local" "admin-pass" "jwt-secret" "totp-secret")" + crm_env="$(render_remote43_crm_env "18143" "file:/tmp/sub2api.db?_foreign_keys=on" "crm-token")" + bootstrap="$(render_remote43_bootstrap_script \ + "/home/ubuntu/test-stack" \ + "/home/ubuntu/test-stack/.env.host" \ + "/home/ubuntu/test-stack/.env.crm" \ + "sub2api-patched" \ + "sub2api-cn-relay-manager-server" \ + "/home/ubuntu/test-stack/data" \ + "/home/ubuntu/test-stack/sub2api-cn-relay-manager.db" \ + "/home/ubuntu/test-stack/crm.pid" \ + "/home/ubuntu/test-stack/crm.log" \ + "test-stack-app" \ + "test-stack-pg" \ + "test-stack-redis" \ + "test-stack-net" \ + "weishaw/sub2api:0.1.129" \ + "postgres:16-alpine" \ + "redis:7-alpine" \ + "db-pass" \ + "sub2api" \ + "18139" \ + "18143" \ + "8080")" + + assert_contains "$host_env" "AUTO_SETUP=true" + assert_contains "$host_env" "DATABASE_HOST=stack-pg" + assert_contains "$host_env" "REDIS_HOST=stack-redis" + assert_contains "$crm_env" "SUB2API_CRM_LISTEN_ADDR=127.0.0.1:18143" + assert_contains "$crm_env" "SUB2API_CRM_ADMIN_TOKEN=crm-token" + assert_contains "$bootstrap" 'rm -f "$DATA_DIR/install.lock" "$DATA_DIR/config.yaml" "$DATA_DIR/.installed"' + assert_contains "$bootstrap" '-v "$HOST_BINARY:/app/sub2api:ro"' + assert_contains "$bootstrap" '-p "127.0.0.1:$HOST_PORT:$HOST_CONTAINER_PORT"' + assert_contains "$bootstrap" '/api/v1/auth/login' + assert_contains "$bootstrap" '/healthz' + assert_contains "$bootstrap" 'source "$1"; set +a; exec "$2"' +} + +run_test_setup_remote43_patched_stack_dry_run() { + local tmpdir pack_dir shared_pack_dir host_bin crm_bin operator_env tunnel_script stdout_file ssh_key + tmpdir="$(mktemp -d)" + trap 'rm -rf "$tmpdir"' RETURN + pack_dir="$tmpdir/pack" + shared_pack_dir="$tmpdir/shared-pack" + host_bin="$tmpdir/sub2api-patched" + crm_bin="$tmpdir/server" + operator_env="$tmpdir/operator.env" + tunnel_script="$tmpdir/tunnel.sh" + stdout_file="$tmpdir/setup.stdout.txt" + ssh_key="$tmpdir/remote43.pem" + + mkdir -p "$pack_dir/providers" + printf '%s\n' '{"pack_id":"openai-cn-pack","version":"1.1.3"}' > "$pack_dir/pack.json" + printf '%s\n' '{"provider_id":"kimi-a7m"}' > "$pack_dir/providers/kimi-a7m.json" + printf '%s\n' '#!/usr/bin/env bash' > "$host_bin" + printf '%s\n' '#!/usr/bin/env bash' > "$crm_bin" + printf '%s\n' 'dummy-key' > "$ssh_key" + chmod +x "$host_bin" "$crm_bin" + + KEY="$ssh_key" \ + REMOTE="ubuntu@example.com" \ + STACK_NAME="test-stack" \ + HOST_PORT=18139 \ + CRM_PORT=18143 \ + HOST_BINARY="$host_bin" \ + CRM_BINARY="$crm_bin" \ + PACK_DIR="$pack_dir" \ + LOCAL_SHARED_PACK_DIR="$shared_pack_dir" \ + LOCAL_OPERATOR_ENV_FILE="$operator_env" \ + LOCAL_TUNNEL_SCRIPT="$tunnel_script" \ + REMOTE_ROOT="/home/ubuntu/test-stack" \ + DRY_RUN=1 \ + bash "$ROOT_DIR/scripts/setup_remote43_patched_stack.sh" >"$stdout_file" + + [[ -f "$operator_env" ]] || fail "operator env file was not created" + [[ -f "$tunnel_script" ]] || fail "tunnel script was not created" + [[ -f "$shared_pack_dir/pack.json" ]] || fail "shared pack mirror was not created" + + local stdout_text operator_env_text tunnel_text + stdout_text="$(cat "$stdout_file")" + operator_env_text="$(cat "$operator_env")" + tunnel_text="$(cat "$tunnel_script")" + + assert_contains "$stdout_text" "remote43 patched stack prepared" + assert_contains "$stdout_text" "local operator env file: $operator_env" + assert_contains "$stdout_text" "DRY_RUN: ssh -i $ssh_key" + assert_contains "$operator_env_text" "CRM_BASE=http://127.0.0.1:18143" + assert_contains "$operator_env_text" "HOST_BASE=http://127.0.0.1:18139" + assert_contains "$operator_env_text" "PACK_PATH=$shared_pack_dir" + assert_contains "$operator_env_text" "REMOTE_HOST_ENV_FILE=/home/ubuntu/test-stack/.env.host" + assert_contains "$tunnel_text" "-L 18143:127.0.0.1:18143" + assert_contains "$tunnel_text" "-L 18139:127.0.0.1:18139" +} + run_test_build_subscription_access_prep_sql run_test_real_host_acceptance_after_import_hook run_test_check_deepseek_completion_split run_test_import_remote43_provider_subscription_prep run_test_migrate_historical_artifacts +run_test_remote43_patched_stack_renderers +run_test_setup_remote43_patched_stack_dry_run echo "PASS: real host script regression checks"