cartethyia 2.7.9 → 2.8.1
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 +6 -5
- package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
- package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
- package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
- package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
- package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
- package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
- package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
- package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
- package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
- package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
- package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
- package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
- package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
- package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
- package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
- package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
- package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
- package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
- package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
- package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
- package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
- package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
- package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
- package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
- package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
- package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
- package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
- package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
- package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
- package/dist/dashboard/assets/providers/sensenova.png +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
- package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
- package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
- package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
- package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
- package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
- package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
- package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
- package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
- package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
- package/package.json +1 -1
- package/src/boot/app.ts +11 -14
- package/src/boot/dependencies.ts +5 -7
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/providers.ts +6 -5
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +28 -8
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +40 -28
- package/src/console/auth/account-reset.ts +7 -9
- package/src/console/backup/contracts.ts +4 -0
- package/src/console/backup/export-endpoint.ts +7 -5
- package/src/console/backup/nine-router.ts +71 -10
- package/src/console/backup/service.ts +8 -1
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +1 -1
- package/src/console/domain-registration.ts +7 -4
- package/src/console/inferhub/client.ts +1 -1
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +7 -13
- package/src/console/observability/live.ts +2 -2
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/contracts.ts +9 -3
- package/src/console/providers/catalog/model-operations.ts +2 -3
- package/src/console/providers/catalog/provider-operations.ts +1 -1
- package/src/console/providers/catalog/routes.ts +3 -2
- package/src/console/providers/catalog/store.ts +1914 -1902
- package/src/console/providers/detail/routes.ts +1 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/providers/oauth/callback-listener.ts +30 -11
- package/src/console/providers/oauth/device-egress.ts +123 -0
- package/src/console/providers/oauth/routes.ts +123 -16
- package/src/console/quota/account-quota-global.ts +14 -24
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +11 -23
- package/src/console/quota/account-test-egress.ts +2 -2
- package/src/console/quota/daily-growth-job.ts +1 -1
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/contracts.ts +1 -1
- package/src/console/routing/pools/operations.ts +32 -47
- package/src/console/routing/pools/probe-verdict.ts +2 -2
- package/src/console/routing/pools/relay-deploy.ts +1 -1
- package/src/console/routing/pools/store.ts +4 -4
- package/src/console/shared/errors.ts +33 -6
- package/src/main.ts +2 -3
- package/src/network/client.ts +67 -0
- package/src/network/egress/bidi-session.ts +322 -0
- package/src/network/egress/dial.ts +201 -0
- package/src/network/egress/factory.ts +191 -0
- package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
- package/src/network/egress/tls-profile.ts +123 -0
- package/src/network/egress/ws.ts +175 -0
- package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
- package/src/network/{types.ts → pool/agent-config.ts} +1 -1
- package/src/network/pool/agent.ts +2 -2
- package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
- package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
- package/src/network/pool/resolver.ts +10 -849
- package/src/network/pool/selector.ts +184 -41
- package/src/network/ssrf.ts +7 -7
- package/src/network/{retry.ts → util/retry.ts} +1 -1
- package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
- package/src/observability/event-log.ts +7 -361
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +16 -26
- package/src/observability/telemetry-reconciliation.ts +9 -37
- package/src/observability/telemetry-row.ts +125 -0
- package/src/persistence/db-mode.ts +4 -1
- package/src/persistence/dialect.ts +18 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +17 -0
- package/src/persistence/ttl-cache.ts +21 -3
- package/src/protocol/codecs.ts +59 -2
- package/src/protocol/dsml.ts +34 -86
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/request/chat.ts +10 -10
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +34 -22
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/messages.ts +34 -7
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +15 -0
- package/src/protocol/upstream/send-claude-messages.ts +2 -2
- package/src/providers/adapters/compatible-adapter.ts +76 -12
- package/src/providers/authentication/oauth-client.ts +40 -6
- package/src/providers/authentication/oauth-device-flow.ts +105 -11
- package/src/providers/authentication/oauth-flow-store.ts +82 -1
- package/src/providers/authentication/oauth-refresh-service.ts +766 -758
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +137 -36
- package/src/providers/discovery/discovery-types.ts +7 -0
- package/src/providers/discovery/probe-phases.ts +20 -6
- package/src/providers/discovery/probing-service.ts +22 -2
- package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
- package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
- package/src/providers/integrations/buddy/account.ts +969 -0
- package/src/providers/integrations/buddy/capabilities.ts +12 -12
- package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
- package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
- package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
- package/src/providers/integrations/buddy/headers.ts +311 -0
- package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
- package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline-oauth.ts +10 -5
- package/src/providers/integrations/cline/cline.ts +5 -6
- package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
- package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
- package/src/providers/integrations/clixad/provider.ts +145 -0
- package/src/providers/integrations/codex/codex-oauth.ts +8 -5
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
- package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
- package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
- package/src/providers/integrations/cursor/cursor-request.ts +26 -12
- package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
- package/src/providers/integrations/cursor/cursor.ts +574 -110
- package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
- package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
- package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
- package/src/providers/integrations/deepai/deepai-common.ts +145 -0
- package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
- package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
- package/src/providers/integrations/deepai/provider.ts +42 -0
- package/src/providers/integrations/devin/devin-auth.ts +7 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
- package/src/providers/integrations/factory/factory-oauth.ts +33 -51
- package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
- package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
- package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
- package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
- package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
- package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
- package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
- package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
- package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
- package/src/providers/integrations/freebuff/freebuff.ts +682 -0
- package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
- package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
- package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
- package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
- package/src/providers/integrations/freepi/freepi-session.ts +158 -0
- package/src/providers/integrations/freepi/freepi.ts +80 -54
- package/src/providers/integrations/github/github-discovery.ts +30 -13
- package/src/providers/integrations/github/github-oauth.ts +217 -44
- package/src/providers/integrations/github/github-quota.ts +22 -5
- package/src/providers/integrations/github/github.ts +40 -32
- package/src/providers/integrations/grok/grok-oauth.ts +52 -49
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
- package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
- package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
- package/src/providers/integrations/kilo/kilo.ts +1 -1
- package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
- package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
- package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
- package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/muse/muse-oauth.ts +5 -5
- package/src/providers/integrations/ollamacloud/index.ts +2 -0
- package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
- package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/openrouter/provider.ts +1 -1
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-protocol.ts +0 -6
- package/src/providers/integrations/perch/perch-quota.ts +2 -16
- package/src/providers/integrations/perch/perch.ts +6 -16
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
- package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
- package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
- package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
- package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
- package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
- package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
- package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
- package/src/providers/integrations/qoder/qoder-types.ts +9 -0
- package/src/providers/integrations/qoder/qoder.ts +3 -26
- package/src/providers/integrations/sensenova/provider.ts +144 -0
- package/src/providers/integrations/trae/trae-protocol.ts +9 -5
- package/src/providers/integrations/trae/trae-quota.ts +21 -29
- package/src/providers/integrations/trae/trae-realm.ts +38 -0
- package/src/providers/integrations/trae/trae.ts +7 -24
- package/src/providers/integrations/vercel/provider.ts +117 -0
- package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
- package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
- package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
- package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
- package/src/providers/integrations/zro/zro.ts +205 -0
- package/src/providers/operations/account-error-classification.ts +199 -307
- package/src/providers/operations/account-health-service.ts +82 -1
- package/src/providers/operations/account-refresh-status.ts +36 -37
- package/src/providers/operations/account-tier.ts +15 -16
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +100 -81
- package/src/providers/operations/cooldown-rules.ts +11 -16
- package/src/providers/operations/oauth-refresh-lead.ts +46 -22
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +118 -51
- package/src/providers/operations/provider-rules/github.ts +48 -0
- package/src/providers/operations/provider-rules/grok.ts +33 -0
- package/src/providers/operations/provider-rules/inception.ts +49 -0
- package/src/providers/operations/provider-rules/index.ts +32 -0
- package/src/providers/operations/provider-rules/perch.ts +37 -0
- package/src/providers/operations/provider-rules/qoder.ts +70 -0
- package/src/providers/operations/provider-rules/trae.ts +81 -0
- package/src/providers/operations/session-resolution.ts +17 -1
- package/src/providers/provider-contracts.ts +6 -4
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +70 -10
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-contracts.ts +2 -0
- package/src/providers/quota/quota-limit-windows.ts +16 -5
- package/src/providers/quota/quota-support.ts +1 -1
- package/src/providers/quota/quota-window-parser.ts +90 -18
- package/src/providers/quota/refresh.ts +100 -21
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/security/ip-boundary.ts +1 -1
- package/src/transport/dispatch/attempt-finalize.ts +15 -14
- package/src/transport/dispatch/attempt-loop.ts +73 -19
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/handler-deps.ts +1 -1
- package/src/transport/dispatch/leases.ts +1 -1
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +8 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/retry-policy.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +10 -7
- package/src/transport/dispatch/streaming/stream.ts +9 -37
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +63 -0
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +16 -30
- package/src/transport/guards/body-policy.ts +7 -1
- package/src/transport/guards/error-lifecycle.ts +13 -1
- package/src/transport/guards/request-context.ts +22 -14
- package/src/transport/request/preparer.ts +81 -71
- package/src/transport/routing/bounded-lru.ts +14 -0
- package/src/transport/routing/drain-reserve.ts +129 -0
- package/src/transport/routing/plan-gate.ts +7 -1
- package/src/transport/routing/round-robin.ts +60 -0
- package/src/transport/routing/route-catalog.ts +123 -111
- package/src/transport/routing/route-model.ts +51 -9
- package/src/transport/routing/router.ts +393 -32
- package/src/transport/shutdown-notice.ts +1 -1
- package/src/transport/surface/adapters.ts +11 -2
- package/src/transport/surface/chat/adapter.ts +2 -2
- package/src/transport/surface/chat/encode.ts +47 -4
- package/src/transport/surface/chat/parse.ts +2 -12
- package/src/transport/surface/completion.ts +44 -33
- package/src/transport/surface/content-parts.ts +21 -0
- package/src/transport/surface/messages/adapter.ts +7 -1
- package/src/transport/surface/messages/encode.ts +11 -3
- package/src/transport/surface/messages/parse.ts +2 -1
- package/src/transport/surface/messages/stream.ts +33 -17
- package/src/transport/surface/responses/encode.ts +22 -5
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/buddy-growth-tasks.ts +8 -8
- package/src/workers/daily-checkin.ts +4 -4
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/zcode-gift-claim.ts +1 -1
- package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
- package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
- package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
- package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
- package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
- package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
- package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
- package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
- package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
- package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
- package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
- package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
- package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
- package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
- package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
- package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
- package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
- package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
- package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
- package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
- package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
- package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
- package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
- package/src/network/pool-health.ts +0 -210
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
- package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
- package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
- package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
- package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
- package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
- package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
- package/src/providers/integrations/qoder/chat-proto.ts +0 -328
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
- package/src/providers/operations/model-discovery-cache.ts +0 -34
- package/src/providers/operations/probe-fetch.ts +0 -15
- package/src/providers/operations/provider-version-cache.ts +0 -30
- package/src/providers/provider-capabilities.ts +0 -33
- package/src/providers/provider-credential-envelope.ts +0 -43
- /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
- /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
- /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
- /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
- /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
- /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
- /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
- /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
|
@@ -25,17 +25,26 @@ import {
|
|
|
25
25
|
type RequiredCapability,
|
|
26
26
|
} from "../../protocol/translation/capabilities";
|
|
27
27
|
import { planAllowsCandidate } from "./plan-gate";
|
|
28
|
+
import { accountReservePct, routingStickyWarmMs, routingStickyMaxSessions } from "../../config/routing";
|
|
29
|
+
import type { NormalizedQuotaFact } from "./route-model";
|
|
28
30
|
import { InMemoryAdmissionController, admissionKey } from "./admission";
|
|
29
31
|
import { ReservationManager } from "./reservations";
|
|
30
|
-
import { createRoundRobinMap, RoundRobinState } from "./round-robin";
|
|
32
|
+
import { createRoundRobinMap, RoundRobinState, StickySessionStore } from "./round-robin";
|
|
33
|
+
import { drainScore, isInReserve } from "./drain-reserve";
|
|
31
34
|
import {
|
|
32
35
|
lookupRouteCandidates,
|
|
33
36
|
parseModelReference,
|
|
34
37
|
routeCandidateIndex,
|
|
35
38
|
} from "./candidate-index";
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
import {
|
|
40
|
+
totalRemainingCredit,
|
|
41
|
+
totalRemainingPercent,
|
|
42
|
+
type ProviderQuotaResult,
|
|
43
|
+
type ProviderQuotaWindow,
|
|
44
|
+
} from "../../providers/quota/quota-contracts";
|
|
45
|
+
import { getCachedQuotaEntries, quotaLens, type CachedQuotaEntry } from "../../providers/quota/cache";
|
|
46
|
+
import { readQuotaSnapshots } from "../../providers/quota/snapshot";
|
|
47
|
+
import type { ProviderQuotaResultSnapshot } from "../../persistence/schema";
|
|
39
48
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
40
49
|
import type { RedisClient } from "../../persistence/redis";
|
|
41
50
|
import { sharedClaudeSlowLanes } from "../../providers/integrations/claude/claude-slow-lane";
|
|
@@ -88,6 +97,179 @@ export function filterCandidateQuotaWindows(
|
|
|
88
97
|
return windows;
|
|
89
98
|
}
|
|
90
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Global freshness horizon for persisted quota/plan evidence. A plan or
|
|
102
|
+
* snapshot older than this is downgraded to `unknown` by routing instead of
|
|
103
|
+
* being trusted indefinitely. First cut uses one global value (not
|
|
104
|
+
* provider-specific); see proposal locked decision #4.
|
|
105
|
+
*/
|
|
106
|
+
export const QUOTA_FRESHNESS_HORIZON_MS = 6 * 60 * 60 * 1000;
|
|
107
|
+
|
|
108
|
+
/** The account's total remaining absolute credit, or `null` when unknown. */
|
|
109
|
+
function accountRemainingCredit(candidate: RoutedCandidate): number | null {
|
|
110
|
+
const windows = candidate.quota_windows;
|
|
111
|
+
if (windows === undefined || windows.length === 0) return null;
|
|
112
|
+
return totalRemainingCredit(windows);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Evaluates whether the candidate sits below its configured usage floor.
|
|
117
|
+
* Mirrors the legacy ordering heuristic (credit vs lowest independent percent),
|
|
118
|
+
* but returns a tri-state used for proactive exclusion under the unified
|
|
119
|
+
* routing engine.
|
|
120
|
+
*/
|
|
121
|
+
function usageFloorStateFor(candidate: RoutedCandidate): "above" | "below" | "unknown" {
|
|
122
|
+
const floor = candidate.usage_floor;
|
|
123
|
+
if (floor === null || floor === undefined || floor <= 0) return "unknown";
|
|
124
|
+
if (candidate.free_tier === true) return "above";
|
|
125
|
+
const windows = candidate.quota_windows;
|
|
126
|
+
if (windows === undefined || windows.length === 0) return "unknown";
|
|
127
|
+
if (candidate.usage_floor_unit === "credit") {
|
|
128
|
+
const credit = accountRemainingCredit(candidate);
|
|
129
|
+
if (credit === null) return "unknown";
|
|
130
|
+
return credit >= floor ? "above" : "below";
|
|
131
|
+
}
|
|
132
|
+
const percent = totalRemainingPercent(windows);
|
|
133
|
+
if (percent === null) return "unknown";
|
|
134
|
+
return percent >= floor ? "above" : "below";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Earliest stated reset across exhausted windows, when any carries one. */
|
|
138
|
+
function exhaustedResetMs(windows: readonly ProviderQuotaWindow[] | undefined): number | undefined {
|
|
139
|
+
if (windows === undefined) return undefined;
|
|
140
|
+
let earliest: number | undefined;
|
|
141
|
+
for (const window of windows) {
|
|
142
|
+
if (window.resetsAt === null || window.resetsAt === undefined) continue;
|
|
143
|
+
const at = Date.parse(window.resetsAt);
|
|
144
|
+
if (!Number.isFinite(at)) continue;
|
|
145
|
+
if (earliest === undefined || at < earliest) earliest = at;
|
|
146
|
+
}
|
|
147
|
+
return earliest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** True when every relevant quota window for the candidate is known-spent. */
|
|
151
|
+
function allWindowsExhausted(windows: readonly ProviderQuotaWindow[]): boolean {
|
|
152
|
+
if (windows.length === 0) return false;
|
|
153
|
+
for (const window of windows) {
|
|
154
|
+
if (window.accounting === "pooled") {
|
|
155
|
+
const remaining = window.remaining;
|
|
156
|
+
if (remaining !== null && remaining !== undefined && remaining > 0) return false;
|
|
157
|
+
const limit = window.limit;
|
|
158
|
+
if (limit !== null && limit !== undefined) {
|
|
159
|
+
const used = window.used;
|
|
160
|
+
if (used !== null && used !== undefined && used < limit) return false;
|
|
161
|
+
if (used === null && window.remainingPercent !== null && window.remainingPercent > 0) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
} else if (window.remainingPercent !== null && window.remainingPercent > 0) {
|
|
165
|
+
return false;
|
|
166
|
+
} else if (remaining === null && window.used === null) {
|
|
167
|
+
// No evidence of either remaining or used — not provably spent.
|
|
168
|
+
return false;
|
|
169
|
+
} else if (remaining !== null && limit === null) {
|
|
170
|
+
// Remaining=0 but no limit: cannot prove the pool is exhausted without
|
|
171
|
+
// knowing the total. This is the "credit window without a known ceiling"
|
|
172
|
+
// shape — treat as unknown, not exhausted.
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
const remainingPercent = window.remainingPercent;
|
|
177
|
+
if (remainingPercent !== null && remainingPercent > 0) return false;
|
|
178
|
+
if (remainingPercent === null) {
|
|
179
|
+
// Unknown independent window: cannot prove exhaustion.
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Derives the normalized quota fact the routing decision engine evaluates.
|
|
189
|
+
*/
|
|
190
|
+
function normalizeQuotaFact(
|
|
191
|
+
candidate: RoutedCandidate,
|
|
192
|
+
cacheEntry: { readonly fetchedAt: string | null; readonly quota: ProviderQuotaResult } | null | undefined,
|
|
193
|
+
snapshotEntry: { readonly fetchedAt: string; readonly quota: ProviderQuotaResult } | null | undefined,
|
|
194
|
+
now = Date.now(),
|
|
195
|
+
): NormalizedQuotaFact {
|
|
196
|
+
const windows = candidate.quota_windows;
|
|
197
|
+
const source =
|
|
198
|
+
cacheEntry?.quota !== undefined
|
|
199
|
+
? cacheEntry.quota.error === null
|
|
200
|
+
? ("live_cache" as const)
|
|
201
|
+
: ("unsupported" as const)
|
|
202
|
+
: snapshotEntry?.quota !== undefined
|
|
203
|
+
? snapshotEntry.quota.error === null
|
|
204
|
+
? ("snapshot" as const)
|
|
205
|
+
: ("unsupported" as const)
|
|
206
|
+
: ("none" as const);
|
|
207
|
+
|
|
208
|
+
// Freshness: only live-cache and snapshot evidence carries an observed-at.
|
|
209
|
+
const observedSrc =
|
|
210
|
+
cacheEntry?.quota !== undefined && cacheEntry.quota.error === null
|
|
211
|
+
? cacheEntry
|
|
212
|
+
: snapshotEntry?.quota !== undefined && snapshotEntry.quota.error === null
|
|
213
|
+
? snapshotEntry
|
|
214
|
+
: null;
|
|
215
|
+
const rawObservedAt = observedSrc?.fetchedAt;
|
|
216
|
+
const observedAt =
|
|
217
|
+
rawObservedAt === null || rawObservedAt === undefined ? Number.NaN : Date.parse(rawObservedAt);
|
|
218
|
+
const hasObservedAt = Number.isFinite(observedAt) && observedAt > 0;
|
|
219
|
+
// Stale is only meaningful when we have an observation timestamp to judge.
|
|
220
|
+
// Without one (candidate-stamped windows, no cache/snapshot metadata) there
|
|
221
|
+
// is nothing to mark stale, so the windows remain usable evidence.
|
|
222
|
+
const isStale = hasObservedAt && now - observedAt > QUOTA_FRESHNESS_HORIZON_MS;
|
|
223
|
+
|
|
224
|
+
const usageFloorState = usageFloorStateFor(candidate);
|
|
225
|
+
// Stale evidence is downgraded to unknown — the freshness horizon (locked
|
|
226
|
+
// decision #4) means routing must not trust a weeks-old balance for either
|
|
227
|
+
// exhaustion or floor decisions.
|
|
228
|
+
if (isStale) {
|
|
229
|
+
return {
|
|
230
|
+
quotaState: "unknown",
|
|
231
|
+
quotaSource: source,
|
|
232
|
+
quotaObservedAt: hasObservedAt ? observedAt : null,
|
|
233
|
+
isStale: true,
|
|
234
|
+
usageFloorState: "unknown",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (windows === undefined || windows.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
quotaState: "unknown",
|
|
240
|
+
quotaSource: source,
|
|
241
|
+
quotaObservedAt: hasObservedAt ? observedAt : null,
|
|
242
|
+
isStale: false,
|
|
243
|
+
usageFloorState,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (usageFloorState === "below") {
|
|
247
|
+
return {
|
|
248
|
+
quotaState: "exhausted",
|
|
249
|
+
quotaSource: source,
|
|
250
|
+
quotaObservedAt: hasObservedAt ? observedAt : null,
|
|
251
|
+
isStale,
|
|
252
|
+
usageFloorState,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (allWindowsExhausted(windows)) {
|
|
256
|
+
return {
|
|
257
|
+
quotaState: "exhausted",
|
|
258
|
+
quotaSource: source,
|
|
259
|
+
quotaObservedAt: hasObservedAt ? observedAt : null,
|
|
260
|
+
isStale,
|
|
261
|
+
usageFloorState,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
quotaState: "available",
|
|
266
|
+
quotaSource: source,
|
|
267
|
+
quotaObservedAt: hasObservedAt ? observedAt : null,
|
|
268
|
+
isStale,
|
|
269
|
+
usageFloorState,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
91
273
|
/**
|
|
92
274
|
* Routing priority for an account's remaining allowance when two candidates sit
|
|
93
275
|
* in the same rank group: positive remaining credit first, then unknown, then
|
|
@@ -103,6 +285,23 @@ function creditPriority(candidate: RoutedCandidate): number {
|
|
|
103
285
|
return credit > 0 ? 1 : -1;
|
|
104
286
|
}
|
|
105
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Hot-window guard: true when the account's most-urgent window is ≥85% used.
|
|
290
|
+
* A nearly exhausted short window means an imminent mid-session block, so these
|
|
291
|
+
* candidates rank behind cooler siblings no matter their drain score — overflow
|
|
292
|
+
* lands on the next-most-urgent cool account instead.
|
|
293
|
+
*/
|
|
294
|
+
function hotWindow(windows: readonly ProviderQuotaWindow[] | undefined): boolean {
|
|
295
|
+
if (windows === undefined || windows.length === 0) return false;
|
|
296
|
+
for (const window of windows) {
|
|
297
|
+
const used = window.usedPercent;
|
|
298
|
+
if (typeof used === "number" && Number.isFinite(used) && used >= 85) return true;
|
|
299
|
+
const remaining = window.remainingPercent;
|
|
300
|
+
if (typeof remaining === "number" && Number.isFinite(remaining) && remaining <= 15) return true;
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
106
305
|
/**
|
|
107
306
|
* Normalizes a requested model name before alias lookup. Coding-agent CLIs
|
|
108
307
|
* version their model ids with variant suffixes (`claude-opus-5[1m]`,
|
|
@@ -299,13 +498,6 @@ export class EligibilityEvaluator {
|
|
|
299
498
|
return { eligible: true, reason: "cooldown", candidate, ...spreadDeadline(candidate.cooldown_until) };
|
|
300
499
|
return { eligible: true, reason: "healthy", candidate };
|
|
301
500
|
}
|
|
302
|
-
|
|
303
|
-
filter(candidates: readonly RouteCandidate[]): readonly RouteCandidate[] {
|
|
304
|
-
return candidates
|
|
305
|
-
.map((c) => this.evaluate(c))
|
|
306
|
-
.filter((d) => d.eligible)
|
|
307
|
-
.map((d) => d.candidate);
|
|
308
|
-
}
|
|
309
501
|
}
|
|
310
502
|
|
|
311
503
|
export class RoutingEngine {
|
|
@@ -313,6 +505,7 @@ export class RoutingEngine {
|
|
|
313
505
|
private readonly _roundRobinByProvider = createRoundRobinMap();
|
|
314
506
|
private readonly eligibility = new EligibilityEvaluator();
|
|
315
507
|
private readonly reservations = new ReservationManager();
|
|
508
|
+
private readonly sticky = new StickySessionStore(routingStickyMaxSessions());
|
|
316
509
|
|
|
317
510
|
constructor(
|
|
318
511
|
private readonly admission: AdmissionController = new InMemoryAdmissionController(),
|
|
@@ -440,6 +633,8 @@ export class RoutingEngine {
|
|
|
440
633
|
candidate: RouteCandidate,
|
|
441
634
|
snapshot: RouteSnapshot,
|
|
442
635
|
tenantId: string | null,
|
|
636
|
+
quotaCache: ReadonlyMap<string, CachedQuotaEntry>,
|
|
637
|
+
snapshotMap: ReadonlyMap<string, ProviderQuotaResultSnapshot>,
|
|
443
638
|
): Promise<RouteCandidate> {
|
|
444
639
|
const accountId = candidate.provider_account_id;
|
|
445
640
|
if (accountId === undefined) return candidate;
|
|
@@ -448,11 +643,7 @@ export class RoutingEngine {
|
|
|
448
643
|
candidate.provider_id,
|
|
449
644
|
candidate.tenant_id ?? tenantId,
|
|
450
645
|
);
|
|
451
|
-
const cached =
|
|
452
|
-
quotaLens(candidate.tenant_id ?? tenantId),
|
|
453
|
-
accountId,
|
|
454
|
-
this.redis,
|
|
455
|
-
).catch(() => undefined);
|
|
646
|
+
const cached = quotaCache.get(accountId);
|
|
456
647
|
// The live cache is authoritative when warm; when it is cold (e.g. right
|
|
457
648
|
// after a restart, before the sweep re-reads accounts), fall back to the
|
|
458
649
|
// persisted quota snapshot — the durable source of truth — so routing
|
|
@@ -460,9 +651,13 @@ export class RoutingEngine {
|
|
|
460
651
|
// is never fresher than the cache, but a stale balance beats an unknown
|
|
461
652
|
// one for ordering, and the sweep refreshes it on schedule.
|
|
462
653
|
let quota = cached?.quota;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
654
|
+
let stored: { readonly fetchedAt: string; readonly quota: ProviderQuotaResult } | null = null;
|
|
655
|
+
if (quota === undefined) {
|
|
656
|
+
const snap = snapshotMap.get(accountId);
|
|
657
|
+
if (snap !== undefined) {
|
|
658
|
+
stored = { fetchedAt: snap.fetchedAt, quota: snap.quota };
|
|
659
|
+
quota = snap.quota;
|
|
660
|
+
}
|
|
466
661
|
}
|
|
467
662
|
const rawWindows = quota?.windows;
|
|
468
663
|
// The cache is authoritative when it has a reading; when it does not,
|
|
@@ -472,14 +667,32 @@ export class RoutingEngine {
|
|
|
472
667
|
rawWindows === undefined
|
|
473
668
|
? undefined
|
|
474
669
|
: candidate.provider_id === "zcode-start" ? rawWindows : filterCandidateQuotaWindows(rawWindows, candidate);
|
|
475
|
-
|
|
670
|
+
const stamped: RoutedCandidate = {
|
|
476
671
|
...candidate,
|
|
477
672
|
...(setting?.usageFloor == null
|
|
478
673
|
? {}
|
|
479
674
|
: { usage_floor: setting.usageFloor, usage_floor_unit: setting.usageFloorUnit }),
|
|
480
|
-
|
|
675
|
+
// When no cache/snapshot evidence exists, keep the candidate's own
|
|
676
|
+
// stamped windows (unit tests and in-memory fixture route this way).
|
|
677
|
+
...(quotaWindows === undefined
|
|
678
|
+
? candidate.quota_windows === undefined
|
|
679
|
+
? {}
|
|
680
|
+
: { quota_windows: candidate.quota_windows }
|
|
681
|
+
: { quota_windows: quotaWindows }),
|
|
481
682
|
...(quota?.source === "zcode-start" && quota.error === null ? { quota_entitlements_known: true } : {}),
|
|
683
|
+
// Drain urgency and reserve are derived from the same windows the credit
|
|
684
|
+
// ordering already reads. They are ordering preferences, never
|
|
685
|
+
// exclusions: an account in reserve or with a zero drain score stays
|
|
686
|
+
// eligible and routed.
|
|
687
|
+
...(quotaWindows === undefined && candidate.quota_windows === undefined
|
|
688
|
+
? {}
|
|
689
|
+
: { drain_score: drainScore(quotaWindows ?? candidate.quota_windows) }),
|
|
690
|
+
...(quotaWindows === undefined && candidate.quota_windows === undefined
|
|
691
|
+
? {}
|
|
692
|
+
: { in_reserve: isInReserve(quotaWindows ?? candidate.quota_windows, accountReservePct()) }),
|
|
482
693
|
};
|
|
694
|
+
const fact = normalizeQuotaFact(stamped, cached, stored);
|
|
695
|
+
return { ...stamped, quota_fact: fact };
|
|
483
696
|
}
|
|
484
697
|
|
|
485
698
|
/**
|
|
@@ -526,6 +739,24 @@ export class RoutingEngine {
|
|
|
526
739
|
},
|
|
527
740
|
};
|
|
528
741
|
}
|
|
742
|
+
// Unified decision engine: normalized quota/plan fact.
|
|
743
|
+
const fact = candidate.quota_fact;
|
|
744
|
+
if (fact !== undefined) {
|
|
745
|
+
if (fact.quotaState === "exhausted" || fact.usageFloorState === "below") {
|
|
746
|
+
const reset = exhaustedResetMs(candidate.quota_windows);
|
|
747
|
+
return {
|
|
748
|
+
candidate,
|
|
749
|
+
decision: {
|
|
750
|
+
eligible: false,
|
|
751
|
+
rank: ROUTING_RANK.SATURATED,
|
|
752
|
+
reason: "quota_exhausted",
|
|
753
|
+
floor_blocked: fact.usageFloorState === "below",
|
|
754
|
+
saturated: false,
|
|
755
|
+
...(reset === undefined ? {} : { cooldown_until: reset }),
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
529
760
|
// Plan gate (free plan).
|
|
530
761
|
if (!planAllowsCandidate(candidate)) {
|
|
531
762
|
return {
|
|
@@ -552,11 +783,54 @@ export class RoutingEngine {
|
|
|
552
783
|
};
|
|
553
784
|
}
|
|
554
785
|
}
|
|
786
|
+
// A soft cooldown is a deprioritization, not an exclusion: the evaluator
|
|
787
|
+
// returned it eligible (so a single-account deployment still routes), but
|
|
788
|
+
// it must still behind every fresh/slow sibling. Without this branch the
|
|
789
|
+
// candidate falls through to the quota classification below, which labels
|
|
790
|
+
// it FRESH_KNOWN/FRESH_UNKNOWN and lets it sort ahead of healthy accounts —
|
|
791
|
+
// the "still hit a cooled-down account, never failed over" report. The raw
|
|
792
|
+
// `reason` stays `"cooldown"` so `ELIGIBILITY_REASON_LABELS` and the
|
|
793
|
+
// envelope `reasons` stream are untouched, and the reset deadline rides
|
|
794
|
+
// along for the all-cooling 429 answer.
|
|
795
|
+
if (eligibility.reason === "cooldown") {
|
|
796
|
+
return {
|
|
797
|
+
candidate,
|
|
798
|
+
decision: {
|
|
799
|
+
eligible: true,
|
|
800
|
+
rank: ROUTING_RANK.COOLING,
|
|
801
|
+
reason: "cooldown",
|
|
802
|
+
...(eligibility.cooldown_until !== undefined
|
|
803
|
+
? { cooldown_until: eligibility.cooldown_until }
|
|
804
|
+
: {}),
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
// Unified engine: unknown quota is eligible but lower-rank than known-available.
|
|
809
|
+
if (fact !== undefined) {
|
|
810
|
+
if (fact.quotaState === "unknown") {
|
|
811
|
+
return {
|
|
812
|
+
candidate,
|
|
813
|
+
decision: {
|
|
814
|
+
eligible: true,
|
|
815
|
+
rank: ROUTING_RANK.FRESH_UNKNOWN,
|
|
816
|
+
reason: "quota_unknown",
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
candidate,
|
|
822
|
+
decision: {
|
|
823
|
+
eligible: true,
|
|
824
|
+
rank: ROUTING_RANK.FRESH_KNOWN,
|
|
825
|
+
reason: "healthy",
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
555
829
|
return {
|
|
556
830
|
candidate,
|
|
557
831
|
decision: {
|
|
558
832
|
eligible: true,
|
|
559
|
-
rank: ROUTING_RANK.
|
|
833
|
+
rank: ROUTING_RANK.FRESH_KNOWN,
|
|
560
834
|
reason: "healthy",
|
|
561
835
|
},
|
|
562
836
|
};
|
|
@@ -578,11 +852,11 @@ export class RoutingEngine {
|
|
|
578
852
|
let i = 0;
|
|
579
853
|
while (i < eligible.length) {
|
|
580
854
|
const cur = eligible[i]!;
|
|
581
|
-
const rank = decisions.get(admissionKey(cur))?.rank ?? ROUTING_RANK.
|
|
855
|
+
const rank = decisions.get(admissionKey(cur))?.rank ?? ROUTING_RANK.FRESH_KNOWN;
|
|
582
856
|
let j = i + 1;
|
|
583
857
|
while (j < eligible.length) {
|
|
584
858
|
const next = eligible[j]!;
|
|
585
|
-
const nextRank = decisions.get(admissionKey(next))?.rank ?? ROUTING_RANK.
|
|
859
|
+
const nextRank = decisions.get(admissionKey(next))?.rank ?? ROUTING_RANK.FRESH_KNOWN;
|
|
586
860
|
if (
|
|
587
861
|
next.provider_id !== cur.provider_id ||
|
|
588
862
|
next.model_id !== cur.model_id ||
|
|
@@ -674,7 +948,7 @@ export class RoutingEngine {
|
|
|
674
948
|
const decision = pinnedDecisions.find((d) => d.candidate === candidate);
|
|
675
949
|
decisions.set(admissionKey(candidate), {
|
|
676
950
|
eligible: true,
|
|
677
|
-
rank: ROUTING_RANK.
|
|
951
|
+
rank: ROUTING_RANK.FRESH_KNOWN,
|
|
678
952
|
reason: "healthy",
|
|
679
953
|
...(decision?.cooldown_until !== undefined
|
|
680
954
|
? { cooldown_until: decision.cooldown_until }
|
|
@@ -699,6 +973,7 @@ export class RoutingEngine {
|
|
|
699
973
|
allowCliMappings = false,
|
|
700
974
|
keyId?: string,
|
|
701
975
|
continuationAffinity?: RouteContinuationAffinity,
|
|
976
|
+
sessionId?: string,
|
|
702
977
|
): Promise<RoutePlan> {
|
|
703
978
|
const tid = tenantId ?? null;
|
|
704
979
|
const { resolved, matching, unmatchedMembers } = this.resolveMatchingCandidates(
|
|
@@ -751,9 +1026,49 @@ export class RoutingEngine {
|
|
|
751
1026
|
// single per-candidate decision. `plan()` then sorts once by rank and
|
|
752
1027
|
// hands the full `decisions` map downstream, so no later layer re-derives
|
|
753
1028
|
// a competing ordering.
|
|
1029
|
+
//
|
|
1030
|
+
// Quota evidence is read in batch before the per-candidate pass: one
|
|
1031
|
+
// in-memory/Redis `MGET` per lens and one DB query for the accounts the
|
|
1032
|
+
// cache did not answer, instead of a serial round trip per candidate. The
|
|
1033
|
+
// per-candidate stamping below then consumes the shared maps, keeping the
|
|
1034
|
+
// live-cache-authoritative / snapshot-fallback semantics identical.
|
|
1035
|
+
const quotaCacheByAccount = new Map<string, CachedQuotaEntry>();
|
|
1036
|
+
const lensToAccounts = new Map<string, string[]>();
|
|
1037
|
+
for (const candidate of matching) {
|
|
1038
|
+
const accountId = candidate.provider_account_id;
|
|
1039
|
+
if (accountId === undefined) continue;
|
|
1040
|
+
const lens = quotaLens(candidate.tenant_id ?? tid);
|
|
1041
|
+
const accounts = lensToAccounts.get(lens);
|
|
1042
|
+
if (accounts === undefined) lensToAccounts.set(lens, [accountId]);
|
|
1043
|
+
else accounts.push(accountId);
|
|
1044
|
+
}
|
|
1045
|
+
for (const [lens, accountIds] of lensToAccounts) {
|
|
1046
|
+
const entries = await getCachedQuotaEntries(lens, accountIds, this.redis).catch(
|
|
1047
|
+
() => new Map<string, CachedQuotaEntry>(),
|
|
1048
|
+
);
|
|
1049
|
+
for (const [accountId, entry] of entries) quotaCacheByAccount.set(accountId, entry);
|
|
1050
|
+
}
|
|
1051
|
+
const snapshotByAccount = new Map<string, ProviderQuotaResultSnapshot>();
|
|
1052
|
+
if (this.db !== undefined) {
|
|
1053
|
+
const needingSnapshot = matching
|
|
1054
|
+
.map((candidate) => candidate.provider_account_id)
|
|
1055
|
+
.filter((accountId): accountId is string => accountId !== undefined && !quotaCacheByAccount.has(accountId));
|
|
1056
|
+
if (needingSnapshot.length > 0) {
|
|
1057
|
+
const snaps = await readQuotaSnapshots(this.db, needingSnapshot).catch(
|
|
1058
|
+
() => new Map<string, ProviderQuotaResultSnapshot>(),
|
|
1059
|
+
);
|
|
1060
|
+
for (const [accountId, snap] of snaps) snapshotByAccount.set(accountId, snap);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
754
1063
|
const evaluations = await Promise.all(
|
|
755
1064
|
matching.map(async (candidate) => {
|
|
756
|
-
const stamped = await this.applyUsageFloorToCandidate(
|
|
1065
|
+
const stamped = await this.applyUsageFloorToCandidate(
|
|
1066
|
+
candidate,
|
|
1067
|
+
snapshot,
|
|
1068
|
+
tid,
|
|
1069
|
+
quotaCacheByAccount,
|
|
1070
|
+
snapshotByAccount,
|
|
1071
|
+
);
|
|
757
1072
|
return this.evaluateCandidate(stamped, snapshot, tid, requiredCapabilities);
|
|
758
1073
|
}),
|
|
759
1074
|
);
|
|
@@ -810,7 +1125,9 @@ export class RoutingEngine {
|
|
|
810
1125
|
|
|
811
1126
|
// Every eligible candidate cooling and no healthy/slow sibling → 429.
|
|
812
1127
|
const healthyCount = [...decisions.values()].filter(
|
|
813
|
-
(d) =>
|
|
1128
|
+
(d) =>
|
|
1129
|
+
d.eligible &&
|
|
1130
|
+
(d.reason === "healthy" || d.reason === "slow" || d.reason === "quota_unknown"),
|
|
814
1131
|
).length;
|
|
815
1132
|
if (healthyCount === 0) {
|
|
816
1133
|
throw accountsRateLimitedError(
|
|
@@ -821,13 +1138,50 @@ export class RoutingEngine {
|
|
|
821
1138
|
}
|
|
822
1139
|
|
|
823
1140
|
// Single sort by rank (fresh → slow → cooling → saturated), stable. Within
|
|
824
|
-
// the same rank,
|
|
825
|
-
//
|
|
1141
|
+
// the same rank, ordered by credit → reserve → hot-window guard → measured
|
|
1142
|
+
// usage → drain → round-robin. The drain score chases quota about to expire
|
|
1143
|
+
// unused; reserve yields to non-reserve siblings; a hot primary window (≥85%
|
|
1144
|
+
// used) defers to cooler accounts so overflow lands on a cool sibling.
|
|
1145
|
+
const reservePct = accountReservePct();
|
|
1146
|
+
const stickyWarmMs = routingStickyWarmMs();
|
|
1147
|
+
const stickyEntry = stickyWarmMs > 0 ? this.sticky.get(eligible[0]?.provider_id ?? "", sessionId) : undefined;
|
|
1148
|
+
const stickyAccountId = stickyEntry?.accountId;
|
|
1149
|
+
const stickyWarm = stickyEntry !== undefined && Date.now() - stickyEntry.lastUsedAtMs < stickyWarmMs;
|
|
826
1150
|
eligible = [...eligible].sort((left, right) => {
|
|
827
|
-
const leftRank = decisions.get(admissionKey(left))?.rank ?? ROUTING_RANK.
|
|
828
|
-
const rightRank = decisions.get(admissionKey(right))?.rank ?? ROUTING_RANK.
|
|
1151
|
+
const leftRank = decisions.get(admissionKey(left))?.rank ?? ROUTING_RANK.FRESH_KNOWN;
|
|
1152
|
+
const rightRank = decisions.get(admissionKey(right))?.rank ?? ROUTING_RANK.FRESH_KNOWN;
|
|
829
1153
|
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
830
|
-
|
|
1154
|
+
// A free-tier model routes to any account: skip every quota-derived
|
|
1155
|
+
// preference (credit, reserve, hot-window, drain) so the original order
|
|
1156
|
+
// is preserved. Sticky warm pins still apply — they are about prompt
|
|
1157
|
+
// cache warmth, not quota.
|
|
1158
|
+
const bothFree = left.free_tier === true && right.free_tier === true;
|
|
1159
|
+
// Sticky warm pin: within the warm window the account that last served
|
|
1160
|
+
// this session wins outright (explicit pins always win).
|
|
1161
|
+
const leftSticky = left.provider_account_id === stickyAccountId;
|
|
1162
|
+
const rightSticky = right.provider_account_id === stickyAccountId;
|
|
1163
|
+
if (leftSticky !== rightSticky) {
|
|
1164
|
+
if (stickyEntry?.explicit === true) return leftSticky ? -1 : 1;
|
|
1165
|
+
if (stickyWarm) return leftSticky ? -1 : 1;
|
|
1166
|
+
}
|
|
1167
|
+
if (bothFree) return 0;
|
|
1168
|
+
const leftReserve = (left as RoutedCandidate).in_reserve ??
|
|
1169
|
+
isInReserve(left.quota_windows, reservePct);
|
|
1170
|
+
const rightReserve = (right as RoutedCandidate).in_reserve ??
|
|
1171
|
+
isInReserve(right.quota_windows, reservePct);
|
|
1172
|
+
if (leftReserve !== rightReserve) return leftReserve ? 1 : -1;
|
|
1173
|
+
const leftPriority = (left as RoutedCandidate).account_priority ?? 0;
|
|
1174
|
+
const rightPriority = (right as RoutedCandidate).account_priority ?? 0;
|
|
1175
|
+
if (leftPriority !== rightPriority) return rightPriority - leftPriority;
|
|
1176
|
+
const leftHot = hotWindow(left.quota_windows);
|
|
1177
|
+
const rightHot = hotWindow(right.quota_windows);
|
|
1178
|
+
if (leftHot !== rightHot) return leftHot ? 1 : -1;
|
|
1179
|
+
const credit = creditPriority(right) - creditPriority(left);
|
|
1180
|
+
if (credit !== 0) return credit;
|
|
1181
|
+
const leftDrain = (left as RoutedCandidate).drain_score ?? 0;
|
|
1182
|
+
const rightDrain = (right as RoutedCandidate).drain_score ?? 0;
|
|
1183
|
+
if (leftDrain !== rightDrain) return rightDrain - leftDrain;
|
|
1184
|
+
return 0;
|
|
831
1185
|
});
|
|
832
1186
|
|
|
833
1187
|
// Provider routing rotates accounts within the same rank group only; it can
|
|
@@ -836,6 +1190,13 @@ export class RoutingEngine {
|
|
|
836
1190
|
eligible = this.applyProviderRoutingSorted(eligible, decisions, snapshot, tid);
|
|
837
1191
|
|
|
838
1192
|
const chosen = eligible[0]!;
|
|
1193
|
+
// Record which account served this session so a follow-up request within the
|
|
1194
|
+
// warm window can prefer it (prompt-cache warmth). Explicit operator pins are
|
|
1195
|
+
// recorded as explicit so they win even past the warm window.
|
|
1196
|
+
if (sessionId !== undefined && chosen.provider_account_id !== undefined) {
|
|
1197
|
+
const explicitPin = continuationAffinity?.providerAccountId === chosen.provider_account_id;
|
|
1198
|
+
this.sticky.set(chosen.provider_id, sessionId, chosen.provider_account_id, explicitPin);
|
|
1199
|
+
}
|
|
839
1200
|
return {
|
|
840
1201
|
revision: snapshot.revision,
|
|
841
1202
|
candidates: eligible,
|
|
@@ -34,7 +34,7 @@ export function shutdownNotice(reason: string | undefined): ShutdownNotice {
|
|
|
34
34
|
return {
|
|
35
35
|
code: "shutting_down",
|
|
36
36
|
message:
|
|
37
|
-
"Service is
|
|
37
|
+
"Service is restarting for an update; it will be back in about a minute (request {requestId}). Maintenance mode — back in a minute 🧘",
|
|
38
38
|
reason: "restart_for_update",
|
|
39
39
|
};
|
|
40
40
|
}
|
|
@@ -113,13 +113,22 @@ function endpointPathSurface(path: string): SourceSurface | undefined {
|
|
|
113
113
|
return undefined;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export class SurfaceAdapterRegistry {
|
|
117
|
-
constructor(private readonly adapters:
|
|
116
|
+
export class SurfaceAdapterRegistry<T extends SurfaceAdapter = SurfaceAdapter> {
|
|
117
|
+
constructor(private readonly adapters: T[]) {
|
|
118
118
|
if (!adapters || adapters.length === 0) {
|
|
119
119
|
throw new Error("SurfaceAdapterRegistry requires at least one adapter");
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Map of surface → adapter. The keys are the adapters' `surface` values, so
|
|
125
|
+
* this is the single place pipeline composition derives the adapter lookup
|
|
126
|
+
* from — never re-spell the adapter list beside the registry.
|
|
127
|
+
*/
|
|
128
|
+
bySurface(): ReadonlyMap<SourceSurface, T> {
|
|
129
|
+
return new Map(this.adapters.map((adapter) => [adapter.surface, adapter]));
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
/**
|
|
124
133
|
* Evaluates every signal once, then picks a winner by the fixed precedence
|
|
125
134
|
* above. Evaluating all three even when an earlier signal already wins is
|
|
@@ -2,9 +2,9 @@ import type { CanonicalEvent, CanonicalMessage, CanonicalRequest, ContentPart }
|
|
|
2
2
|
import type { SurfaceAdapter, SurfaceInput, SurfaceOutput } from "../adapters";
|
|
3
3
|
import { isRecord } from "../../../protocol/codecs";
|
|
4
4
|
import { readString } from "../../../protocol/codecs";
|
|
5
|
+
import { decodeWireBody } from "../content-parts";
|
|
5
6
|
import type { ChatEncodeOptions } from "./parse";
|
|
6
7
|
import {
|
|
7
|
-
decodeBody,
|
|
8
8
|
parseGenerationControls,
|
|
9
9
|
parseMessage,
|
|
10
10
|
requestBody,
|
|
@@ -38,7 +38,7 @@ export class ChatAdapter implements SurfaceAdapter {
|
|
|
38
38
|
* false "ambiguous body shape" disagreement while routing correctly.
|
|
39
39
|
*/
|
|
40
40
|
matchesBodyShape(body: unknown): boolean {
|
|
41
|
-
const value =
|
|
41
|
+
const value = decodeWireBody(body);
|
|
42
42
|
if (!isRecord(value) || "input" in value || !Array.isArray(value.messages)) return false;
|
|
43
43
|
if (value.messages.length === 0) return false;
|
|
44
44
|
return value.messages.some((message) => isRecord(message) && typeof message.role === "string");
|