cartethyia 2.7.0 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +107 -106
- package/README.md +25 -2
- package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-XaUmiBrC.js} +1 -1
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CqD7QiLM.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-0XUo4UMn.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-CXGS_Ct-.js} +1 -1
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-DAG5J9EY.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js +1 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +9 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-7P2n--Fv.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CxZ1fTkN.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +1 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-Dqf4zpS-.js} +1 -1
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-Byj2lAEm.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +56 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-ClCMCWWQ.js} +3 -3
- package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js +1 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-DFVFrnWW.js} +2 -2
- package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/providers/inception.svg +5 -0
- package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/{select-KIT0rC6G.js → select-DBNVLdUu.js} +1 -1
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
- package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
- package/package.json +96 -96
- package/scripts/cli/console-menu.ts +5 -2
- package/scripts/cli/tray.ts +21 -6
- package/src/boot/app.ts +684 -714
- package/src/boot/dependencies.ts +65 -62
- package/src/config/cooldowns.ts +16 -0
- package/src/config/providers.ts +13 -1
- package/src/config/resources.ts +70 -70
- package/src/config/security.ts +37 -37
- package/src/config/spec.ts +371 -358
- package/src/console/cli-tools/contracts.ts +10 -4
- package/src/console/console-router.ts +233 -225
- package/src/console/domain-registration.ts +398 -381
- package/src/console/providers/catalog/catalog-projections.ts +2 -6
- package/src/console/providers/catalog/contracts.ts +19 -0
- package/src/console/providers/catalog/provider-operations.ts +38 -0
- package/src/console/providers/catalog/routes.ts +14 -0
- package/src/console/providers/catalog/store-helpers.ts +326 -0
- package/src/console/providers/catalog/store.ts +1292 -1563
- package/src/console/quota/account-quota-global.ts +2 -2
- package/src/console/quota/account-quota-shared.ts +1 -1
- package/src/console/quota/account-quota-tenant.ts +35 -12
- package/src/console/quota/quota-view.ts +2 -2
- package/src/console/routing/pools/contracts.ts +41 -1
- package/src/console/routing/pools/operations.ts +183 -4
- package/src/console/routing/pools/relay-deploy.ts +17 -4
- package/src/console/routing/pools/routes.ts +30 -0
- package/src/console/routing/pools/store.ts +98 -8
- package/src/main.ts +183 -167
- package/src/network/outbound-fetch.ts +124 -208
- package/src/network/pool/agent.ts +15 -0
- package/src/network/pool/pool-state.ts +7 -0
- package/src/network/pool/selector.ts +110 -2
- package/src/network/pool-cooldown-store.ts +252 -0
- package/src/network/pool-health-machine.ts +5 -3
- package/src/network/pool-health.ts +36 -9
- package/src/observability/event-query.ts +57 -0
- package/src/observability/metrics.ts +467 -467
- package/src/observability/runtime-metrics.ts +269 -269
- package/src/persistence/health-event-flush.ts +2 -0
- package/src/persistence/schema.ts +19 -0
- package/src/persistence/usage-flush.ts +8 -3
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/dsml.ts +377 -0
- package/src/protocol/gateway-error.ts +232 -232
- package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
- package/src/protocol/request/chat.ts +7 -7
- package/src/protocol/request/codex.ts +2 -2
- package/src/protocol/request/gemini.ts +1 -1
- package/src/protocol/request/messages.ts +6 -6
- package/src/protocol/request/responses.ts +7 -7
- package/src/protocol/response/dsml.ts +12 -133
- package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
- package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
- package/src/{transport → protocol}/translation/capabilities.ts +2 -2
- package/src/{transport → protocol}/translation/normalize.ts +1 -1
- package/src/{transport → protocol}/translation/quirks.ts +1 -1
- package/src/{transport → protocol}/translation/search.ts +1 -1
- package/src/{transport → protocol}/translation/thinking.ts +23 -1
- package/src/{transport → protocol}/translation/tool-id.ts +1 -1
- package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream-parsing.ts +176 -0
- package/src/providers/adapters/compatible-adapter.ts +11 -2
- package/src/providers/authentication/oauth-refresh-service.ts +9 -5
- package/src/providers/default-registry.ts +22 -5
- package/src/providers/discovery/probe-phases.ts +16 -3
- package/src/providers/discovery/probing-service.ts +4 -1
- package/src/providers/integrations/anthropic.ts +1 -1
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
- package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
- package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
- package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
- package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
- package/src/providers/integrations/claude/claude.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +1 -1
- package/src/providers/integrations/factory/factory.ts +1 -1
- package/src/providers/integrations/github/github.ts +1 -1
- package/src/providers/integrations/grok/grok.ts +1 -1
- package/src/providers/integrations/inception/provider.ts +91 -0
- package/src/providers/integrations/inferhub.ts +1 -1
- package/src/providers/integrations/kiro/kiro-request.ts +1 -1
- package/src/providers/integrations/mistral.ts +193 -19
- package/src/providers/integrations/ollamacloud-models.ts +50 -0
- package/src/providers/integrations/opencode/fingerprint.ts +19 -2
- package/src/providers/integrations/opencode/prompt.ts +111 -0
- package/src/providers/integrations/opencode/provider.ts +101 -23
- package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
- package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -1
- package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
- package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
- package/src/providers/integrations/qoder/qoder.ts +315 -400
- package/src/providers/integrations/shared/install-id.ts +2 -1
- package/src/providers/integrations/trae/trae-cache.ts +110 -0
- package/src/providers/integrations/trae/trae-oauth.ts +29 -4
- package/src/providers/integrations/trae/trae-protocol.ts +2 -1
- package/src/providers/integrations/trae/trae.ts +106 -23
- package/src/providers/operations/account-error-classification.ts +1684 -0
- package/src/providers/operations/account-health-service.ts +27 -1503
- package/src/providers/operations/account-reset-service.ts +2 -0
- package/src/providers/operations/provider-catalog-service.ts +11 -1
- package/src/providers/provider-contracts.ts +7 -4
- package/src/providers/provider-metadata.ts +25 -4
- package/src/providers/provider-probe.ts +2 -2
- package/src/{console → providers}/quota/cache.ts +1 -1
- package/src/{console → providers}/quota/refresh.ts +5 -5
- package/src/security/cli-client-fingerprint.ts +9 -2
- package/src/transport/dispatch/attempt-finalize.ts +114 -12
- package/src/transport/dispatch/attempt-loop.ts +3 -0
- package/src/transport/dispatch/empty-stream.ts +9 -2
- package/src/transport/dispatch/generation.ts +37 -55
- package/src/transport/dispatch/handler-deps.ts +32 -0
- package/src/transport/dispatch/leases.ts +9 -4
- package/src/transport/dispatch/proxy-request.ts +6 -30
- package/src/transport/dispatch/responses-compact.ts +22 -43
- package/src/transport/dispatch/streaming/finalize.ts +3 -0
- package/src/transport/dispatch/streaming/stream.ts +0 -8
- package/src/transport/dispatch/systemone.ts +22 -42
- package/src/transport/dispatch/token-count.ts +23 -39
- package/src/transport/dispatch/upstream.ts +1 -3
- package/src/transport/dispatch/websearch.ts +22 -41
- package/src/transport/failure-policy.ts +92 -178
- package/src/transport/guards/abuse-guard.ts +10 -0
- package/src/transport/guards/auth-guard.ts +109 -109
- package/src/transport/guards/error-lifecycle.ts +1 -1
- package/src/transport/guards/pipeline.ts +220 -220
- package/src/transport/guards/request-context.ts +245 -238
- package/src/transport/request/degradation.ts +20 -1
- package/src/transport/request/inflight.ts +4 -114
- package/src/transport/request/preparer.ts +6 -6
- package/src/transport/request/state.ts +66 -38
- package/src/transport/request/tenant-preferences.ts +1 -1
- package/src/transport/routing/route-catalog.ts +75 -43
- package/src/transport/routing/router.ts +30 -2
- package/src/transport/surface/messages/parse.ts +1 -1
- package/src/transport/surface/responses/encode.ts +2 -3
- package/src/transport/surface/responses/parse.ts +49 -7
- package/src/types/undici.d.ts +33 -0
- package/src/workers/buddy-growth-tasks.ts +346 -425
- package/src/workers/daily-checkin.ts +17 -6
- package/src/workers/quota-refresh-sweep.ts +2 -2
- package/src/workers/scheduler.ts +17 -1
- package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
- package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
- package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
- package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
- package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
- package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
- package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
- package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
- package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
- package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
- package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
- package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
- package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
- package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
- package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
- package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
- package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
- package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
- package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
- package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
- package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
- package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
upstreamErrorIdentifier,
|
|
12
12
|
} from "../../../transport/failure-policy";
|
|
13
13
|
import { GatewayError } from "../../../protocol/gateway-error";
|
|
14
|
+
import { resolveImageSource } from "../../../protocol/codecs";
|
|
14
15
|
import type {
|
|
15
16
|
CanonicalEvent,
|
|
16
17
|
CanonicalRequest,
|
|
@@ -19,7 +20,7 @@ import type {
|
|
|
19
20
|
ToolChoice,
|
|
20
21
|
ToolDefinition,
|
|
21
22
|
} from "../../../protocol/canonical-model";
|
|
22
|
-
import { toolResultParts } from "../../../protocol/canonical-model";
|
|
23
|
+
import { toolResultParts, toolCallParts } from "../../../protocol/canonical-model";
|
|
23
24
|
import { decodeSseEvents } from "../../../protocol/sse-decoder";
|
|
24
25
|
import { usageFromProvider, readReasoningText } from "../../provider-usage";
|
|
25
26
|
import { readCredentialSecret, type ProviderDispatchTarget, type ProviderAdapter, type ProviderDispatchContext, type ProviderId } from "../../provider-contracts";
|
|
@@ -28,9 +29,62 @@ import { parseQoderOAuthState } from "./qoder-oauth";
|
|
|
28
29
|
import { getQoderVersion } from "../../operations/client-versions";
|
|
29
30
|
import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
|
|
30
31
|
import { qoderMachineOs } from "../../operations/cli-platform";
|
|
31
|
-
import {
|
|
32
|
+
import { qoderInferenceCandidates, QODER_CHAT_PATH, QODER_REALMS, type QoderRealm } from "./qoder-realm";
|
|
32
33
|
import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS, QODER_INTL_MODELS } from "./qoder-catalog";
|
|
33
34
|
import { isQoderVirtualModel, qoderDispatchModelId } from "./qoder-virtual";
|
|
35
|
+
import {
|
|
36
|
+
LEAK_MARKER_ANY_RE,
|
|
37
|
+
LEAK_MARKER_LITERALS,
|
|
38
|
+
parseQoderTextToolCalls,
|
|
39
|
+
isQoderEchoAttempt,
|
|
40
|
+
} from "./qoder-text-tools";
|
|
41
|
+
|
|
42
|
+
/** How long a trailing partial marker may be before it is held for the next frame. */
|
|
43
|
+
const LEAK_HOLD_WINDOW = 24;
|
|
44
|
+
/** Fail-open caps so an endless lookalike cannot hold text forever. */
|
|
45
|
+
const LEAK_HOLD_MAX_CHARS = 32_768;
|
|
46
|
+
const LEAK_HOLD_MAX_FRAMES = 200;
|
|
47
|
+
|
|
48
|
+
const BAD_ECHO_MESSAGE =
|
|
49
|
+
"Qoder returned an unparseable tool-call echo; no tools were emitted";
|
|
50
|
+
|
|
51
|
+
function badEchoError(): GatewayError {
|
|
52
|
+
return new GatewayError(
|
|
53
|
+
"platform_unavailable",
|
|
54
|
+
502,
|
|
55
|
+
BAD_ECHO_MESSAGE,
|
|
56
|
+
{ providerId: QODER_PROVIDER_ID },
|
|
57
|
+
"upstream",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when `suffix` is a strict prefix of some marker literal. */
|
|
62
|
+
function isLeakMarkerPrefix(suffix: string): boolean {
|
|
63
|
+
if (suffix.length === 0 || suffix.length >= LEAK_HOLD_WINDOW) return false;
|
|
64
|
+
return LEAK_MARKER_LITERALS.some((literal) => literal.startsWith(suffix));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* True when `text` is still plausibly the INTL marker echo block (marker +
|
|
69
|
+
* JSON array/object, or a tool-style tag). Used to keep holding across frames;
|
|
70
|
+
* false means it is ordinary prose and can be released.
|
|
71
|
+
*/
|
|
72
|
+
function qoderStillLeakCandidate(text: string): boolean {
|
|
73
|
+
if (text.length === 0) return true;
|
|
74
|
+
const marker = LEAK_MARKER_ANY_RE.exec(text);
|
|
75
|
+
if (!marker) {
|
|
76
|
+
return isLeakMarkerPrefix(text) || LEAK_MARKER_ANY_RE.test(text);
|
|
77
|
+
}
|
|
78
|
+
const rest = text.slice(marker.index + marker[0].length).trim();
|
|
79
|
+
if (rest.length === 0) return true;
|
|
80
|
+
if (rest.startsWith("[") || rest.startsWith("{")) return true;
|
|
81
|
+
if (/^<[a-zA-Z0-9_.:-]+\b/.test(rest)) return true;
|
|
82
|
+
// Angle-wrapped JSON: `<{` / `<[` — candidate until a closing `>` appears.
|
|
83
|
+
if (rest.startsWith("<{") || rest.startsWith("<[")) {
|
|
84
|
+
return !rest.includes(">") || rest.endsWith(">");
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
34
88
|
|
|
35
89
|
interface QoderModeProfile {
|
|
36
90
|
readonly chatUrl: string;
|
|
@@ -126,10 +180,25 @@ interface QoderModelConfig {
|
|
|
126
180
|
source?: string;
|
|
127
181
|
}
|
|
128
182
|
|
|
129
|
-
|
|
130
183
|
function qoderMd5(value: string | Uint8Array): string {
|
|
131
184
|
return createHash("md5").update(value).digest("hex");
|
|
132
185
|
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Canonical compact JSON used by Qoder's COSY signing: keys sorted
|
|
189
|
+
* lexicographically, no whitespace, null/undefined rendered as empty string.
|
|
190
|
+
* The upstream verifies the signature against this exact serialization, so a
|
|
191
|
+
* payload built with plain `JSON.stringify` (insertion order) fails with
|
|
192
|
+
* `code 101 / Signature invalid`.
|
|
193
|
+
*/
|
|
194
|
+
function qoderJsonSortedCompact(mapping: Record<string, unknown>): string {
|
|
195
|
+
const parts: string[] = [];
|
|
196
|
+
for (const key of Object.keys(mapping).sort()) {
|
|
197
|
+
const value = mapping[key] === null || mapping[key] === undefined ? "" : mapping[key];
|
|
198
|
+
parts.push(`${JSON.stringify(key)}:${JSON.stringify(value)}`);
|
|
199
|
+
}
|
|
200
|
+
return `{${parts.join(",")}}`;
|
|
201
|
+
}
|
|
133
202
|
function qoderPathSignature(url: string): string {
|
|
134
203
|
const path = new URL(url).pathname;
|
|
135
204
|
return path.startsWith("/algo") ? path.slice(5) : path;
|
|
@@ -245,7 +314,7 @@ function qoderBuildCosyHeaders(
|
|
|
245
314
|
version = getQoderVersion(),
|
|
246
315
|
): Record<string, string> {
|
|
247
316
|
const key = randomUUID().replaceAll("-", "").slice(0, 16);
|
|
248
|
-
const identity =
|
|
317
|
+
const identity = qoderJsonSortedCompact({
|
|
249
318
|
name: auth.userName,
|
|
250
319
|
aid: auth.userId,
|
|
251
320
|
uid: auth.userId,
|
|
@@ -260,7 +329,7 @@ function qoderBuildCosyHeaders(
|
|
|
260
329
|
const info = Buffer.concat([cipher.update(identity), cipher.final()]).toString("base64");
|
|
261
330
|
const cosyKey = publicEncrypt({ key: QODER_RSA_PUBLIC_KEY, padding: constants.RSA_PKCS1_PADDING }, Buffer.from(key)).toString("base64");
|
|
262
331
|
const payload = Buffer.from(
|
|
263
|
-
|
|
332
|
+
qoderJsonSortedCompact({ version: "v1", requestId: randomUUID(), info, cosyVersion: version, ideVersion: "" }),
|
|
264
333
|
).toString("base64");
|
|
265
334
|
const date = String(Math.floor(Date.now() / 1000));
|
|
266
335
|
const signature = qoderMd5(`${payload}\n${cosyKey}\n${date}\n${Buffer.from(body).toString("latin1")}\n${qoderPathSignature(url)}`);
|
|
@@ -512,16 +581,25 @@ function qoderKeepsReasoning(modelId: string): boolean {
|
|
|
512
581
|
*
|
|
513
582
|
* Presence, not length, decides: a client that already knows the rule sends
|
|
514
583
|
* `""` on a thinking turn whose text is not readable back, and dropping that
|
|
515
|
-
* empty string re-triggers the complaint it was sent to avoid.
|
|
516
|
-
*
|
|
584
|
+
* empty string re-triggers the complaint it was sent to avoid.
|
|
585
|
+
*
|
|
586
|
+
* DeepSeek multi-turn consistency also backfills the field on every assistant
|
|
587
|
+
* turn once the conversation carries a reasoning trace at all — the reference
|
|
588
|
+
* bridge does the same (`backfill_reasoning_content`), and notes the symptom
|
|
589
|
+
* when it is missed: occasional failures that sometimes pass on retry. An
|
|
590
|
+
* assistant turn with no reasoning and no trace gains nothing, so nothing is
|
|
591
|
+
* fabricated.
|
|
517
592
|
*/
|
|
518
593
|
function qoderReasoningEcho(
|
|
519
594
|
message: { content: readonly ContentPart[] },
|
|
520
595
|
modelId: string,
|
|
596
|
+
hasReasoningTrace = false,
|
|
521
597
|
): { reasoning_content: string } | Record<string, never> {
|
|
522
598
|
if (!qoderKeepsReasoning(modelId)) return {};
|
|
523
599
|
const reasoning = message.content.filter((part) => part.kind === "reasoning");
|
|
524
|
-
if (reasoning.length === 0)
|
|
600
|
+
if (reasoning.length === 0) {
|
|
601
|
+
return hasReasoningTrace ? { reasoning_content: "" } : {};
|
|
602
|
+
}
|
|
525
603
|
const text = reasoning
|
|
526
604
|
.map((part) => (part.kind === "reasoning" ? (part.summary ?? part.payload ?? "") : ""))
|
|
527
605
|
.join("");
|
|
@@ -576,7 +654,6 @@ function qoderTools(
|
|
|
576
654
|
return out;
|
|
577
655
|
}
|
|
578
656
|
|
|
579
|
-
|
|
580
657
|
/**
|
|
581
658
|
* Projects the canonical tool choice onto the upstream's field.
|
|
582
659
|
*
|
|
@@ -598,26 +675,18 @@ function qoderToolChoice(choice: ToolChoice | undefined): unknown {
|
|
|
598
675
|
return choice.mode;
|
|
599
676
|
}
|
|
600
677
|
|
|
601
|
-
/**
|
|
602
|
-
* Marks a tool call recorded into the text of an assistant turn.
|
|
603
|
-
*
|
|
604
|
-
* The upstream only reads structured turns on its CN realm, so on INTL a past
|
|
605
|
-
* call has to be described in prose or the model loses the record of what it
|
|
606
|
-
* asked for. The wording matches the reference bridge, which is what the
|
|
607
|
-
* upstream has been trained to read back.
|
|
608
|
-
*/
|
|
609
|
-
const QODER_TOOL_MARKER = "[assistant 请求调用工具]";
|
|
610
|
-
|
|
611
678
|
/**
|
|
612
679
|
* Whether this request sends its tool history as structured turns.
|
|
613
680
|
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
681
|
+
* Qoder's wire is OpenAI-compatible `tool_calls` deltas on every realm; the
|
|
682
|
+
* model is fed the same structured turns in both INTL and CN. Writing a past
|
|
683
|
+
* call as prose (`[assistant 请求调用工具] …`) contaminates the conversation
|
|
684
|
+
* history with a text echo the model starts imitating — the origin of the
|
|
685
|
+
* text-leak feedback loop. Structured history keeps the model seeing only real
|
|
686
|
+
* tool calls, so it never learns the prose marker.
|
|
618
687
|
*/
|
|
619
|
-
function qoderStructuredTools(
|
|
620
|
-
return
|
|
688
|
+
function qoderStructuredTools(_realm: QoderRealm): boolean {
|
|
689
|
+
return true;
|
|
621
690
|
}
|
|
622
691
|
|
|
623
692
|
export function buildQoderRequest(
|
|
@@ -644,12 +713,57 @@ export function buildQoderRequest(
|
|
|
644
713
|
const systemPrompt = systemParts.join("\n\n").trim();
|
|
645
714
|
const qoderMessages: Array<Record<string, unknown>> = [];
|
|
646
715
|
const structuredTools = qoderStructuredTools(realm);
|
|
716
|
+
// DeepSeek multi-turn: once any turn in the conversation carries a reasoning
|
|
717
|
+
// trace, every assistant message must replay `reasoning_content` (see
|
|
718
|
+
// `qoderReasoningEcho`).
|
|
719
|
+
const hasReasoningTrace = allMessages.some((message) =>
|
|
720
|
+
message.content.some((part) => part.kind === "reasoning"),
|
|
721
|
+
);
|
|
647
722
|
if (MODERN_PROFILE.mirrorTopLevelSystem && systemPrompt.length > 0) {
|
|
648
723
|
qoderMessages.push({
|
|
649
724
|
role: "system",
|
|
650
725
|
content: systemPrompt,
|
|
651
726
|
});
|
|
652
727
|
}
|
|
728
|
+
// Every tool result across the whole conversation, keyed by call id. An
|
|
729
|
+
// assistant `tool_calls` turn is only sent structured when every call has a
|
|
730
|
+
// matching result somewhere in the history — otherwise the upstream rejects
|
|
731
|
+
// the transcript ("An assistant message with 'tool_calls' must be followed by
|
|
732
|
+
// tool messages responding to each 'tool_call_id'"). Calls without a result
|
|
733
|
+
// are left out of the structured turn instead of poisoning it.
|
|
734
|
+
const toolResultsById = new Map<string, string>();
|
|
735
|
+
for (const msg of allMessages) {
|
|
736
|
+
for (const part of toolResultParts(msg)) {
|
|
737
|
+
if (typeof part.call_id === "string" && part.call_id.length > 0) {
|
|
738
|
+
toolResultsById.set(part.call_id, part.call_id);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// The tool calls that actually survive onto the wire. A call only makes it
|
|
743
|
+
// when every call on its assistant turn carries an id AND a matching result
|
|
744
|
+
// exists in the history. This is the single source of truth for both sides
|
|
745
|
+
// of the transcript: the assistant `tool_calls` emit and the `tool` result
|
|
746
|
+
// turns. If a result's call is not here, its `tool` turn is dropped too —
|
|
747
|
+
// leaving it would emit an orphan `tool` turn with no preceding call, which
|
|
748
|
+
// the upstream rejects ("Messages with role 'tool' must be a response to a
|
|
749
|
+
// preceding message with 'tool_calls'"). This is what a cancelled or
|
|
750
|
+
// compacted parallel call looks like, and it is the "insufficient tool
|
|
751
|
+
// messages" failure the client sees on the turn after.
|
|
752
|
+
const emittableCallIds = new Set<string>();
|
|
753
|
+
for (const msg of allMessages) {
|
|
754
|
+
if (msg.role !== "assistant") continue;
|
|
755
|
+
const calls = toolCallParts(msg);
|
|
756
|
+
if (calls.length === 0) continue;
|
|
757
|
+
const idsOk = calls.every(
|
|
758
|
+
(part) => typeof part.call_id === "string" && part.call_id.length > 0,
|
|
759
|
+
);
|
|
760
|
+
const everyCallHasResult = calls.every((part) =>
|
|
761
|
+
typeof part.call_id === "string" ? toolResultsById.has(part.call_id) : false,
|
|
762
|
+
);
|
|
763
|
+
if (idsOk && everyCallHasResult) {
|
|
764
|
+
for (const part of calls) emittableCallIds.add(part.call_id as string);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
653
767
|
for (const m of allMessages) {
|
|
654
768
|
if (m.role === "system" || m.role === "developer") continue;
|
|
655
769
|
// Answers may live in a `user` turn (the Messages ledger re-homes them),
|
|
@@ -658,7 +772,13 @@ export function buildQoderRequest(
|
|
|
658
772
|
// is emitted after, so nothing is duplicated or lost.
|
|
659
773
|
const results = structuredTools ? toolResultParts(m) : [];
|
|
660
774
|
if (results.length > 0) {
|
|
775
|
+
let emittedAnyTool = false;
|
|
661
776
|
for (const block of results) {
|
|
777
|
+
// Drop orphan results: their assistant `tool_calls` turn was filtered
|
|
778
|
+
// out (a sibling call has no result), and emitting the `tool` turn
|
|
779
|
+
// anyway would make the upstream reject the whole transcript.
|
|
780
|
+
if (!emittableCallIds.has(block.call_id)) continue;
|
|
781
|
+
emittedAnyTool = true;
|
|
662
782
|
const text = flattenCanonicalContent(
|
|
663
783
|
typeof block.content === "string"
|
|
664
784
|
? [{ kind: "text", text: block.content }]
|
|
@@ -676,11 +796,13 @@ export function buildQoderRequest(
|
|
|
676
796
|
// same content, which doubled the payload for no reason.
|
|
677
797
|
const remaining = m.content.filter((part) => part.kind !== "toolResult");
|
|
678
798
|
const flat = flattenCanonicalContent(remaining);
|
|
679
|
-
if (flat.length === 0) continue;
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
799
|
+
if (flat.length === 0 && !emittedAnyTool) continue;
|
|
800
|
+
if (flat.length > 0) {
|
|
801
|
+
qoderMessages.push({
|
|
802
|
+
role: "user",
|
|
803
|
+
content: flat,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
684
806
|
continue;
|
|
685
807
|
}
|
|
686
808
|
// An assistant turn that called tools must carry the calls, not just its
|
|
@@ -689,29 +811,16 @@ export function buildQoderRequest(
|
|
|
689
811
|
// rejected the whole transcript: "Messages with role 'tool' must be a
|
|
690
812
|
// response to a preceding message with 'tool_calls'".
|
|
691
813
|
const allCalls = m.content.filter((part) => part.kind === "toolCall");
|
|
692
|
-
const reasoningEcho = qoderReasoningEcho(m, modelId);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const prose = flattenCanonicalContent(textParts);
|
|
703
|
-
const recorded = allCalls.map((part) => ({
|
|
704
|
-
name: part.name,
|
|
705
|
-
arguments: typeof part.arguments === "string" ? part.arguments : "",
|
|
706
|
-
}));
|
|
707
|
-
qoderMessages.push({
|
|
708
|
-
role: "assistant",
|
|
709
|
-
content: (prose.length > 0 ? prose + "\n\n" : "") + QODER_TOOL_MARKER + "\n" + JSON.stringify(recorded),
|
|
710
|
-
...reasoningEcho,
|
|
711
|
-
});
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
const calls = structuredTools ? allCalls : [];
|
|
814
|
+
const reasoningEcho = qoderReasoningEcho(m, modelId, hasReasoningTrace);
|
|
815
|
+
// Structured tool turns are only safe when every call carries an id AND a
|
|
816
|
+
// matching result exists in the history — the upstream rejects a transcript
|
|
817
|
+
// whose `tool_calls` is not fully answered by `tool` turns. The same set
|
|
818
|
+
// that gates the `tool` emit above decides this, so the two halves of the
|
|
819
|
+
// transcript can never disagree.
|
|
820
|
+
const calls =
|
|
821
|
+
structuredTools && allCalls.every((part) => emittableCallIds.has(part.call_id))
|
|
822
|
+
? allCalls
|
|
823
|
+
: [];
|
|
715
824
|
if (calls.length > 0) {
|
|
716
825
|
const textParts = m.content.filter((part) => part.kind !== "toolCall");
|
|
717
826
|
const flat = flattenCanonicalContent(textParts);
|
|
@@ -724,18 +833,52 @@ export function buildQoderRequest(
|
|
|
724
833
|
type: "function",
|
|
725
834
|
function: {
|
|
726
835
|
name: part.name,
|
|
727
|
-
arguments:
|
|
836
|
+
arguments:
|
|
837
|
+
typeof part.arguments === "string"
|
|
838
|
+
? part.arguments
|
|
839
|
+
: part.arguments !== undefined && part.arguments !== null
|
|
840
|
+
? JSON.stringify(part.arguments)
|
|
841
|
+
: "",
|
|
728
842
|
},
|
|
729
843
|
})),
|
|
730
844
|
});
|
|
731
845
|
continue;
|
|
732
846
|
}
|
|
733
|
-
const flat = flattenCanonicalContent(m.content);
|
|
734
847
|
const role = m.role === "assistant" ? "assistant" : "user";
|
|
848
|
+
// Images must travel as structured OpenAI parts inside the message content —
|
|
849
|
+
// the upstream reads `image_url` parts there and not the top-level
|
|
850
|
+
// `image_urls` (mirroring `flatten_messages` in the reference bridge). A
|
|
851
|
+
// bare file_id reference degrades to a text note instead of being dropped.
|
|
852
|
+
const hasImages = m.content.some((part) => part.kind === "image");
|
|
853
|
+
if (hasImages) {
|
|
854
|
+
const parts: Array<Record<string, unknown>> = [];
|
|
855
|
+
for (const part of m.content) {
|
|
856
|
+
if (part.kind === "text") {
|
|
857
|
+
parts.push({ type: "text", text: part.text });
|
|
858
|
+
} else if (part.kind === "image") {
|
|
859
|
+
const source = resolveImageSource(part.payload);
|
|
860
|
+
if (source?.url !== undefined) {
|
|
861
|
+
parts.push({ type: "image_url", image_url: { url: source.url } });
|
|
862
|
+
} else {
|
|
863
|
+
parts.push({
|
|
864
|
+
type: "text",
|
|
865
|
+
text: source?.fileId !== undefined ? `[image: ${source.fileId}]` : "[image: unsupported source]",
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
qoderMessages.push({
|
|
871
|
+
role,
|
|
872
|
+
content: parts,
|
|
873
|
+
...(role === "assistant" ? qoderReasoningEcho(m, modelId, hasReasoningTrace) : {}),
|
|
874
|
+
});
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const flat = flattenCanonicalContent(m.content);
|
|
735
878
|
qoderMessages.push({
|
|
736
879
|
role,
|
|
737
880
|
content: flat,
|
|
738
|
-
...(role === "assistant" ? qoderReasoningEcho(m, modelId) : {}),
|
|
881
|
+
...(role === "assistant" ? qoderReasoningEcho(m, modelId, hasReasoningTrace) : {}),
|
|
739
882
|
});
|
|
740
883
|
}
|
|
741
884
|
const latestUserMessage = [...qoderMessages].reverse().find((m) => m["role"] === "user");
|
|
@@ -806,234 +949,6 @@ export function buildQoderRequest(
|
|
|
806
949
|
};
|
|
807
950
|
}
|
|
808
951
|
|
|
809
|
-
// ---------------------------------------------------------------------------
|
|
810
|
-
// DSML tool-call recovery — DeepSeek's native markup in the text stream
|
|
811
|
-
// ---------------------------------------------------------------------------
|
|
812
|
-
// Some DeepSeek-family models write their tool calls as XML-like DSML markup
|
|
813
|
-
// inside the plain-text `content` stream instead of the structured
|
|
814
|
-
// `tool_calls` field. Left alone, the markup reaches the caller as visible
|
|
815
|
-
// prose and the tool never runs — which is exactly the symptom reported.
|
|
816
|
-
//
|
|
817
|
-
// The reference bridge (`qoder_proxy.py`, `parse_dsml_tool_calls`) recovers
|
|
818
|
-
// these at end of stream. This is the same rule: buffer once DSML appears,
|
|
819
|
-
// parse at the end, emit the clean prose then the real tool calls.
|
|
820
|
-
//
|
|
821
|
-
// Patterns mirror the Python originals (`re.DOTALL` becomes the `s` flag):
|
|
822
|
-
// TAG_START = r"<[^>]*DSML[^>]*"
|
|
823
|
-
// DSML_CALLS_RE = TAG_START + r"calls>(.*?)</[^>]*DSML[^>]*calls>"
|
|
824
|
-
// DSML_INVOKE_RE = TAG_START + r"invoke\s+name=["']([^"']+)["']>(.*?)</[^>]*invoke>"
|
|
825
|
-
// DSML_PARAM_RE = TAG_START + r"parameter\s+name=["']([^"']+)["'][^>]*>(.*?)</[^>]*parameter>"
|
|
826
|
-
|
|
827
|
-
const DSML_CALLS_RE = /<[^>]*DSML[^>]*calls>([\s\S]*?)<\/[^>]*DSML[^>]*calls>/;
|
|
828
|
-
const DSML_INVOKE_RE = /<[^>]*DSML[^>]*invoke\s+name=["']([^"']+)["']>([\s\S]*?)<\/[^>]*invoke>/g;
|
|
829
|
-
const DSML_PARAM_RE = /<[^>]*DSML[^>]*parameter\s+name=["']([^"']+)["'][^>]*>([\s\S]*?)<\/[^>]*parameter>/g;
|
|
830
|
-
|
|
831
|
-
/** One recovered call, in the shape the canonical events want. */
|
|
832
|
-
export interface DsmlCall {
|
|
833
|
-
readonly id: string;
|
|
834
|
-
readonly name: string;
|
|
835
|
-
readonly arguments: string;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Recovers DSML tool calls out of a plain-text string.
|
|
840
|
-
*
|
|
841
|
-
* `clean` is the text with the markup removed. With no DSML present, `calls`
|
|
842
|
-
* is empty and `clean` is the original text unchanged — so this is safe to run
|
|
843
|
-
* over every stream, not just suspected ones.
|
|
844
|
-
*/
|
|
845
|
-
export function parseDsmlToolCalls(text: string): { calls: DsmlCall[]; clean: string } {
|
|
846
|
-
if (text.length === 0 || !text.includes("DSML")) return { calls: [], clean: text };
|
|
847
|
-
|
|
848
|
-
const callsMatch = DSML_CALLS_RE.exec(text);
|
|
849
|
-
let block: string;
|
|
850
|
-
let clean: string;
|
|
851
|
-
|
|
852
|
-
if (callsMatch) {
|
|
853
|
-
block = callsMatch[1] ?? "";
|
|
854
|
-
const before = text.slice(0, callsMatch.index).trimEnd();
|
|
855
|
-
const after = text.slice(callsMatch.index + callsMatch[0].length).trimStart();
|
|
856
|
-
clean = (before + " " + after).trim();
|
|
857
|
-
} else {
|
|
858
|
-
// No <calls> wrapper (issue #51): find bare <invoke> blocks in the whole
|
|
859
|
-
// text, then cut the prose off at the last `<` before the first invoke.
|
|
860
|
-
DSML_INVOKE_RE.lastIndex = 0;
|
|
861
|
-
const first = DSML_INVOKE_RE.exec(text);
|
|
862
|
-
if (!first) return { calls: [], clean: text };
|
|
863
|
-
block = text;
|
|
864
|
-
const cut = text.lastIndexOf("<", first.index);
|
|
865
|
-
clean = text.slice(0, cut >= 0 ? cut : first.index).trim();
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const calls: DsmlCall[] = [];
|
|
869
|
-
DSML_INVOKE_RE.lastIndex = 0;
|
|
870
|
-
let inv: RegExpExecArray | null;
|
|
871
|
-
while ((inv = DSML_INVOKE_RE.exec(block)) !== null) {
|
|
872
|
-
const params: Record<string, string> = {};
|
|
873
|
-
DSML_PARAM_RE.lastIndex = 0;
|
|
874
|
-
let par: RegExpExecArray | null;
|
|
875
|
-
while ((par = DSML_PARAM_RE.exec(inv[2] ?? "")) !== null) {
|
|
876
|
-
params[par[1] ?? ""] = (par[2] ?? "").trim();
|
|
877
|
-
}
|
|
878
|
-
calls.push({
|
|
879
|
-
id: `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
|
|
880
|
-
name: inv[1] ?? "",
|
|
881
|
-
arguments: JSON.stringify(params),
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
return { calls, clean };
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
/**
|
|
889
|
-
* Escapes the bare quotes that belong inside a JSON string value.
|
|
890
|
-
*
|
|
891
|
-
* Escaped quotes are parked first so they are not escaped twice, then every
|
|
892
|
-
* remaining quote is escaped, then the parked ones are restored.
|
|
893
|
-
*/
|
|
894
|
-
function repairJsonQuotes(text: string): string {
|
|
895
|
-
const ph = "\u0000";
|
|
896
|
-
return text.replace(/\\"/g, ph).replace(/"/g, '\\"').replaceAll(ph, '\\"');
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
/** `[{"name":"…","arguments":"<unescaped JSON>"}]`, one call only. */
|
|
900
|
-
const ECHO_LOOSE_RE = /^\[\s*\{\s*"name"\s*:\s*"([^"]{1,200})"\s*,\s*"arguments"\s*:\s*"(.*)"\s*\}\s*\]$/s;
|
|
901
|
-
|
|
902
|
-
/**
|
|
903
|
-
* Rebuilds the single-call echo shape when strict parsing fails.
|
|
904
|
-
*
|
|
905
|
-
* Returns `null` when the text is not that shape at all, so a caller can fall
|
|
906
|
-
* back rather than accepting a guess. The result still goes through the same
|
|
907
|
-
* strict checks as a normally-parsed array.
|
|
908
|
-
*/
|
|
909
|
-
function salvageEchoArray(rest: string): unknown {
|
|
910
|
-
const m = ECHO_LOOSE_RE.exec(rest.trim());
|
|
911
|
-
if (!m) return null;
|
|
912
|
-
const name = m[1] ?? "";
|
|
913
|
-
const raw = m[2] ?? "";
|
|
914
|
-
for (const candidate of [raw, repairJsonQuotes(raw)]) {
|
|
915
|
-
try {
|
|
916
|
-
JSON.parse(candidate);
|
|
917
|
-
return [{ name, arguments: candidate }];
|
|
918
|
-
} catch {
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
return null;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* Recovers a tool call the model echoed back as text.
|
|
927
|
-
*
|
|
928
|
-
* On INTL the upstream reads tool calls as prose, so a turn that called a tool
|
|
929
|
-
* is described with `QODER_TOOL_MARKER` followed by a JSON array of
|
|
930
|
-
* `{name, arguments}`. The model occasionally reproduces that shape verbatim as
|
|
931
|
-
* its answer instead of calling again, and left alone the client shows the JSON
|
|
932
|
-
* as content while the tool never runs.
|
|
933
|
-
*
|
|
934
|
-
* Only a well-formed array whose every entry names a tool and carries parseable
|
|
935
|
-
* arguments is accepted. Anything else returns no calls and the original text,
|
|
936
|
-
* so a model merely talking about the format is never mistaken for a call.
|
|
937
|
-
*/
|
|
938
|
-
export function parseLeakedToolCalls(
|
|
939
|
-
text: string,
|
|
940
|
-
allowedNames?: ReadonlySet<string>,
|
|
941
|
-
): { calls: DsmlCall[]; clean: string } {
|
|
942
|
-
if (text.length === 0 || !text.includes(QODER_TOOL_MARKER)) {
|
|
943
|
-
return { calls: [], clean: text };
|
|
944
|
-
}
|
|
945
|
-
const at = text.indexOf(QODER_TOOL_MARKER);
|
|
946
|
-
const head = text.slice(0, at).trim();
|
|
947
|
-
const rest = text.slice(at + QODER_TOOL_MARKER.length).trim();
|
|
948
|
-
|
|
949
|
-
let parsed: unknown;
|
|
950
|
-
try {
|
|
951
|
-
parsed = JSON.parse(rest);
|
|
952
|
-
} catch {
|
|
953
|
-
// A model echoing the call writes the inner quotes unescaped:
|
|
954
|
-
// `[{"name":"Read","arguments":"{"file_path":"a.ts"}"}]`. That is not
|
|
955
|
-
// valid JSON at either level, so the array has to be rebuilt before it can
|
|
956
|
-
// be trusted. Only the single-call shape is salvaged, which is what an echo
|
|
957
|
-
// almost always is.
|
|
958
|
-
const salvaged = salvageEchoArray(rest);
|
|
959
|
-
if (salvaged === null) return { calls: [], clean: text };
|
|
960
|
-
parsed = salvaged;
|
|
961
|
-
}
|
|
962
|
-
if (!Array.isArray(parsed) || parsed.length === 0) return { calls: [], clean: text };
|
|
963
|
-
|
|
964
|
-
const calls: DsmlCall[] = [];
|
|
965
|
-
for (const item of parsed) {
|
|
966
|
-
if (typeof item !== "object" || item === null) return { calls: [], clean: text };
|
|
967
|
-
const rec = item as Record<string, unknown>;
|
|
968
|
-
const name = typeof rec["name"] === "string" ? rec["name"].trim() : "";
|
|
969
|
-
if (name.length === 0) return { calls: [], clean: text };
|
|
970
|
-
if (allowedNames && !allowedNames.has(name)) return { calls: [], clean: text };
|
|
971
|
-
let args = rec["arguments"];
|
|
972
|
-
if (typeof args === "object" && args !== null) args = JSON.stringify(args);
|
|
973
|
-
if (typeof args !== "string") return { calls: [], clean: text };
|
|
974
|
-
// Arguments that do not parse are not a call this can trust.
|
|
975
|
-
if (args.trim().length > 0) {
|
|
976
|
-
try {
|
|
977
|
-
JSON.parse(args);
|
|
978
|
-
} catch {
|
|
979
|
-
return { calls: [], clean: text };
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
calls.push({ id: `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`, name, arguments: args });
|
|
983
|
-
}
|
|
984
|
-
return { calls, clean: head };
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
/**
|
|
988
|
-
* The holdable tail of a content chunk: a short suffix that looks like the
|
|
989
|
-
* start of a DSML tag and may finish in the next frame. Empty when there is
|
|
990
|
-
* nothing worth holding.
|
|
991
|
-
*
|
|
992
|
-
* Mirrors `_dsml_tail_hold` in the reference bridge.
|
|
993
|
-
*/
|
|
994
|
-
export function dsmlTailHold(text: string): string {
|
|
995
|
-
const at = text.lastIndexOf("<");
|
|
996
|
-
if (at < 0 || text.length - at > 24) return "";
|
|
997
|
-
const tail = text.slice(at);
|
|
998
|
-
// A complete tag already — nothing to wait for.
|
|
999
|
-
if (tail.includes(">")) return "";
|
|
1000
|
-
const body = tail.replace(/\uFF5C/g, "|").toUpperCase().slice(1).replace(/^\//, "");
|
|
1001
|
-
if (body.length === 0) return tail; // bare "<" or "</"
|
|
1002
|
-
if (!body.startsWith("|")) return "";
|
|
1003
|
-
const rest = body.slice(1);
|
|
1004
|
-
if (rest.length === 0) return tail;
|
|
1005
|
-
if ("DSML".startsWith(rest) || rest.startsWith("DSML")) return tail;
|
|
1006
|
-
return "";
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
/**
|
|
1010
|
-
* Removes leftover DSML closing tags from the tail of a text.
|
|
1011
|
-
*
|
|
1012
|
-
* The upstream sometimes leaks only the tail of a tool-call block — bare
|
|
1013
|
-
* `</parameter>` / `</invoke>` / `</calls>` with no opening and, in the
|
|
1014
|
-
* thinking channel, occasionally without the `DSML` marker at all. Those
|
|
1015
|
-
* fragments are pure protocol residue: they are never natural prose, so
|
|
1016
|
-
* dropping them is safe and keeps internal markup out of what the user sees.
|
|
1017
|
-
*
|
|
1018
|
-
* Anchored to the end, as in the reference bridge: a closing tag in the middle
|
|
1019
|
-
* of a text is not this residue and stripping it would eat real prose. The
|
|
1020
|
-
* caller loops, so several trailing tags come off one per pass.
|
|
1021
|
-
*
|
|
1022
|
-
* Mirrors `_strip_dsml_close_tail`, widened to the bare form because the
|
|
1023
|
-
* thinking channel does not always carry the marker.
|
|
1024
|
-
*/
|
|
1025
|
-
const DSML_CLOSE_TAG_RE = /<\/(?:[^>]*DSML[^>]*|parameter|invoke|calls|tool_calls|function_calls)\s*>\s*$/i;
|
|
1026
|
-
|
|
1027
|
-
export function stripDsmlCloseTags(text: string): string {
|
|
1028
|
-
let previous: string | null = null;
|
|
1029
|
-
let out = text.trimEnd();
|
|
1030
|
-
while (previous !== out) {
|
|
1031
|
-
previous = out;
|
|
1032
|
-
out = out.replace(DSML_CLOSE_TAG_RE, "").trimEnd();
|
|
1033
|
-
}
|
|
1034
|
-
return out;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
952
|
// Stream transformation — decode SSE, unwrap COSY envelope, map OpenAI chat chunks to CanonicalEvent
|
|
1038
953
|
|
|
1039
954
|
function mapQoderFinishReason(raw: unknown): CanonicalStopReason | undefined {
|
|
@@ -1053,20 +968,16 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1053
968
|
let finishReason: unknown = undefined;
|
|
1054
969
|
let rawUsage: Record<string, unknown> | undefined = undefined;
|
|
1055
970
|
let hasTerminal = false;
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
let
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
// so recovering only the content channel loses the call *and* leaves the
|
|
1067
|
-
// tags visible in the user's thinking.
|
|
1068
|
-
let reasonBuf = "";
|
|
1069
|
-
let reasonTail = "";
|
|
971
|
+
/** First-sighting id per wire tool-call index, for continuation chunks. */
|
|
972
|
+
const indexIds = new Map<number, string>();
|
|
973
|
+
/** Last seen call id/name, for index-less continuation fragments. */
|
|
974
|
+
let lastSeenToolId: string | undefined = undefined;
|
|
975
|
+
let lastSeenToolName: string | undefined = undefined;
|
|
976
|
+
// INTL leak-marker hold-back: text that may grow into the marker+JSON echo
|
|
977
|
+
// is withheld from the client until end of stream proves it is a tool call
|
|
978
|
+
// (recovered) or ordinary prose (released).
|
|
979
|
+
let leakBuf = "";
|
|
980
|
+
let leakFrames = 0;
|
|
1070
981
|
|
|
1071
982
|
for await (const sse of decodeSseEvents(body, { signal })) {
|
|
1072
983
|
const sseData = sse.data;
|
|
@@ -1095,66 +1006,120 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1095
1006
|
const delta = choice["delta"] as Record<string, unknown> | undefined;
|
|
1096
1007
|
if (delta) {
|
|
1097
1008
|
if (typeof delta["content"] === "string" && (delta["content"] as string).length > 0) {
|
|
1098
|
-
const piece =
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1009
|
+
const piece = delta["content"] as string;
|
|
1010
|
+
// Marker+JSON hold-back. A complete marker and everything after it is
|
|
1011
|
+
// held until end of stream; prose before it is emitted now.
|
|
1012
|
+
if (leakBuf.length > 0) {
|
|
1013
|
+
leakBuf += piece;
|
|
1014
|
+
leakFrames = piece.length === 0 ? leakFrames + 1 : 0;
|
|
1015
|
+
if (
|
|
1016
|
+
leakBuf.length > LEAK_HOLD_MAX_CHARS ||
|
|
1017
|
+
leakFrames > LEAK_HOLD_MAX_FRAMES ||
|
|
1018
|
+
!qoderStillLeakCandidate(leakBuf)
|
|
1019
|
+
) {
|
|
1020
|
+
if (leakBuf.length > 0) {
|
|
1021
|
+
yield {
|
|
1022
|
+
type: "content_delta",
|
|
1023
|
+
sequence_number: seq++,
|
|
1024
|
+
content: { kind: "text", text: leakBuf },
|
|
1025
|
+
} as CanonicalEvent;
|
|
1026
|
+
}
|
|
1027
|
+
leakBuf = "";
|
|
1028
|
+
leakFrames = 0;
|
|
1109
1029
|
}
|
|
1110
|
-
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const marker = LEAK_MARKER_ANY_RE.exec(piece);
|
|
1033
|
+
if (marker) {
|
|
1034
|
+
const emit = piece.slice(0, marker.index);
|
|
1035
|
+
if (emit.length > 0) {
|
|
1111
1036
|
yield {
|
|
1112
1037
|
type: "content_delta",
|
|
1113
1038
|
sequence_number: seq++,
|
|
1114
|
-
content: { kind: "text", text:
|
|
1039
|
+
content: { kind: "text", text: emit },
|
|
1115
1040
|
} as CanonicalEvent;
|
|
1116
1041
|
}
|
|
1117
|
-
|
|
1042
|
+
leakBuf = piece.slice(marker.index);
|
|
1043
|
+
leakFrames = 1;
|
|
1118
1044
|
continue;
|
|
1119
1045
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if (reasonBuf.length === 0) {
|
|
1128
|
-
const hold = dsmlTailHold(piece);
|
|
1129
|
-
const body = hold.length > 0 ? piece.slice(0, piece.length - hold.length) : piece;
|
|
1130
|
-
if (body.includes("DSML")) {
|
|
1131
|
-
reasonBuf = body;
|
|
1132
|
-
reasonTail = hold;
|
|
1133
|
-
continue;
|
|
1046
|
+
// No marker: hold only a possible partial-marker suffix.
|
|
1047
|
+
const tail = piece.slice(-LEAK_HOLD_WINDOW);
|
|
1048
|
+
let heldFrom = -1;
|
|
1049
|
+
for (let n = tail.length; n >= 1; n -= 1) {
|
|
1050
|
+
if (isLeakMarkerPrefix(tail.slice(-n))) {
|
|
1051
|
+
heldFrom = piece.length - n;
|
|
1052
|
+
break;
|
|
1134
1053
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1054
|
+
}
|
|
1055
|
+
if (heldFrom >= 0) {
|
|
1056
|
+
const emit = piece.slice(0, heldFrom);
|
|
1057
|
+
if (emit.length > 0) {
|
|
1137
1058
|
yield {
|
|
1138
1059
|
type: "content_delta",
|
|
1139
1060
|
sequence_number: seq++,
|
|
1140
|
-
content: { kind: "
|
|
1061
|
+
content: { kind: "text", text: emit },
|
|
1141
1062
|
} as CanonicalEvent;
|
|
1142
1063
|
}
|
|
1143
|
-
|
|
1064
|
+
leakBuf = piece.slice(heldFrom);
|
|
1065
|
+
leakFrames = 1;
|
|
1144
1066
|
continue;
|
|
1145
1067
|
}
|
|
1146
|
-
|
|
1068
|
+
yield {
|
|
1069
|
+
type: "content_delta",
|
|
1070
|
+
sequence_number: seq++,
|
|
1071
|
+
content: { kind: "text", text: piece },
|
|
1072
|
+
} as CanonicalEvent;
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
const reasoningText = readReasoningText(delta);
|
|
1076
|
+
if (typeof reasoningText === "string" && reasoningText.length > 0) {
|
|
1077
|
+
yield {
|
|
1078
|
+
type: "content_delta",
|
|
1079
|
+
sequence_number: seq++,
|
|
1080
|
+
content: { kind: "reasoning", payload: null, summary: reasoningText },
|
|
1081
|
+
} as CanonicalEvent;
|
|
1147
1082
|
continue;
|
|
1148
1083
|
}
|
|
1149
1084
|
const toolCalls = delta["tool_calls"] as Array<Record<string, unknown>> | undefined;
|
|
1150
1085
|
if (toolCalls) {
|
|
1151
1086
|
for (const tc of toolCalls) {
|
|
1152
1087
|
const fn = tc["function"] as Record<string, unknown> | undefined;
|
|
1088
|
+
const index = typeof tc["index"] === "number" ? (tc["index"] as number) : undefined;
|
|
1089
|
+
const declaredId = typeof tc["id"] === "string" ? (tc["id"] as string) : undefined;
|
|
1090
|
+
const fragName = typeof fn?.["name"] === "string" ? (fn["name"] as string) : undefined;
|
|
1091
|
+
// OpenAI continuation chunks re-identify their call by wire
|
|
1092
|
+
// `index` alone — the `id` appears only once, in the first chunk.
|
|
1093
|
+
// Some bridges omit `index` too, so a fragment then falls back to
|
|
1094
|
+
// the most recently seen call id (mirroring the generic chat
|
|
1095
|
+
// parser's `lastSeenToolId`). Pinning the first id per index and
|
|
1096
|
+
// reusing it for continuations is what keeps fragments attached
|
|
1097
|
+
// to the right call; synthesizing a fresh `call_<index>` id per
|
|
1098
|
+
// chunk instead made every continuation an unknown nameless call
|
|
1099
|
+
// that encoders dropped, so the client saw a call whose arguments
|
|
1100
|
+
// were always empty.
|
|
1101
|
+
let callId: string;
|
|
1102
|
+
if (declaredId !== undefined) {
|
|
1103
|
+
callId = declaredId;
|
|
1104
|
+
if (index !== undefined) indexIds.set(index, callId);
|
|
1105
|
+
lastSeenToolId = callId;
|
|
1106
|
+
if (fragName !== undefined) lastSeenToolName = fragName;
|
|
1107
|
+
} else if (index !== undefined && indexIds.has(index)) {
|
|
1108
|
+
callId = indexIds.get(index)!;
|
|
1109
|
+
} else if (
|
|
1110
|
+
lastSeenToolId !== undefined &&
|
|
1111
|
+
(fragName === undefined || lastSeenToolName === fragName)
|
|
1112
|
+
) {
|
|
1113
|
+
callId = lastSeenToolId;
|
|
1114
|
+
} else {
|
|
1115
|
+
callId = `call_${String(index ?? 0)}`;
|
|
1116
|
+
}
|
|
1153
1117
|
yield {
|
|
1154
1118
|
type: "tool_call_delta",
|
|
1155
1119
|
sequence_number: seq++,
|
|
1156
|
-
call_id:
|
|
1157
|
-
|
|
1120
|
+
call_id: callId,
|
|
1121
|
+
...(index !== undefined ? { index } : {}),
|
|
1122
|
+
...(fragName === undefined ? {} : { name: fragName }),
|
|
1158
1123
|
arguments_delta: (fn?.["arguments"] as string) ?? "",
|
|
1159
1124
|
} as CanonicalEvent;
|
|
1160
1125
|
}
|
|
@@ -1169,44 +1134,13 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1169
1134
|
if (json["finish_reason"] !== undefined) finishReason = json["finish_reason"];
|
|
1170
1135
|
}
|
|
1171
1136
|
}
|
|
1172
|
-
// End of stream:
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
sequence_number: seq++,
|
|
1180
|
-
content: { kind: "reasoning", payload: null, summary: visible },
|
|
1181
|
-
} as CanonicalEvent;
|
|
1182
|
-
}
|
|
1183
|
-
// A call recovered here counts exactly like one recovered from the content
|
|
1184
|
-
// channel — the two are deduplicated below, so a model that writes the
|
|
1185
|
-
// block in one channel and its tail in the other still yields one call.
|
|
1186
|
-
if (calls.length > 0) reasonCalls = calls;
|
|
1187
|
-
}
|
|
1188
|
-
if (buf.length > 0) {
|
|
1189
|
-
// Two ways a call can arrive as text: DSML markup, and the marker + JSON
|
|
1190
|
-
// array the upstream itself uses on INTL. The second is tried only when the
|
|
1191
|
-
// first finds nothing, so a DSML stream is never re-read as an echo.
|
|
1192
|
-
let { calls, clean } = parseDsmlToolCalls(buf);
|
|
1193
|
-
if (calls.length === 0) {
|
|
1194
|
-
const leaked = parseLeakedToolCalls(buf);
|
|
1195
|
-
if (leaked.calls.length > 0) {
|
|
1196
|
-
calls = leaked.calls;
|
|
1197
|
-
clean = leaked.clean;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
if (calls.length > 0) {
|
|
1201
|
-
const visible = stripDsmlCloseTags(clean).trim();
|
|
1202
|
-
if (visible.length > 0) {
|
|
1203
|
-
yield {
|
|
1204
|
-
type: "content_delta",
|
|
1205
|
-
sequence_number: seq++,
|
|
1206
|
-
content: { kind: "text", text: visible },
|
|
1207
|
-
} as CanonicalEvent;
|
|
1208
|
-
}
|
|
1209
|
-
for (const call of calls) {
|
|
1137
|
+
// End of stream: recover a held-back marker+JSON echo. A valid echo becomes
|
|
1138
|
+
// tool calls; anything else is released as ordinary prose (or fails closed
|
|
1139
|
+
// when it is unmistakably an echo that cannot be parsed).
|
|
1140
|
+
if (leakBuf.length > 0) {
|
|
1141
|
+
const leaked = parseQoderTextToolCalls(leakBuf);
|
|
1142
|
+
if (leaked.calls.length > 0) {
|
|
1143
|
+
for (const call of leaked.calls) {
|
|
1210
1144
|
yield {
|
|
1211
1145
|
type: "tool_call_delta",
|
|
1212
1146
|
sequence_number: seq++,
|
|
@@ -1215,13 +1149,14 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1215
1149
|
arguments_delta: call.arguments,
|
|
1216
1150
|
} as CanonicalEvent;
|
|
1217
1151
|
}
|
|
1218
|
-
reasonCalls = [];
|
|
1219
|
-
// The turn ended by calling a tool, whatever the upstream reported.
|
|
1220
1152
|
finishReason = "tool_calls";
|
|
1221
1153
|
} else {
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1224
|
-
|
|
1154
|
+
// An echo that could not be recovered must fail the turn (502) so the
|
|
1155
|
+
// harness retries — never surface protocol text as visible content.
|
|
1156
|
+
if (isQoderEchoAttempt(leakBuf)) {
|
|
1157
|
+
throw badEchoError();
|
|
1158
|
+
}
|
|
1159
|
+
const visible = leaked.clean.trim();
|
|
1225
1160
|
if (visible.length > 0) {
|
|
1226
1161
|
yield {
|
|
1227
1162
|
type: "content_delta",
|
|
@@ -1231,28 +1166,6 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1231
1166
|
}
|
|
1232
1167
|
}
|
|
1233
1168
|
}
|
|
1234
|
-
// Content carried no call of its own: whatever the thinking channel
|
|
1235
|
-
// recovered is the turn's call.
|
|
1236
|
-
for (const call of reasonCalls) {
|
|
1237
|
-
yield {
|
|
1238
|
-
type: "tool_call_delta",
|
|
1239
|
-
sequence_number: seq++,
|
|
1240
|
-
call_id: call.id,
|
|
1241
|
-
name: call.name,
|
|
1242
|
-
arguments_delta: call.arguments,
|
|
1243
|
-
} as CanonicalEvent;
|
|
1244
|
-
}
|
|
1245
|
-
// A recovered call outranks what the upstream reported. It says `stop`
|
|
1246
|
-
// because the markup never became a real tool call on its side, so leaving it
|
|
1247
|
-
// alone ends the turn "finished" with a call the caller never sees run.
|
|
1248
|
-
if (reasonCalls.length > 0) finishReason = "tool_calls";
|
|
1249
|
-
if (tail.length > 0) {
|
|
1250
|
-
yield {
|
|
1251
|
-
type: "content_delta",
|
|
1252
|
-
sequence_number: seq++,
|
|
1253
|
-
content: { kind: "text", text: tail },
|
|
1254
|
-
} as CanonicalEvent;
|
|
1255
|
-
}
|
|
1256
1169
|
// If no terminal arrived, synthesize one (prevents truncation error)
|
|
1257
1170
|
const stopReason = mapQoderFinishReason(finishReason);
|
|
1258
1171
|
const _providerStop = typeof finishReason === "string" ? finishReason : undefined;
|
|
@@ -1371,11 +1284,13 @@ class QoderAdapter implements ProviderAdapter {
|
|
|
1371
1284
|
const qoderBody = buildQoderRequest(modelId, request, modelConfig, auth, this.realm);
|
|
1372
1285
|
// A full URL from the route wins, but the fallback is this realm's
|
|
1373
1286
|
// gateway — not the intl host hardcoded into `MODERN_PROFILE`. Dispatching
|
|
1374
|
-
// a CN credential to `*.qoder.sh` is refused outright.
|
|
1287
|
+
// a CN credential to `*.qoder.sh` is refused outright. The host order
|
|
1288
|
+
// follows the token kind: job tokens (`jt-...`) belong on `api2`, which
|
|
1289
|
+
// is the only intl host that serves them (`api3` answers "Login expired").
|
|
1375
1290
|
const url =
|
|
1376
1291
|
candidate.endpoint_path && candidate.endpoint_path.startsWith("http")
|
|
1377
1292
|
? candidate.endpoint_path
|
|
1378
|
-
: `${
|
|
1293
|
+
: `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
|
|
1379
1294
|
|
|
1380
1295
|
const response = await callQoder(url, qoderBody, modelId, auth, lifecycle.signal, fetcher, version);
|
|
1381
1296
|
if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
|