cartethyia 2.4.6 → 2.5.0
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/README.md +45 -13
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-D3xqZbN-.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-u6W1UXSc.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-CTTpCb25.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-C5XpWlvy.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-DHoB3Agn.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-CvjZxHE4.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-D3nza7LH.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Dtt12WT1.js +1 -0
- package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-CbK3hXvU.js} +4 -4
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
- package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
- package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
- package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
- package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
- package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D70pChfn.js → index-CXm3FIyt.js} +2 -2
- package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
- package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
- package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.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 +51 -52
- package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
- package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
- package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
- package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
- package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
- package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
- package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
- package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
- package/package.json +3 -2
- package/scripts/cli/command/console-menu.ts +30 -11
- package/scripts/cli/command/start.ts +77 -13
- package/scripts/cli/command/update.ts +1 -1
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -101
- package/scripts/cli/ui/background.ts +110 -49
- package/src/boot/app.ts +20 -6
- package/src/boot/dependencies.ts +19 -0
- package/src/boot/elysia-typebox-bridge.ts +37 -0
- package/src/boot/lifecycle.ts +2 -0
- package/src/config/cooldowns.ts +138 -138
- package/src/config/providers.ts +5 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +9 -0
- package/src/console/api-keys/contracts.ts +414 -391
- package/src/console/api-keys/routes.ts +880 -870
- package/src/console/auth/account-reset-store.ts +14 -0
- package/src/console/auth/account-reset.ts +3 -2
- package/src/console/auth/setup-service.ts +6 -3
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -455
- package/src/console/observability/store.ts +1015 -952
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/store.ts +1532 -1525
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/updates/routes.ts +162 -4
- package/src/main.ts +3 -0
- package/src/network/http2-bidi.ts +85 -0
- package/src/network/outbound-fetch.ts +1 -26
- package/src/network/pool/agent.ts +478 -460
- package/src/network/pool/resolver.ts +1104 -544
- package/src/network/pool-health-machine.ts +12 -5
- package/src/observability/payload-capture.ts +344 -324
- package/src/persistence/db-pg.ts +9 -3
- package/src/persistence/db-sqlite.ts +497 -495
- package/src/persistence/health-event-flush.ts +137 -0
- package/src/persistence/migrate.ts +181 -129
- package/src/persistence/schema.ts +1360 -1295
- package/src/persistence/settings-revision.ts +25 -0
- package/src/persistence/shared-cache.ts +217 -0
- package/src/persistence/stores/share-store.ts +556 -528
- package/src/persistence/usage-flush.ts +61 -1
- package/src/protocol/canonical-model.ts +23 -1
- package/src/protocol/response/chat.ts +143 -10
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/providers/default-registry.ts +173 -1
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
- package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
- package/src/providers/integrations/claude/claude.ts +606 -582
- package/src/providers/integrations/cline/cline-quota.ts +18 -16
- package/src/providers/integrations/cline/cline.ts +539 -468
- package/src/providers/integrations/commandcode/commandcode.ts +675 -541
- package/src/providers/integrations/commandcode/discovery.ts +116 -0
- package/src/providers/integrations/commandcode/quota.ts +160 -0
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
- package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
- package/src/providers/integrations/cursor/cursor-request.ts +471 -0
- package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
- package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
- package/src/providers/integrations/cursor/cursor.ts +960 -0
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
- package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
- package/src/providers/integrations/github/github.ts +4 -4
- package/src/providers/integrations/hermes.ts +79 -8
- package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
- package/src/providers/integrations/kilo/kilo.ts +153 -146
- package/src/providers/integrations/opencode/provider.ts +532 -538
- package/src/providers/integrations/openrouter/provider.ts +139 -281
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
- package/src/providers/integrations/qoder/qoder.ts +865 -730
- package/src/providers/integrations/trae/trae-catalog.ts +190 -0
- package/src/providers/integrations/trae/trae-headers.ts +149 -0
- package/src/providers/integrations/trae/trae-oauth.ts +773 -0
- package/src/providers/integrations/trae/trae-protocol.ts +400 -0
- package/src/providers/integrations/trae/trae-realm.ts +220 -0
- package/src/providers/integrations/trae/trae.ts +458 -0
- package/src/providers/operations/account-health-service.ts +107 -10
- package/src/providers/operations/client-versions.ts +133 -6
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-credential-service.ts +46 -7
- package/src/providers/provider-contracts.ts +76 -0
- package/src/providers/provider-metadata.ts +78 -6
- package/src/providers/provider-model-definition.ts +373 -349
- package/src/security/api-key-auth.ts +126 -8
- package/src/transport/contracts.ts +3 -0
- package/src/transport/dispatch/attempt-finalize.ts +32 -1
- package/src/transport/dispatch/attempt-loop.ts +530 -515
- package/src/transport/dispatch/generation.ts +88 -21
- package/src/transport/dispatch/leases.ts +99 -2
- package/src/transport/dispatch/proxy-request.ts +60 -3
- package/src/transport/dispatch/streaming/finalize.ts +16 -1
- package/src/transport/dispatch/streaming/stream.ts +22 -2
- package/src/transport/dispatch/upstream.ts +19 -1
- package/src/transport/guards/auth-guard.ts +26 -1
- package/src/transport/guards/body-policy.ts +5 -4
- package/src/transport/request/preparer.ts +103 -0
- package/src/transport/request/state.ts +14 -0
- package/src/transport/routing/plan-gate.ts +45 -0
- package/src/transport/routing/route-catalog.ts +799 -777
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/src/network/tls/bun-client-hello.ts +0 -112
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// Trae adapter.
|
|
2
|
+
//
|
|
3
|
+
// Realm-aware: `trae` (intl) and `traecn` (CN) share this code but nothing of
|
|
4
|
+
// the address, the authorization scheme, or the roster. The intl deployment
|
|
5
|
+
// additionally gates on the *current* client version, which is why the version
|
|
6
|
+
// here comes from `getTraeVersion()` (a live resolver) rather than a constant.
|
|
7
|
+
|
|
8
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
9
|
+
import type { CanonicalEvent, CanonicalRequest, CanonicalStopReason } from "../../../protocol/canonical-model";
|
|
10
|
+
import { usageFromProvider } from "../../provider-usage";
|
|
11
|
+
import {
|
|
12
|
+
readCredentialSecret,
|
|
13
|
+
type ProviderAdapter,
|
|
14
|
+
type ProviderDispatchContext,
|
|
15
|
+
type ProviderDispatchTarget,
|
|
16
|
+
type ProviderId,
|
|
17
|
+
} from "../../provider-contracts";
|
|
18
|
+
import { mapUpstreamHttpError } from "../../../transport/failure-policy";
|
|
19
|
+
import { getTraeVersion } from "../../operations/client-versions";
|
|
20
|
+
import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
|
|
21
|
+
import { TRAE_CN_MODELS, TRAE_INTL_MODELS, traeMaxContextWindow } from "./trae-catalog";
|
|
22
|
+
import { buildTraeHeaders } from "./trae-headers";
|
|
23
|
+
import {
|
|
24
|
+
buildTraeChatBody,
|
|
25
|
+
isTraeAgentEndpoint,
|
|
26
|
+
parseTraeSse,
|
|
27
|
+
toTraeAgentBody,
|
|
28
|
+
TRAE_AGENT_ENDPOINT,
|
|
29
|
+
TRAE_CHAT_ENDPOINT,
|
|
30
|
+
} from "./trae-protocol";
|
|
31
|
+
import { TRAE_REGION_CORE_HOSTS, traeCoreCandidates, type TraeRealm } from "./trae-realm";
|
|
32
|
+
|
|
33
|
+
export interface TraeAdapterOptions {
|
|
34
|
+
readonly providerId?: string;
|
|
35
|
+
readonly realm?: TraeRealm;
|
|
36
|
+
readonly fetch?: typeof fetch;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Identity resolved from a persisted credential. */
|
|
40
|
+
interface TraeAuth {
|
|
41
|
+
readonly token: string;
|
|
42
|
+
readonly uid: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TRAE_PROVIDER_ID = "trae";
|
|
46
|
+
const TRAE_CN_PROVIDER_ID = "traecn";
|
|
47
|
+
|
|
48
|
+
/** Upstream finish reason → canonical stop reason. */
|
|
49
|
+
const STOP_REASONS: Readonly<Record<string, CanonicalStopReason>> = {
|
|
50
|
+
stop: "stop",
|
|
51
|
+
length: "length",
|
|
52
|
+
tool_calls: "tool_use",
|
|
53
|
+
function_call: "tool_use",
|
|
54
|
+
content_filter: "content_filter",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reads the credential.
|
|
59
|
+
*
|
|
60
|
+
* Trae credentials are OAuth token pairs, so the whole JSON export is the
|
|
61
|
+
* secret: extracting only the access token would drop the refresh token and
|
|
62
|
+
* leave the account permanently unrefreshable. The refresh itself runs through
|
|
63
|
+
* the registered OAuth refresher (`trae-oauth.ts`), not here — this only needs
|
|
64
|
+
* the access token to authorize a request.
|
|
65
|
+
*/
|
|
66
|
+
function traeAuth(secret: string, accountId: string | undefined): TraeAuth {
|
|
67
|
+
const token = extractToken(secret);
|
|
68
|
+
if (token === undefined) {
|
|
69
|
+
throw new GatewayError("authentication_failed", 401, "Trae credential has no access token", {}, "upstream");
|
|
70
|
+
}
|
|
71
|
+
return { token, uid: accountId ?? "" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractToken(secret: string): string | undefined {
|
|
75
|
+
const trimmed = secret.trim();
|
|
76
|
+
if (trimmed.startsWith("{")) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
79
|
+
const candidates = ["accessToken", "access_token", "token", "x-ide-token"];
|
|
80
|
+
for (const key of candidates) {
|
|
81
|
+
const value = parsed[key];
|
|
82
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Fall through to the bare-token reading below.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class TraeAdapter implements ProviderAdapter {
|
|
92
|
+
readonly provider_id: ProviderId;
|
|
93
|
+
private readonly realm: TraeRealm;
|
|
94
|
+
private readonly fetchFn: typeof fetch;
|
|
95
|
+
/**
|
|
96
|
+
* Maps a frame's `index` to the call id seen for it.
|
|
97
|
+
*
|
|
98
|
+
* Argument fragments arrive without the id, so the association has to be
|
|
99
|
+
* remembered between frames rather than read from the fragment itself.
|
|
100
|
+
*/
|
|
101
|
+
readonly #toolCallIds = new Map<string, string>();
|
|
102
|
+
|
|
103
|
+
constructor(options: TraeAdapterOptions = {}) {
|
|
104
|
+
this.provider_id = (options.providerId ?? TRAE_PROVIDER_ID) as ProviderId;
|
|
105
|
+
this.realm = options.realm ?? "intl";
|
|
106
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async *dispatch(
|
|
110
|
+
request: CanonicalRequest,
|
|
111
|
+
candidate: ProviderDispatchTarget,
|
|
112
|
+
context: ProviderDispatchContext,
|
|
113
|
+
): AsyncIterable<CanonicalEvent> {
|
|
114
|
+
if (candidate.wire_family !== "chat") {
|
|
115
|
+
throw new GatewayError(
|
|
116
|
+
"capability_unsupported",
|
|
117
|
+
400,
|
|
118
|
+
`trae supports chat only, got ${candidate.wire_family}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const secret = readCredentialSecret(context.credential, "Trae credential is required");
|
|
122
|
+
const modelId = candidate.model_id || request.model;
|
|
123
|
+
const lifecycle = createUpstreamDeadlineLifecycle(context);
|
|
124
|
+
const outboundFetch =
|
|
125
|
+
(context.outbound_fetch as unknown as typeof fetch | undefined) ?? this.fetchFn;
|
|
126
|
+
|
|
127
|
+
// intl refuses a stale client version with 4015, so the resolved version is
|
|
128
|
+
// part of every request rather than a startup detail.
|
|
129
|
+
const version = getTraeVersion();
|
|
130
|
+
const auth = traeAuth(secret, context.credential.account_id);
|
|
131
|
+
const body = buildTraeChatBody(modelId, request, this.realm);
|
|
132
|
+
/**
|
|
133
|
+
* The endpoint follows the model, not the provider.
|
|
134
|
+
*
|
|
135
|
+
* Trae's two surfaces are disjoint and neither serves the other's ids, so
|
|
136
|
+
* the roster's own `endpoint_path` decides. Falling back to the agent path
|
|
137
|
+
* rather than the chat one matters: the intl catalog is agent-only, and a
|
|
138
|
+
* caller whose target carries no route would otherwise land on a surface
|
|
139
|
+
* that rejects every model in it.
|
|
140
|
+
*/
|
|
141
|
+
const agentSurface = isTraeAgentEndpoint(candidate.endpoint_path);
|
|
142
|
+
const payload = agentSurface ? toTraeAgentBody(body) : body;
|
|
143
|
+
const headers = buildTraeHeaders({
|
|
144
|
+
realm: this.realm,
|
|
145
|
+
token: auth.token,
|
|
146
|
+
uid: auth.uid,
|
|
147
|
+
clientVersion: version,
|
|
148
|
+
stream: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const primary = traeCoreForAccount(this.realm, context.credential.auth_state);
|
|
152
|
+
const path = agentSurface ? TRAE_AGENT_ENDPOINT : TRAE_CHAT_ENDPOINT;
|
|
153
|
+
const url =
|
|
154
|
+
candidate.endpoint_path && candidate.endpoint_path.startsWith("http")
|
|
155
|
+
? candidate.endpoint_path
|
|
156
|
+
: `${primary}${path}`;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const response = await outboundFetch(url, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers,
|
|
162
|
+
body: JSON.stringify(payload),
|
|
163
|
+
signal: lifecycle.signal,
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) throw await mapUpstreamHttpError(response, this.provider_id);
|
|
166
|
+
if (!response.body) {
|
|
167
|
+
throw new GatewayError("platform_unavailable", 502, "Trae returned empty body", {}, "upstream");
|
|
168
|
+
}
|
|
169
|
+
// Headers arrived, so the pre-stream deadline has served its purpose;
|
|
170
|
+
// from here the gateway's own stall watchdog governs the body.
|
|
171
|
+
lifecycle.release();
|
|
172
|
+
yield* this.#events(
|
|
173
|
+
response.body as ReadableStream<Uint8Array>,
|
|
174
|
+
lifecycle.signal,
|
|
175
|
+
request.model,
|
|
176
|
+
);
|
|
177
|
+
} catch (err: unknown) {
|
|
178
|
+
const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
|
|
179
|
+
if (abortError) throw abortError;
|
|
180
|
+
throw err;
|
|
181
|
+
} finally {
|
|
182
|
+
lifecycle.release();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async *#events(
|
|
187
|
+
body: ReadableStream<Uint8Array>,
|
|
188
|
+
_signal: AbortSignal,
|
|
189
|
+
requestModel: string,
|
|
190
|
+
): AsyncIterable<CanonicalEvent> {
|
|
191
|
+
let seq = 1;
|
|
192
|
+
yield { type: "response_start", sequence_number: seq++, model: requestModel } as CanonicalEvent;
|
|
193
|
+
let stopReason: CanonicalStopReason = "stop";
|
|
194
|
+
let usage: Record<string, unknown> | undefined;
|
|
195
|
+
/**
|
|
196
|
+
* Tool calls arrive fragmented across frames and the id is not repeated.
|
|
197
|
+
*
|
|
198
|
+
* Upstream streams one `tool_calls` array per frame where only the first
|
|
199
|
+
* carries `id`, only early frames carry `name`, and `arguments` arrives as
|
|
200
|
+
* successive JSON fragments. So name is announced once on first sight of
|
|
201
|
+
* the id and arguments are accumulated; emitting each frame verbatim would
|
|
202
|
+
* produce several calls with empty ids.
|
|
203
|
+
*/
|
|
204
|
+
const announced = new Set<string>();
|
|
205
|
+
|
|
206
|
+
for await (const frame of parseTraeSse(body)) {
|
|
207
|
+
if (frame.event === "done") {
|
|
208
|
+
const data = frame.data as { finish_reason?: unknown };
|
|
209
|
+
if (typeof data.finish_reason === "string") {
|
|
210
|
+
stopReason = STOP_REASONS[data.finish_reason] ?? "stop";
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (frame.event === "error") {
|
|
215
|
+
const data = frame.data as { message?: unknown };
|
|
216
|
+
const message = typeof data.message === "string" ? data.message : "Trae stream error";
|
|
217
|
+
throw new GatewayError("platform_unavailable", 502, message, {}, "upstream");
|
|
218
|
+
}
|
|
219
|
+
if (typeof frame.data !== "object" || frame.data === null) continue;
|
|
220
|
+
const record = frame.data as Record<string, unknown>;
|
|
221
|
+
/**
|
|
222
|
+
* Two frame layouts reach here and both are live.
|
|
223
|
+
*
|
|
224
|
+
* The donor's client nests the answer under `chunk`; the region host this
|
|
225
|
+
* gateway talks to sends it flat — `event: output` with
|
|
226
|
+
* `{"response":"…"}` — and reports usage as a separate `token_usage`
|
|
227
|
+
* frame with the OpenAI spelling at the top level. Reading only `chunk`
|
|
228
|
+
* made a working upstream look like an empty stream.
|
|
229
|
+
*/
|
|
230
|
+
const chunk = record["chunk"];
|
|
231
|
+
if (typeof chunk !== "object" || chunk === null) {
|
|
232
|
+
const flat = record["response"];
|
|
233
|
+
if (typeof flat === "string" && flat.length > 0) {
|
|
234
|
+
yield {
|
|
235
|
+
type: "content_delta",
|
|
236
|
+
sequence_number: seq++,
|
|
237
|
+
content: { kind: "text", text: flat },
|
|
238
|
+
} as CanonicalEvent;
|
|
239
|
+
}
|
|
240
|
+
const flatReasoning = record["reasoning_content"];
|
|
241
|
+
if (typeof flatReasoning === "string" && flatReasoning.length > 0) {
|
|
242
|
+
yield {
|
|
243
|
+
type: "content_delta",
|
|
244
|
+
sequence_number: seq++,
|
|
245
|
+
content: { kind: "reasoning", payload: flatReasoning, summary: flatReasoning },
|
|
246
|
+
} as CanonicalEvent;
|
|
247
|
+
}
|
|
248
|
+
yield* this.#toolCallDeltas(record["tool_calls"], announced, () => seq++);
|
|
249
|
+
if (typeof record["finish_reason"] === "string") {
|
|
250
|
+
stopReason = STOP_REASONS[record["finish_reason"] as string] ?? "stop";
|
|
251
|
+
}
|
|
252
|
+
// `token_usage` frames carry the counters themselves, not under `usage`.
|
|
253
|
+
const counters =
|
|
254
|
+
record["usage"] !== undefined && typeof record["usage"] === "object"
|
|
255
|
+
? (record["usage"] as Record<string, unknown>)
|
|
256
|
+
: frame.event === "token_usage" || record["prompt_tokens"] !== undefined
|
|
257
|
+
? record
|
|
258
|
+
: undefined;
|
|
259
|
+
if (counters !== undefined) usage = counters;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const payload = chunk as Record<string, unknown>;
|
|
263
|
+
const text = payload["content"];
|
|
264
|
+
if (typeof text === "string" && text.length > 0) {
|
|
265
|
+
yield {
|
|
266
|
+
type: "content_delta",
|
|
267
|
+
sequence_number: seq++,
|
|
268
|
+
content: { kind: "text", text },
|
|
269
|
+
} as CanonicalEvent;
|
|
270
|
+
}
|
|
271
|
+
const reasoning = payload["reasoning_content"];
|
|
272
|
+
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
273
|
+
yield {
|
|
274
|
+
type: "content_delta",
|
|
275
|
+
sequence_number: seq++,
|
|
276
|
+
content: { kind: "reasoning", payload: reasoning, summary: reasoning },
|
|
277
|
+
} as CanonicalEvent;
|
|
278
|
+
}
|
|
279
|
+
if (typeof payload["finish_reason"] === "string") {
|
|
280
|
+
stopReason = STOP_REASONS[payload["finish_reason"]] ?? "stop";
|
|
281
|
+
}
|
|
282
|
+
if (typeof payload["usage"] === "object" && payload["usage"] !== null) {
|
|
283
|
+
usage = payload["usage"] as Record<string, unknown>;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (usage !== undefined) {
|
|
288
|
+
yield* this.#usageEvent(usage, seq++);
|
|
289
|
+
}
|
|
290
|
+
// The canonical terminal is `terminal`; a stream that ends without one is
|
|
291
|
+
// read as "the upstream never reported a result".
|
|
292
|
+
yield {
|
|
293
|
+
type: "terminal",
|
|
294
|
+
sequence_number: seq,
|
|
295
|
+
state: "complete",
|
|
296
|
+
stop_reason: stopReason,
|
|
297
|
+
} as unknown as CanonicalEvent;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Emits canonical tool-call deltas from one `tool_calls` array.
|
|
302
|
+
*
|
|
303
|
+
* Upstream nests the payload under `function_call`, not OpenAI's `function`,
|
|
304
|
+
* and spreads one call over many frames with three different shapes: the
|
|
305
|
+
* first carries id and name, the middle ones carry `arguments` as successive
|
|
306
|
+
* JSON fragments with an **empty** id, and the last repeats the id with no
|
|
307
|
+
* arguments. Grouping by `index` is what ties them together — the id is
|
|
308
|
+
* absent exactly when the arguments are present, so keying on id would drop
|
|
309
|
+
* every fragment and yield `arguments: "{}"`.
|
|
310
|
+
*/
|
|
311
|
+
async *#toolCallDeltas(
|
|
312
|
+
raw: unknown,
|
|
313
|
+
announced: Set<string>,
|
|
314
|
+
nextSeq: () => number,
|
|
315
|
+
): AsyncIterable<CanonicalEvent> {
|
|
316
|
+
if (!Array.isArray(raw)) return;
|
|
317
|
+
for (const entry of raw) {
|
|
318
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
319
|
+
const call = entry as Record<string, unknown>;
|
|
320
|
+
const fn = call["function_call"];
|
|
321
|
+
const fnRecord = typeof fn === "object" && fn !== null ? (fn as Record<string, unknown>) : undefined;
|
|
322
|
+
const name = typeof fnRecord?.["name"] === "string" ? (fnRecord["name"] as string) : undefined;
|
|
323
|
+
const args = typeof fnRecord?.["arguments"] === "string" ? (fnRecord["arguments"] as string) : "";
|
|
324
|
+
const declaredId = typeof call["id"] === "string" ? call["id"] : "";
|
|
325
|
+
const index = typeof call["index"] === "number" ? call["index"] : 0;
|
|
326
|
+
const slot = `idx:${index}`;
|
|
327
|
+
|
|
328
|
+
if (declaredId.length > 0) {
|
|
329
|
+
this.#toolCallIds.set(slot, declaredId);
|
|
330
|
+
// The closing frame repeats a known id with no payload; emitting it
|
|
331
|
+
// again would append a duplicate empty call.
|
|
332
|
+
if (announced.has(declaredId)) continue;
|
|
333
|
+
announced.add(declaredId);
|
|
334
|
+
yield {
|
|
335
|
+
type: "tool_call_delta",
|
|
336
|
+
sequence_number: nextSeq(),
|
|
337
|
+
call_id: declaredId,
|
|
338
|
+
name: name ?? "",
|
|
339
|
+
arguments_delta: "",
|
|
340
|
+
} as CanonicalEvent;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (args.length === 0) continue;
|
|
345
|
+
const id = this.#toolCallIds.get(slot);
|
|
346
|
+
if (id === undefined) continue;
|
|
347
|
+
yield {
|
|
348
|
+
type: "tool_call_delta",
|
|
349
|
+
sequence_number: nextSeq(),
|
|
350
|
+
call_id: id,
|
|
351
|
+
arguments_delta: args,
|
|
352
|
+
} as CanonicalEvent;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Emits the terminal usage event, or none at all.
|
|
358
|
+
*
|
|
359
|
+
* Trae reports usage with the OpenAI spelling (`prompt_tokens` /
|
|
360
|
+
* `completion_tokens`). `usageFromProvider` normalizes both spellings and
|
|
361
|
+
* returns `undefined` when nothing was actually reported, and an absent
|
|
362
|
+
* usage event is correct in that case — emitting zeros would record a
|
|
363
|
+
* request that cost nothing.
|
|
364
|
+
*/
|
|
365
|
+
async *#usageEvent(usage: Record<string, unknown>, seq: number): AsyncIterable<CanonicalEvent> {
|
|
366
|
+
const record = usageFromProvider(usage);
|
|
367
|
+
if (record === undefined) return;
|
|
368
|
+
yield { type: "usage", sequence_number: seq, usage: record } as unknown as CanonicalEvent;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function createTraeAdapter(options: TraeAdapterOptions = {}): ProviderAdapter {
|
|
373
|
+
return new TraeAdapter(options);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Adapter for the China deployment: different host, auth scheme, and roster. */
|
|
377
|
+
export function createTraeCnAdapter(options: TraeAdapterOptions = {}): ProviderAdapter {
|
|
378
|
+
return new TraeAdapter({
|
|
379
|
+
...options,
|
|
380
|
+
realm: "cn",
|
|
381
|
+
providerId: TRAE_CN_PROVIDER_ID,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Live model discovery.
|
|
387
|
+
*
|
|
388
|
+
* The listing is per account region, not per realm: it answers on CN and on the
|
|
389
|
+
* intl region host that serves the account (`coresg-normal.trae.ai` verified),
|
|
390
|
+
* while the generic intl core answers `403 no schema found`. An empty result is
|
|
391
|
+
* therefore a real "this account has no listing", and the static roster stays
|
|
392
|
+
* the fallback rather than being replaced by nothing.
|
|
393
|
+
*/
|
|
394
|
+
export async function discoverTraeModels(input: {
|
|
395
|
+
readonly realm: TraeRealm;
|
|
396
|
+
readonly token: string;
|
|
397
|
+
readonly uid: string;
|
|
398
|
+
readonly fetcher: typeof fetch;
|
|
399
|
+
readonly signal?: AbortSignal;
|
|
400
|
+
readonly providerId?: string;
|
|
401
|
+
/** Account region, so the listing is asked of the host that serves it. */
|
|
402
|
+
readonly region?: string;
|
|
403
|
+
}): Promise<readonly { id: string; ctx: number | undefined }[]> {
|
|
404
|
+
const headers = buildTraeHeaders({
|
|
405
|
+
realm: input.realm,
|
|
406
|
+
token: input.token,
|
|
407
|
+
uid: input.uid,
|
|
408
|
+
});
|
|
409
|
+
const url = `${traeCoreForAccount(input.realm, input.region === undefined ? undefined : { region: input.region })}/api/ide/v1/model_list`;
|
|
410
|
+
const response = await input.fetcher(url, {
|
|
411
|
+
method: "POST",
|
|
412
|
+
headers,
|
|
413
|
+
body: JSON.stringify({ type: "chat" }),
|
|
414
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
415
|
+
});
|
|
416
|
+
if (!response.ok) return [];
|
|
417
|
+
const parsed = (await response.json()) as {
|
|
418
|
+
data?: unknown;
|
|
419
|
+
models?: unknown;
|
|
420
|
+
model_configs?: unknown;
|
|
421
|
+
};
|
|
422
|
+
const rows = Array.isArray(parsed.model_configs)
|
|
423
|
+
? parsed.model_configs
|
|
424
|
+
: Array.isArray(parsed.models)
|
|
425
|
+
? parsed.models
|
|
426
|
+
: Array.isArray(parsed.data)
|
|
427
|
+
? parsed.data
|
|
428
|
+
: [];
|
|
429
|
+
return rows
|
|
430
|
+
.filter((row): row is Record<string, unknown> => typeof row === "object" && row !== null)
|
|
431
|
+
.map((row) => ({
|
|
432
|
+
// `model_name` is the key upstream's listing uses; the others are the
|
|
433
|
+
// spellings the older donors assumed.
|
|
434
|
+
id: String(row["model_name"] ?? row["name"] ?? row["id"] ?? "").trim(),
|
|
435
|
+
ctx: traeMaxContextWindow(row),
|
|
436
|
+
}))
|
|
437
|
+
.filter((row) => row.id.length > 0);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export { TRAE_CN_MODELS, TRAE_INTL_MODELS };
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Core host for one account.
|
|
444
|
+
*
|
|
445
|
+
* An intl account is provisioned into a region and its token only authenticates
|
|
446
|
+
* against that region's host, so the account's own `region` wins over the
|
|
447
|
+
* realm default. Falling back to the realm keeps older accounts (stored before
|
|
448
|
+
* the region was captured) working exactly as before.
|
|
449
|
+
*/
|
|
450
|
+
function traeCoreForAccount(
|
|
451
|
+
realm: TraeRealm,
|
|
452
|
+
authState: Readonly<Record<string, unknown>> | undefined,
|
|
453
|
+
): string {
|
|
454
|
+
const region = typeof authState?.["region"] === "string" ? authState["region"].toLowerCase() : "";
|
|
455
|
+
const regional = TRAE_REGION_CORE_HOSTS[region];
|
|
456
|
+
if (regional !== undefined) return regional;
|
|
457
|
+
return traeCoreCandidates(realm)[0] ?? "";
|
|
458
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { healthEventBuffer } from "../../persistence/health-event-flush";
|
|
1
2
|
// Account health: error classification, failure/success recording, recovery and cooldown sweeps.
|
|
2
3
|
import { and, desc, eq, inArray, isNotNull, lte, or, sql } from "drizzle-orm";
|
|
3
4
|
import { affectedRows, pruneExpiredCooldowns } from "../../persistence/dialect";
|
|
@@ -214,6 +215,20 @@ export interface AccountErrorClassification {
|
|
|
214
215
|
readonly reason: string;
|
|
215
216
|
readonly retryAt: Date | null;
|
|
216
217
|
readonly mutatesAccount: boolean;
|
|
218
|
+
/**
|
|
219
|
+
* True when this write changed a field the routing snapshot actually reads.
|
|
220
|
+
*
|
|
221
|
+
* The snapshot projects `provider_accounts.status` and
|
|
222
|
+
* `.cooldownUntil` and nothing else (`route-catalog.ts:286-287`), so only a
|
|
223
|
+
* write that moves one of those has to drop it. Everything else a failure
|
|
224
|
+
* persists — the failure streak, the error text, the category, a
|
|
225
|
+
* model-scoped cooldown — is invisible to routing, and invalidating for it
|
|
226
|
+
* forced the next request to rebuild an 11-SELECT snapshot for no reason.
|
|
227
|
+
*
|
|
228
|
+
* That made a failure wave self-sustaining: N failed attempts produced N
|
|
229
|
+
* invalidations, and every following request paid the full rebuild.
|
|
230
|
+
*/
|
|
231
|
+
readonly routesVisible: boolean;
|
|
217
232
|
/**
|
|
218
233
|
* True when `cooldownMs` came from the provider's own statement — a parsed
|
|
219
234
|
* `Retry-After`-family header, or a reset quoted in its message — rather than
|
|
@@ -390,6 +405,11 @@ export function classifyAccountError(
|
|
|
390
405
|
reason: origin === "upstream" ? rawUpstreamResponse ?? message.slice(0, 160) : explanation,
|
|
391
406
|
retryAt,
|
|
392
407
|
mutatesAccount,
|
|
408
|
+
// Set below by the caller that knows which columns it wrote, not here:
|
|
409
|
+
// the same classification feeds both the account-wide path (status +
|
|
410
|
+
// cooldownUntil — routing-visible) and the model-scoped path (neither —
|
|
411
|
+
// invisible). Defaulting true keeps existing callers conservative.
|
|
412
|
+
routesVisible: true,
|
|
393
413
|
providerStated,
|
|
394
414
|
modelUnsupported,
|
|
395
415
|
});
|
|
@@ -457,6 +477,16 @@ export function classifyAccountError(
|
|
|
457
477
|
!priceRefusal &&
|
|
458
478
|
!deploymentInfraError &&
|
|
459
479
|
(statusCode === 402 ||
|
|
480
|
+
// Qoder answers an exhausted credit balance with
|
|
481
|
+
// `{"code":"112","message":"{\"pricingUrl\":...}"}` — the body points at
|
|
482
|
+
// the pricing page, i.e. "buy more", which is a balance verdict, not an
|
|
483
|
+
// auth one. It fell through to `auth_invalidated` and the console showed
|
|
484
|
+
// "Re-login required" on an account whose credential was fine, sending
|
|
485
|
+
// operators to re-authenticate instead of topping up.
|
|
486
|
+
// Recognising it here keeps it out of `auth_invalidated`; the branch
|
|
487
|
+
// below scopes the resulting cooldown to the model, because the account
|
|
488
|
+
// keeps serving its other models.
|
|
489
|
+
providerCode === "112" ||
|
|
460
490
|
providerCode === "insufficient_quota" ||
|
|
461
491
|
providerCode === "quota_exceeded" ||
|
|
462
492
|
providerCode === "subscription:free-usage-exhausted" ||
|
|
@@ -489,7 +519,9 @@ export function classifyAccountError(
|
|
|
489
519
|
lower.includes("insufficient balance") ||
|
|
490
520
|
lower.includes("out of credits") ||
|
|
491
521
|
lower.includes("credits exhausted") ||
|
|
492
|
-
lower.includes("spending limit")
|
|
522
|
+
lower.includes("spending limit") ||
|
|
523
|
+
// Same Qoder family reached by body text when the code is not parsed out.
|
|
524
|
+
(lower.includes("pricingurl") && lower.includes("qoder.com/pricing")));
|
|
493
525
|
// A deterministic content-policy rejection is NOT credential invalidation.
|
|
494
526
|
// CodeBuddy returns HTTP 403 with provider code 11140 ("request illegal",
|
|
495
527
|
// "did not pass the safety review") — refreshing the token cannot change
|
|
@@ -687,6 +719,31 @@ export function classifyAccountError(
|
|
|
687
719
|
}
|
|
688
720
|
|
|
689
721
|
if (quotaSignal) {
|
|
722
|
+
/**
|
|
723
|
+
* Qoder's `112` is a verdict on the (account, model) pair, not on the
|
|
724
|
+
* credential or the balance as a whole.
|
|
725
|
+
*
|
|
726
|
+
* The body is `{"code":"112","message":"{\"pricingUrl\":…}"}` — a link to
|
|
727
|
+
* the pricing page, i.e. "this model is not in your plan". Observed
|
|
728
|
+
* behaviour agrees: the account keeps serving every other model after the
|
|
729
|
+
* refusal. Parking the whole account therefore took healthy models down
|
|
730
|
+
* with the one that is not entitled, which is the same mistake the
|
|
731
|
+
* `model_unreachable` branch already avoids for Cursor and buddy.
|
|
732
|
+
*/
|
|
733
|
+
const qoderPlanGated = providerCode === "112";
|
|
734
|
+
if (qoderPlanGated) {
|
|
735
|
+
const hits = Math.max(1, Math.floor(options?.modelBlockHits ?? 1));
|
|
736
|
+
const cooldownMs = unsupportedModelCooldownMs(hits);
|
|
737
|
+
return result(
|
|
738
|
+
"model_unreachable",
|
|
739
|
+
"cooldown",
|
|
740
|
+
cooldownMs,
|
|
741
|
+
new Date(Date.now() + cooldownMs),
|
|
742
|
+
reason("Model not included in this account's plan"),
|
|
743
|
+
origin === "upstream" && options?.modelId !== undefined,
|
|
744
|
+
true,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
690
747
|
// xAI Grok Build's free tier resets on a rolling window. Use a duration
|
|
691
748
|
// stated by the provider when present; otherwise park for the conservative
|
|
692
749
|
// 24-hour fallback.
|
|
@@ -997,8 +1054,8 @@ export async function recordAccountFailure(
|
|
|
997
1054
|
lastErrorAt: now,
|
|
998
1055
|
})
|
|
999
1056
|
.where(eq(providerAccounts.id, accountId));
|
|
1000
|
-
if (!alreadyCooling
|
|
1001
|
-
await client
|
|
1057
|
+
if (!alreadyCooling) {
|
|
1058
|
+
await writeHealthEvent(client, {
|
|
1002
1059
|
entityKind: "account",
|
|
1003
1060
|
accountId,
|
|
1004
1061
|
// The account's own status, on both sides: a model-scoped throttle
|
|
@@ -1015,7 +1072,10 @@ export async function recordAccountFailure(
|
|
|
1015
1072
|
createdAt: now,
|
|
1016
1073
|
});
|
|
1017
1074
|
}
|
|
1018
|
-
|
|
1075
|
+
// `status` and `cooldownUntil` are untouched on this path — only the
|
|
1076
|
+
// (account, model) pair backed off — so the snapshot is still accurate
|
|
1077
|
+
// and must not be dropped.
|
|
1078
|
+
return { ...classification, routesVisible: false };
|
|
1019
1079
|
}
|
|
1020
1080
|
|
|
1021
1081
|
await client
|
|
@@ -1042,8 +1102,8 @@ export async function recordAccountFailure(
|
|
|
1042
1102
|
})
|
|
1043
1103
|
.where(eq(providerAccounts.id, accountId));
|
|
1044
1104
|
|
|
1045
|
-
|
|
1046
|
-
await client
|
|
1105
|
+
{
|
|
1106
|
+
await writeHealthEvent(client, {
|
|
1047
1107
|
entityKind: "account",
|
|
1048
1108
|
accountId,
|
|
1049
1109
|
fromStatus: fromStatus as "active" | "cooldown" | "disabled",
|
|
@@ -1054,7 +1114,9 @@ export async function recordAccountFailure(
|
|
|
1054
1114
|
createdAt: now,
|
|
1055
1115
|
});
|
|
1056
1116
|
}
|
|
1057
|
-
|
|
1117
|
+
// This path wrote `status` and `cooldownUntil`, both of which the snapshot
|
|
1118
|
+
// projects, so routing has to see it.
|
|
1119
|
+
return { ...classification, routesVisible: true };
|
|
1058
1120
|
};
|
|
1059
1121
|
|
|
1060
1122
|
try {
|
|
@@ -1176,8 +1238,8 @@ export async function recoverAccount(
|
|
|
1176
1238
|
lastErrorAt: null,
|
|
1177
1239
|
})
|
|
1178
1240
|
.where(eq(providerAccounts.id, accountId));
|
|
1179
|
-
|
|
1180
|
-
await client
|
|
1241
|
+
{
|
|
1242
|
+
await writeHealthEvent(client, {
|
|
1181
1243
|
entityKind: "account",
|
|
1182
1244
|
accountId,
|
|
1183
1245
|
fromStatus: fromStatus as "active" | "cooldown" | "disabled",
|
|
@@ -1314,6 +1376,36 @@ export interface AttemptHealthReport {
|
|
|
1314
1376
|
readonly snapshotService?: { invalidate(): unknown };
|
|
1315
1377
|
}
|
|
1316
1378
|
|
|
1379
|
+
/**
|
|
1380
|
+
* Writes one health audit row, off the request path when a buffer is installed.
|
|
1381
|
+
*
|
|
1382
|
+
* `health_events` is append-only and read by nobody in the request path, so it
|
|
1383
|
+
* does not need to land before the request finishes — unlike the
|
|
1384
|
+
* `provider_accounts` UPDATE beside it, which routing reads and which therefore
|
|
1385
|
+
* stays synchronous and transactional.
|
|
1386
|
+
*/
|
|
1387
|
+
async function writeHealthEvent(
|
|
1388
|
+
client: CartethyiaDatabase,
|
|
1389
|
+
row: {
|
|
1390
|
+
readonly entityKind: "account";
|
|
1391
|
+
readonly accountId: string;
|
|
1392
|
+
readonly fromStatus: string;
|
|
1393
|
+
readonly toStatus: string;
|
|
1394
|
+
readonly reason: string;
|
|
1395
|
+
readonly errorCategory: string | null;
|
|
1396
|
+
readonly modelId?: string | null;
|
|
1397
|
+
readonly createdAt: Date;
|
|
1398
|
+
},
|
|
1399
|
+
): Promise<void> {
|
|
1400
|
+
const buffer = healthEventBuffer();
|
|
1401
|
+
if (buffer !== undefined) {
|
|
1402
|
+
buffer.enqueue({ ...row, modelId: row.modelId ?? null });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (typeof client.insert !== "function") return;
|
|
1406
|
+
await client.insert(healthEvents).values(row as never);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1317
1409
|
export async function reportAttemptOutcome(
|
|
1318
1410
|
db: CartethyiaDatabase,
|
|
1319
1411
|
report: AttemptHealthReport,
|
|
@@ -1328,7 +1420,12 @@ export async function reportAttemptOutcome(
|
|
|
1328
1420
|
...report.evidence,
|
|
1329
1421
|
...(report.modelId === undefined ? {} : { modelId: report.modelId }),
|
|
1330
1422
|
}).catch(() => null);
|
|
1331
|
-
|
|
1423
|
+
// Gated on `routesVisible`, not on `mutatesAccount`: a failure that only
|
|
1424
|
+
// cooled one (account, model) pair, or only advanced the streak, changed
|
|
1425
|
+
// nothing routing reads. Invalidating for those rebuilt an 11-SELECT snapshot
|
|
1426
|
+
// on the next request, and under a failure wave each rebuild fed the next
|
|
1427
|
+
// invalidation.
|
|
1428
|
+
if (classification?.routesVisible) await report.snapshotService?.invalidate();
|
|
1332
1429
|
}
|
|
1333
1430
|
|
|
1334
1431
|
export async function listAccountHealthEvents(
|