cartethyia 2.4.4 → 2.4.6
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 +4 -1
- package/README.md +18 -0
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/{BannedPage-TDue2X0p.js → BannedPage-QhvbJB9o.js} +1 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-qaHXuiI5.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BFuQBKez.js → CliToolsPage-DyNXefph.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BhglcqyR.js → CustomizationPage-D6jEU7nL.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CeqROJrT.js → LoginPage-BJlMVN4U.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-CRXSAITY.js → OverviewPage-CQxXbo2g.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-B6LA4ewF.js → UsagePage-BAFaSDQV.js} +14 -14
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
- package/dist/dashboard/assets/app-Cjq7nrwu.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
- package/dist/dashboard/assets/{assert-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js +1 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BWIP_EvW.js} +1 -1
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-BKcgVugF.js} +1 -1
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js +1 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-BA_ni9Ix.js → index-D70pChfn.js} +2 -2
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BArzXirQ.js} +1 -1
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/{routing-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/{select-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js +2 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-D87FZwH0.js} +1 -1
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.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 +123 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
- package/package.json +25 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +227 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +13 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/spawn.ts +101 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/support/version.ts +37 -0
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +49 -0
- package/src/boot/app.ts +3 -1
- package/src/boot/dependencies.ts +36 -13
- package/src/config/providers.ts +27 -1
- package/src/config/spec.ts +11 -0
- package/src/console/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/observability/contracts.ts +54 -22
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +68 -28
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +5 -1
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- package/src/network/outbound-fetch.ts +113 -25
- package/src/network/tls/bun-client-hello.ts +112 -0
- package/src/observability/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/runtime-metrics.ts +7 -0
- package/src/persistence/schema.ts +40 -2
- package/src/protocol/response/codex.ts +9 -75
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +4 -0
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- package/src/providers/integrations/codex/codex.ts +5 -8
- package/src/providers/integrations/factory/factory-oauth.ts +7 -11
- package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +42 -3
- package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
- package/src/providers/integrations/shared/install-id.ts +29 -2
- package/src/providers/operations/client-versions.ts +11 -1
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- package/src/transport/dispatch/attempt-finalize.ts +7 -1
- package/src/transport/dispatch/attempt-loop.ts +9 -7
- package/src/transport/dispatch/empty-stream.ts +17 -26
- package/src/transport/failure-policy.ts +20 -0
- package/src/transport/routing/route-catalog.ts +42 -3
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-vkZBxMVo.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DDkHfwpV.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-cNAD_6uN.js +0 -3
- package/dist/dashboard/assets/ProvidersPage-Cv5tdxlx.js +0 -1
- package/dist/dashboard/assets/ProxyPage-CWvMfzjj.js +0 -7
- package/dist/dashboard/assets/QuotaPage-IYQOOHR4.js +0 -1
- package/dist/dashboard/assets/SetupPage-BPDNo3Ek.js +0 -1
- package/dist/dashboard/assets/SortableList-Bol1ASmI.js +0 -1
- package/dist/dashboard/assets/StudioPage-BE5DJONZ.js +0 -31
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-DTiWhIY-.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -766
- package/scripts/cli/version.ts +0 -66
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
19
19
|
import {
|
|
20
|
+
base64UrlEncode,
|
|
20
21
|
nonEmptyTrimmedString,
|
|
21
22
|
record,
|
|
22
23
|
} from "../../authentication/oauth-flow-store";
|
|
@@ -58,13 +59,6 @@ export function parseQoderOAuthState(value: unknown): QoderOAuthState | undefine
|
|
|
58
59
|
return { userId, machineId };
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function base64Url(buffer: Buffer): string {
|
|
62
|
-
return buffer
|
|
63
|
-
.toString("base64")
|
|
64
|
-
.replace(/=/g, "")
|
|
65
|
-
.replace(/\+/g, "-")
|
|
66
|
-
.replace(/\//g, "_");
|
|
67
|
-
}
|
|
68
62
|
|
|
69
63
|
/**
|
|
70
64
|
* Converts the upstream's expiry hint into a Unix-millisecond timestamp.
|
|
@@ -131,8 +125,8 @@ export class QoderOAuthClient extends OAuthDeviceFlow {
|
|
|
131
125
|
override async startDeviceAuth(
|
|
132
126
|
_context?: OAuthDeviceFlowContext,
|
|
133
127
|
): Promise<OAuthDeviceStartResult> {
|
|
134
|
-
const verifier =
|
|
135
|
-
const challenge =
|
|
128
|
+
const verifier = base64UrlEncode(randomBytes(32));
|
|
129
|
+
const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
|
|
136
130
|
const nonce = randomUUID();
|
|
137
131
|
const machineId = randomUUID();
|
|
138
132
|
const params = new URLSearchParams({
|
|
@@ -53,17 +53,44 @@ function installIdDirectories(): readonly string[] {
|
|
|
53
53
|
* process so a request does not see a different identity than the one before
|
|
54
54
|
* it, which is the least surprising behaviour available when persistence is
|
|
55
55
|
* impossible.
|
|
56
|
+
*
|
|
57
|
+
* Both maps are bounded: the key is derived from the install-id *path*, so an
|
|
58
|
+
* unbounded map would grow with every directory an operator ever pointed
|
|
59
|
+
* `CARTETHYIA_INSTALL_ID_DIR` at. The cap matches the other per-process
|
|
60
|
+
* identity caches (`qoderMachineIds`, `MAX_SESSION_STATES` in antigravity):
|
|
61
|
+
* oldest-first eviction, which for an identity cache only costs a fresh uuid.
|
|
56
62
|
*/
|
|
63
|
+
const MAX_IN_MEMORY_IDS = 1024;
|
|
57
64
|
const inMemoryIds = new Map<string, string>();
|
|
58
65
|
const warnedPaths = new Set<string>();
|
|
59
66
|
|
|
67
|
+
/** Evicts oldest-first once the cap is exceeded. */
|
|
68
|
+
function boundMapInsert<K, V>(map: Map<K, V>, key: K, value: V): void {
|
|
69
|
+
map.set(key, value);
|
|
70
|
+
while (map.size > MAX_IN_MEMORY_IDS) {
|
|
71
|
+
const oldest = map.keys().next();
|
|
72
|
+
if (oldest.done === true) break;
|
|
73
|
+
map.delete(oldest.value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Evicts oldest-first once the cap is exceeded. */
|
|
78
|
+
function boundSetInsert(set: Set<string>, value: string): void {
|
|
79
|
+
set.add(value);
|
|
80
|
+
while (set.size > MAX_IN_MEMORY_IDS) {
|
|
81
|
+
const oldest = set.values().next();
|
|
82
|
+
if (oldest.done === true) break;
|
|
83
|
+
set.delete(oldest.value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
60
87
|
function fallbackId(key: string, path: string, reason: unknown): string {
|
|
61
88
|
const existing = inMemoryIds.get(key);
|
|
62
89
|
if (existing !== undefined) return existing;
|
|
63
90
|
const id = randomUUID();
|
|
64
|
-
inMemoryIds
|
|
91
|
+
boundMapInsert(inMemoryIds, key, id);
|
|
65
92
|
if (!warnedPaths.has(key)) {
|
|
66
|
-
warnedPaths
|
|
93
|
+
boundSetInsert(warnedPaths, key);
|
|
67
94
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
68
95
|
log.warn(
|
|
69
96
|
`[install-id] could not persist "${path}" (${message}); using an id that ` +
|
|
@@ -525,9 +525,19 @@ export const getKimiCliVersion = accessor("kimiCli").get;
|
|
|
525
525
|
|
|
526
526
|
export const getCodeBuddyVersion = accessor("codebuddy").get;
|
|
527
527
|
export const getCodeBuddyCnVersion = accessor("codebuddyCn").get;
|
|
528
|
+
/**
|
|
529
|
+
* The CodeBuddy User-Agent.
|
|
530
|
+
*
|
|
531
|
+
* Both identities share the CLI *package* version, which is why `codebuddy`
|
|
532
|
+
* (the npm `@tencent-ai/codebuddy-code` pin, `2.168.2` — the value both
|
|
533
|
+
* references use for CLI) is the version for each. `codebuddyCn` is the CN
|
|
534
|
+
* *desktop* build (`4.12.2.…`), and using it here produced
|
|
535
|
+
* `CLI/4.12.2.41207131 CodeBuddy/4.12.2.41207131` — a CLI claiming a desktop
|
|
536
|
+
* version, which no official client emits.
|
|
537
|
+
*/
|
|
528
538
|
export function buildCodeBuddyUserAgent(
|
|
529
539
|
identity: "IDE" | "CLI",
|
|
530
|
-
version =
|
|
540
|
+
version = getCodeBuddyVersion(),
|
|
531
541
|
): string {
|
|
532
542
|
return `${identity}/${version} CodeBuddy/${version}`;
|
|
533
543
|
}
|
|
@@ -14,10 +14,14 @@ import { cachedRead } from "../../console/shared/read-through-cache";
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Provider entries retained before the least-recently-used catalog is evicted.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
*
|
|
18
|
+
* Must clear the bundled provider count — there are **73** of them — because a
|
|
19
|
+
* cap below it thrashes: a catalog rebuild evicts a provider it is about to
|
|
20
|
+
* need again, so `bundledModelCatalog()` re-imports a module it already had.
|
|
21
|
+
* Sized above that count with room for BYOK rows. One entry is a model list, so
|
|
22
|
+
* the bound still matters; it just has to be the right one.
|
|
19
23
|
*/
|
|
20
|
-
export const MODEL_CATALOG_CACHE_MAX_ENTRIES =
|
|
24
|
+
export const MODEL_CATALOG_CACHE_MAX_ENTRIES = 128;
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Cross-instance tier lifetime. Static provider catalogs change only on a
|
|
@@ -82,3 +86,14 @@ export function getCachedModels(
|
|
|
82
86
|
export function resetModelCatalogCacheForTesting(): void {
|
|
83
87
|
catalogCache.clear();
|
|
84
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Live entry count, for the `/metrics` gauge.
|
|
92
|
+
*
|
|
93
|
+
* Exported so a rebuild that saturates the cap is visible from outside the
|
|
94
|
+
* process: before this, every in-process cache except the quota tier was
|
|
95
|
+
* invisible, and growth could only be found with a heap dump.
|
|
96
|
+
*/
|
|
97
|
+
export function modelCatalogCacheSize(): number {
|
|
98
|
+
return catalogCache.size;
|
|
99
|
+
}
|
|
@@ -20,6 +20,57 @@ import { GATEWAY_USER_AGENT } from "./gateway-user-agent";
|
|
|
20
20
|
import { isIP } from "node:net";
|
|
21
21
|
import { isAddressAllowed } from "../../network/ssrf";
|
|
22
22
|
import type { SsrfPolicy } from "../../config/network";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Revision of the compiled provider catalog, and the last one materialized.
|
|
26
|
+
*
|
|
27
|
+
* The snapshot builder re-seeds on **every** rebuild, and a re-seed is a
|
|
28
|
+
* `DELETE` per provider plus a full `INSERT … ON CONFLICT DO UPDATE` of the
|
|
29
|
+
* compiled catalog. That is correct work when the catalog changed and pure
|
|
30
|
+
* waste when it did not — and the invalidation that triggers it is a timer
|
|
31
|
+
* (`account-health-sweep`), so the waste lands on a proxy request.
|
|
32
|
+
*
|
|
33
|
+
* Only the paths that can actually change the compiled catalog bump this: a
|
|
34
|
+
* provider registered, edited, or retired. A cooldown recovery changes account
|
|
35
|
+
* health, not the catalog, so it must not.
|
|
36
|
+
*/
|
|
37
|
+
let catalogRevision = 0;
|
|
38
|
+
let lastSeededCatalogRevision = -1;
|
|
39
|
+
|
|
40
|
+
/** Current catalog revision. */
|
|
41
|
+
export function currentCatalogRevision(): number {
|
|
42
|
+
return catalogRevision;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Bumps the revision after a change to the compiled catalog. */
|
|
46
|
+
export function bumpCatalogRevision(): number {
|
|
47
|
+
catalogRevision += 1;
|
|
48
|
+
return catalogRevision;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Forces the next re-seed to run even when the revision is unchanged.
|
|
53
|
+
*
|
|
54
|
+
* For the boot path and for tests: the compiled catalog has to be materialized
|
|
55
|
+
* once for the current process regardless of what an earlier call already
|
|
56
|
+
* applied, because a fresh database or a fresh registry is not a revision
|
|
57
|
+
* change.
|
|
58
|
+
*/
|
|
59
|
+
export function invalidateCatalogSeed(): void {
|
|
60
|
+
lastSeededCatalogRevision = -1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Records that the caller has just materialized the current catalog itself, so
|
|
65
|
+
* the next `refreshActiveProviderCatalogs` is a no-op.
|
|
66
|
+
*
|
|
67
|
+
* The boot path seeds directly rather than through the gate — it must always
|
|
68
|
+
* run, against a database and a registry that are both fresh — and without this
|
|
69
|
+
* the first snapshot build would pay for a second identical seed.
|
|
70
|
+
*/
|
|
71
|
+
export function markCatalogSeeded(): void {
|
|
72
|
+
lastSeededCatalogRevision = catalogRevision;
|
|
73
|
+
}
|
|
23
74
|
/**
|
|
24
75
|
* Idempotently materializes every shipped provider in the database catalog.
|
|
25
76
|
* The canonical provider definition supplies identity and routing defaults;
|
|
@@ -60,6 +111,9 @@ export async function seedBundledProviders(db: CartethyiaDatabase): Promise<void
|
|
|
60
111
|
),
|
|
61
112
|
},
|
|
62
113
|
});
|
|
114
|
+
// The compiled catalog is derived from the registry, and this is the path
|
|
115
|
+
// that reconciles the bundle's own rows into it.
|
|
116
|
+
bumpCatalogRevision();
|
|
63
117
|
}
|
|
64
118
|
|
|
65
119
|
/**
|
|
@@ -94,6 +148,9 @@ export async function retireUnbundledProviders(db: CartethyiaDatabase): Promise<
|
|
|
94
148
|
.delete(providers)
|
|
95
149
|
.where(and(isNull(providers.tenantId), notInArray(providers.id, bundledIds)))
|
|
96
150
|
.returning({ id: providers.id });
|
|
151
|
+
// Deleting a provider removes it from the compiled catalog, so the next
|
|
152
|
+
// re-seed must run even though no revision bumped on this path otherwise.
|
|
153
|
+
if (retired.length > 0) bumpCatalogRevision();
|
|
97
154
|
return retired.length;
|
|
98
155
|
}
|
|
99
156
|
|
|
@@ -172,13 +229,22 @@ export async function bundledModelCatalog(
|
|
|
172
229
|
* boot. Seeding is no longer account-gated, so this is now simply the same walk
|
|
173
230
|
* as at boot; it exists because an account makes the rows routable and the
|
|
174
231
|
* snapshot must reflect that without waiting for a restart.
|
|
232
|
+
*
|
|
233
|
+
* Gated on {@link currentCatalogRevision}: this is the seeder the snapshot
|
|
234
|
+
* builder calls on every rebuild, and the invalidation that triggers it is a
|
|
235
|
+
* 30 s timer, so an ungated re-seed could be pushed onto the next proxy
|
|
236
|
+
* request for a catalog that had not changed. The paths that do change the
|
|
237
|
+
* catalog bump the revision; everything else is a no-op here.
|
|
175
238
|
*/
|
|
176
239
|
export async function refreshActiveProviderCatalogs(
|
|
177
240
|
db: CartethyiaDatabase,
|
|
178
241
|
registry: ProviderRegistry,
|
|
179
242
|
): Promise<void> {
|
|
243
|
+
const revision = currentCatalogRevision();
|
|
244
|
+
if (revision === lastSeededCatalogRevision) return;
|
|
180
245
|
const catalog = await bundledModelCatalog(registry);
|
|
181
246
|
await seedBundledModels(db, catalog.modelsByProvider);
|
|
247
|
+
lastSeededCatalogRevision = revision;
|
|
182
248
|
}
|
|
183
249
|
|
|
184
250
|
|
|
@@ -217,6 +283,7 @@ export async function registerByokProviders(
|
|
|
217
283
|
.from(providers)
|
|
218
284
|
.where(and(isNotNull(providers.baseUrl), eq(providers.enabled, true)));
|
|
219
285
|
const hosts = new Map<string, ByokUpstreamHost>();
|
|
286
|
+
let registered = false;
|
|
220
287
|
for (const row of rows) {
|
|
221
288
|
if (!row.baseUrl) continue;
|
|
222
289
|
const baseUrl = row.baseUrl;
|
|
@@ -307,7 +374,13 @@ export async function registerByokProviders(
|
|
|
307
374
|
}),
|
|
308
375
|
});
|
|
309
376
|
byokRegistrationFingerprints.set(providerId, fingerprint);
|
|
377
|
+
registered = true;
|
|
310
378
|
}
|
|
379
|
+
// A changed BYOK registration changes what the compiled catalog contains, so
|
|
380
|
+
// the next re-seed must run. An unchanged one deliberately does not: this
|
|
381
|
+
// function is called on every console provider mutation, most of which do not
|
|
382
|
+
// alter the registration at all.
|
|
383
|
+
if (registered) bumpCatalogRevision();
|
|
311
384
|
return hosts;
|
|
312
385
|
}
|
|
313
386
|
|
|
@@ -341,6 +414,9 @@ export async function syncByokProvider(
|
|
|
341
414
|
if (host === undefined && fingerprintBefore !== undefined) {
|
|
342
415
|
registry.unregister(normalized);
|
|
343
416
|
byokRegistrationFingerprints.delete(normalized);
|
|
417
|
+
// The provider left the registry, so the compiled catalog no longer
|
|
418
|
+
// contains it: the next re-seed must run to prune its rows.
|
|
419
|
+
bumpCatalogRevision();
|
|
344
420
|
}
|
|
345
421
|
return host;
|
|
346
422
|
}
|
|
@@ -249,7 +249,13 @@ export function captureProviderExchange(
|
|
|
249
249
|
method: init?.method ?? "GET",
|
|
250
250
|
url,
|
|
251
251
|
...(headers === undefined ? {} : { headers }),
|
|
252
|
-
|
|
252
|
+
// Capped the same way the response is. A capture is a diagnostic, and an
|
|
253
|
+
// unbounded translated request body per attempt — up to `maxAttempts` of
|
|
254
|
+
// them, all alive until the loop returns — costs more than it can tell
|
|
255
|
+
// an operator.
|
|
256
|
+
...(rawBody === null
|
|
257
|
+
? {}
|
|
258
|
+
: { body: parseCapturedBody(rawBody.slice(0, PROVIDER_CAPTURE_MAX_BYTES)) }),
|
|
253
259
|
};
|
|
254
260
|
const response = await inner(input, init);
|
|
255
261
|
sink.response =
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
12
12
|
import type { ResolvedCredential } from "../../providers/provider-contracts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
classifyTerminalOutcome,
|
|
15
|
+
fallbackRetryDelayMs,
|
|
16
|
+
isRetryableFailure,
|
|
17
|
+
MIN_RETRY_BUDGET_MS,
|
|
18
|
+
sleep,
|
|
19
|
+
} from "../failure-policy";
|
|
14
20
|
import { resolveRouteMaxAttempts } from "../../config/timeouts";
|
|
15
21
|
import type { OAuthTokenRefresher } from "../../providers/authentication/oauth-refresh-service";
|
|
16
22
|
import type { OAuthRefreshService } from "../../providers/authentication/oauth-refresh-service";
|
|
@@ -178,12 +184,8 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
178
184
|
const perMemberCap =
|
|
179
185
|
memberCount > 1 ? Math.max(1, Math.ceil(maxAttempts / memberCount)) : maxAttempts;
|
|
180
186
|
const attemptsByMember = new Map<string, number>();
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// slot, prepare), so it needs at least that much budget on top of whatever
|
|
184
|
-
// the upstream itself takes. Below this, the attempt would be killed by the
|
|
185
|
-
// deadline before it could return anything the client could use.
|
|
186
|
-
const MIN_RETRY_BUDGET_MS = 15_000;
|
|
187
|
+
// The budget itself is owned by failure-policy, so this loop, the
|
|
188
|
+
// empty-stream replay, and the Codex retry budget cannot drift apart.
|
|
187
189
|
let index = 0;
|
|
188
190
|
let attempts = 0;
|
|
189
191
|
while (index < candidates.length && attempts < maxAttempts) {
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* rather than surfacing an empty body to the client.
|
|
19
19
|
*/
|
|
20
20
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
21
|
+
import {
|
|
22
|
+
fallbackRetryDelayMs,
|
|
23
|
+
MIN_RETRY_BUDGET_MS,
|
|
24
|
+
sleep as abortableSleep,
|
|
25
|
+
} from "../failure-policy";
|
|
21
26
|
|
|
22
27
|
// ─── Public types ──────────────────────────────────────────────────────────
|
|
23
28
|
|
|
@@ -38,9 +43,17 @@ export type EmptyStreamFetch = (
|
|
|
38
43
|
init: { headers: Record<string, string>; body: string; signal: AbortSignal },
|
|
39
44
|
) => Promise<Response>;
|
|
40
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Retry-delay policy.
|
|
48
|
+
*
|
|
49
|
+
* Delays come from `fallbackRetryDelayMs` (capped exponential with full
|
|
50
|
+
* jitter), whose base and cap are operator-tunable. There is deliberately no
|
|
51
|
+
* per-caller base: the local formula this replaced was uncapped and
|
|
52
|
+
* unjittered, so a provider returning 200-with-no-events could drive an
|
|
53
|
+
* unbounded wait, and concurrent clients could resynchronize their retries.
|
|
54
|
+
*/
|
|
41
55
|
export interface EmptyStreamPolicy {
|
|
42
56
|
readonly maxEmptyReplays?: number;
|
|
43
|
-
readonly baseDelayMs?: number;
|
|
44
57
|
readonly firstEventTimeoutMs?: number;
|
|
45
58
|
readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
46
59
|
}
|
|
@@ -55,9 +68,8 @@ export interface EmptyStreamResult {
|
|
|
55
68
|
// ─── Defaults ──────────────────────────────────────────────────────────────
|
|
56
69
|
|
|
57
70
|
const DEFAULT_MAX_REPLAYS = 2;
|
|
58
|
-
const DEFAULT_BASE_DELAY_MS = 500;
|
|
59
71
|
const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 200_000; // 200s, matches CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS default
|
|
60
|
-
|
|
72
|
+
|
|
61
73
|
|
|
62
74
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
63
75
|
|
|
@@ -66,27 +78,7 @@ export function isEmptyStream(tally: StreamTally): boolean {
|
|
|
66
78
|
return tally.dataEvents === 0 || (!tally.meaningful && !tally.thoughtOnly);
|
|
67
79
|
}
|
|
68
80
|
|
|
69
|
-
function defaultSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
if (signal.aborted) {
|
|
72
|
-
reject(new GatewayError("transport_closed", 499, "Aborted"));
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const timer = setTimeout(() => {
|
|
76
|
-
signal.removeEventListener("abort", onAbort);
|
|
77
|
-
resolve();
|
|
78
|
-
}, ms);
|
|
79
|
-
const onAbort = () => {
|
|
80
|
-
clearTimeout(timer);
|
|
81
|
-
reject(new GatewayError("transport_closed", 499, "Aborted"));
|
|
82
|
-
};
|
|
83
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
81
|
|
|
87
|
-
function backoffMs(attempt: number, base: number): number {
|
|
88
|
-
return base * 2 ** attempt;
|
|
89
|
-
}
|
|
90
82
|
|
|
91
83
|
// ─── Main function ─────────────────────────────────────────────────────────
|
|
92
84
|
|
|
@@ -109,9 +101,8 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
109
101
|
},
|
|
110
102
|
): Promise<EmptyStreamResult> {
|
|
111
103
|
const maxReplays = deps.policy?.maxEmptyReplays ?? DEFAULT_MAX_REPLAYS;
|
|
112
|
-
const baseDelay = deps.policy?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
113
104
|
const firstEventTimeout = deps.policy?.firstEventTimeoutMs ?? DEFAULT_FIRST_EVENT_TIMEOUT_MS;
|
|
114
|
-
const sleep = deps.policy?.sleep ??
|
|
105
|
+
const sleep = deps.policy?.sleep ?? abortableSleep;
|
|
115
106
|
|
|
116
107
|
if (endpoints.length === 0) {
|
|
117
108
|
throw new GatewayError("platform_unavailable", 502, "No endpoints available", {}, "upstream");
|
|
@@ -173,7 +164,7 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
173
164
|
|
|
174
165
|
// Empty — replay if we haven't exhausted this endpoint's budget
|
|
175
166
|
if (replay < replaysAllowed) {
|
|
176
|
-
const delay =
|
|
167
|
+
const delay = fallbackRetryDelayMs(replay);
|
|
177
168
|
try {
|
|
178
169
|
await sleep(delay, deps.signal);
|
|
179
170
|
} catch (error) {
|
|
@@ -884,10 +884,30 @@ export function isRetryableFailure(error: unknown): boolean {
|
|
|
884
884
|
return classifyUpstreamFailure(error).retryable;
|
|
885
885
|
}
|
|
886
886
|
|
|
887
|
+
/**
|
|
888
|
+
* Minimum wall-clock budget a follow-on attempt needs to be worth starting.
|
|
889
|
+
*
|
|
890
|
+
* A retry repeats the whole pre-dispatch phase — credential resolution,
|
|
891
|
+
* admission, pool slot, request preparation — so it needs at least this much
|
|
892
|
+
* headroom on top of whatever the upstream itself takes. Below this, the
|
|
893
|
+
* attempt is killed by the deadline before it can return anything usable, and
|
|
894
|
+
* starting it replaces the first attempt's real error with a second one's
|
|
895
|
+
* timeout.
|
|
896
|
+
*
|
|
897
|
+
* Owned here so the attempt loop, the empty-stream replay, and the Codex
|
|
898
|
+
* retry budget cannot drift apart; each previously declared its own copy of
|
|
899
|
+
* the constant.
|
|
900
|
+
*/
|
|
901
|
+
export const MIN_RETRY_BUDGET_MS = 15_000;
|
|
902
|
+
|
|
887
903
|
/**
|
|
888
904
|
* Capped exponential backoff with full jitter for a zero-based attempt index.
|
|
889
905
|
* The base and cap are operator-tunable (`CARTETHYIA_FALLBACK_RETRY_BASE_MS`,
|
|
890
906
|
* `CARTETHYIA_FALLBACK_RETRY_CAP_MS`) and default to 100ms / 2000ms.
|
|
907
|
+
*
|
|
908
|
+
* Used for every retry delay, including the empty-stream replay: jitter keeps
|
|
909
|
+
* concurrent clients from resynchronizing, and the cap keeps a provider that
|
|
910
|
+
* returns 200-with-no-events from driving an unbounded wait.
|
|
891
911
|
*/
|
|
892
912
|
export function fallbackRetryDelayMs(attemptIndex: number): number {
|
|
893
913
|
const exp = Math.min(resolveFallbackRetryCapMs(), resolveFallbackRetryBaseMs() * 2 ** attemptIndex);
|
|
@@ -16,8 +16,6 @@ import {
|
|
|
16
16
|
cliToolSettings,
|
|
17
17
|
} from "../../persistence/schema";
|
|
18
18
|
import { DEFAULT_PROVIDER_MAX_INFLIGHT } from "../../persistence/schema";
|
|
19
|
-
import "../../providers/integrations/claude/claude-oauth";
|
|
20
|
-
import "../../providers/integrations/codex/codex-oauth";
|
|
21
19
|
import { resolveTenantOverride } from "../../persistence/tenant-scope";
|
|
22
20
|
import {
|
|
23
21
|
type RouteCandidate,
|
|
@@ -149,6 +147,42 @@ export function buildCapabilityProfile(row: {
|
|
|
149
147
|
};
|
|
150
148
|
}
|
|
151
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The capability profile for one model row, memoized across rebuilds.
|
|
152
|
+
*
|
|
153
|
+
* Bounded LRU keyed by `providerId/modelId`: the value is a pure function of
|
|
154
|
+
* the row's metadata, so two rebuilds that see the same row get the same object
|
|
155
|
+
* instead of two identical ones. The bound keeps a long-lived process from
|
|
156
|
+
* pinning one profile per row it has ever seen — a model deleted from the
|
|
157
|
+
* catalog must not leave its profile behind forever.
|
|
158
|
+
*/
|
|
159
|
+
const CAPABILITY_PROFILE_CACHE_MAX_ENTRIES = 4096;
|
|
160
|
+
const capabilityProfileCache = new Map<string, Record<string, boolean>>();
|
|
161
|
+
|
|
162
|
+
function capabilityProfileFor(row: {
|
|
163
|
+
modalities: unknown;
|
|
164
|
+
reasoning: boolean;
|
|
165
|
+
toolCall: boolean;
|
|
166
|
+
providerId: string;
|
|
167
|
+
modelId: string;
|
|
168
|
+
}): Record<string, boolean> {
|
|
169
|
+
const key = `${row.providerId}/${row.modelId}`;
|
|
170
|
+
const cached = capabilityProfileCache.get(key);
|
|
171
|
+
if (cached !== undefined) return cached;
|
|
172
|
+
const profile = buildCapabilityProfile(row);
|
|
173
|
+
if (capabilityProfileCache.size >= CAPABILITY_PROFILE_CACHE_MAX_ENTRIES) {
|
|
174
|
+
const oldest = capabilityProfileCache.keys().next().value;
|
|
175
|
+
if (oldest !== undefined) capabilityProfileCache.delete(oldest);
|
|
176
|
+
}
|
|
177
|
+
capabilityProfileCache.set(key, profile);
|
|
178
|
+
return profile;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Live memo size, for tests and the metrics gauge. */
|
|
182
|
+
export function capabilityProfileCacheSize(): number {
|
|
183
|
+
return capabilityProfileCache.size;
|
|
184
|
+
}
|
|
185
|
+
|
|
152
186
|
/** Resolves route identity only for bundled adapters without a native User-Agent builder. */
|
|
153
187
|
export function resolveRouteUserAgent(
|
|
154
188
|
providerId: string,
|
|
@@ -527,7 +561,12 @@ class RouteCatalogRepository {
|
|
|
527
561
|
const providerTenantId = providerTenantById.get(model.providerId);
|
|
528
562
|
if (providerTenantId === undefined) continue;
|
|
529
563
|
const accounts = accountsByProvider.get(model.providerId) ?? [];
|
|
530
|
-
|
|
564
|
+
// One profile per model row, memoized across snapshot rebuilds. The
|
|
565
|
+
// profile is derived purely from the row's metadata, is never mutated
|
|
566
|
+
// (grep finds no write to `capability_profile`), and is shared by every
|
|
567
|
+
// candidate this row fans out to — so a rebuild no longer constructs a
|
|
568
|
+
// fresh object per model on top of the fan-out it already had.
|
|
569
|
+
const capabilityProfile = capabilityProfileFor(model);
|
|
531
570
|
if (accounts.length === 0) {
|
|
532
571
|
const providerRequiresAccount = requiresAccountById.get(model.providerId) ?? true;
|
|
533
572
|
if (!providerRequiresAccount) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CanonicalEvent, CanonicalStopReason } from "../../../protocol/canonical-model";
|
|
2
|
+
import { jsonrepair } from "jsonrepair";
|
|
2
3
|
import { reasoningSummaryIdentity, reasoningSummarySeparator, type ReasoningSummaryIdentity } from "../reasoning-summary";
|
|
3
4
|
import { eventText } from "../adapters";
|
|
4
5
|
import { isRecord } from "../../../protocol/codecs";
|
|
@@ -20,50 +21,36 @@ import type { ChatEncodeOptions, ChunkBase, JsonObject } from "./parse";
|
|
|
20
21
|
* a raw control character inside a string, a trailing comma, an unquoted key.
|
|
21
22
|
* The client parses the assembled string with `JSON.parse` and, on failure,
|
|
22
23
|
* reports the call as `Tool <name> not found` with the parser's message
|
|
23
|
-
* (`Unexpected trailing characters`, `Invalid number`).
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* (`Unexpected trailing characters`, `Invalid number`).
|
|
25
|
+
*
|
|
26
|
+
* Repairing is delegated to `jsonrepair`, which handles the whole family of
|
|
27
|
+
* near-JSON shapes (truncated objects, unquoted keys, single quotes, trailing
|
|
28
|
+
* commas) instead of the two cases a hand-rolled scanner covered.
|
|
29
|
+
*
|
|
30
|
+
* The policy around it is unchanged and is the load-bearing part: the result
|
|
31
|
+
* is parsed only to validate, and anything that does not parse *to an object*
|
|
32
|
+
* is discarded in favour of the original text — a half-repaired call is worse
|
|
33
|
+
* than the original error. The object check matters because `jsonrepair` will
|
|
34
|
+
* happily turn arbitrary prose into a quoted JSON string, which parses but is
|
|
35
|
+
* not a tool-argument object.
|
|
27
36
|
*/
|
|
28
37
|
export function repairToolArguments(raw: string): string {
|
|
29
38
|
if (raw.length === 0) return "{}";
|
|
30
39
|
const repaired = repairToolArgumentText(raw);
|
|
31
40
|
try {
|
|
32
|
-
JSON.parse(repaired);
|
|
41
|
+
const parsed: unknown = JSON.parse(repaired);
|
|
42
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return raw;
|
|
33
43
|
return repaired;
|
|
34
44
|
} catch {
|
|
35
45
|
return raw;
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
/** Repairs near-JSON text into strict JSON. Throws when the text is unrepairable. */
|
|
39
50
|
function repairToolArgumentText(json: string): string {
|
|
40
|
-
|
|
41
|
-
let last = 0;
|
|
42
|
-
let inString = false;
|
|
43
|
-
for (let i = 0; i < json.length; i++) {
|
|
44
|
-
const code = json.charCodeAt(i);
|
|
45
|
-
if (!inString) {
|
|
46
|
-
if (code === 0x22) inString = true;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (code === 0x22) {
|
|
50
|
-
inString = false;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (code === 0x5c) {
|
|
54
|
-
i++;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (code < 0x20) {
|
|
58
|
-
const escape =
|
|
59
|
-
code === 0x0a ? "\\n" : code === 0x0d ? "\\r" : code === 0x09 ? "\\t" : `\\u${code.toString(16).padStart(4, "0")}`;
|
|
60
|
-
parts.push(json.slice(last, i), escape);
|
|
61
|
-
last = i + 1;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
const escaped = parts.length === 0 ? json : parts.join("") + json.slice(last);
|
|
65
|
-
return escaped.replace(/,(\s*[}\]])/g, "$1");
|
|
51
|
+
return jsonrepair(json);
|
|
66
52
|
}
|
|
53
|
+
|
|
67
54
|
export function bytesToString(value: string | Uint8Array): string {
|
|
68
55
|
return typeof value === "string" ? value : new TextDecoder().decode(value);
|
|
69
56
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ReasoningSummaryIdentity,
|
|
7
7
|
} from "../reasoning-summary";
|
|
8
8
|
import {
|
|
9
|
+
argumentsFrom,
|
|
9
10
|
createToolCallTracker,
|
|
10
11
|
resetToolCallTracker,
|
|
11
12
|
toolIdentityKey,
|
|
@@ -96,7 +97,7 @@ export class MessagesStreamEncoder extends SurfaceStreamEncoder<CanonicalEvent,
|
|
|
96
97
|
const index = existing ?? this.nextIndex++;
|
|
97
98
|
this.tools.indexById.set(callId, index);
|
|
98
99
|
const sent = this.emittedArguments.get(callId) ?? 0;
|
|
99
|
-
const pending = entry
|
|
100
|
+
const pending = argumentsFrom(entry, sent);
|
|
100
101
|
if (!this.openedToolCalls.has(callId)) {
|
|
101
102
|
this.openedToolCalls.add(callId);
|
|
102
103
|
out.push(...this.closeOpenBlock());
|
|
@@ -120,7 +121,7 @@ export class MessagesStreamEncoder extends SurfaceStreamEncoder<CanonicalEvent,
|
|
|
120
121
|
index,
|
|
121
122
|
delta: { type: "input_json_delta", partial_json: pending },
|
|
122
123
|
});
|
|
123
|
-
this.emittedArguments.set(callId,
|
|
124
|
+
this.emittedArguments.set(callId, sent + pending.length);
|
|
124
125
|
}
|
|
125
126
|
return out;
|
|
126
127
|
}
|