cartethyia 2.7.9 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -2
- package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-BJ87Pp0U.js → ProxyPage-BwNC_-Cx.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
- package/dist/dashboard/assets/assert-CqTLw5e7.js +1 -0
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-CEL5Sg_J.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-HvBW0AjI.js} +1 -1
- package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
- package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +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-BFQX2VBc.js} +1 -1
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
- package/dist/dashboard/assets/useMutation-BClG3yWj.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/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/package.json +1 -1
- package/src/boot/app.ts +10 -13
- package/src/boot/dependencies.ts +1 -1
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +20 -0
- 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/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/domain-registration.ts +0 -2
- 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/store.ts +0 -53
- package/src/console/providers/catalog/contracts.ts +2 -2
- package/src/console/providers/catalog/discover-constants.ts +6 -0
- package/src/console/providers/catalog/routes.ts +1 -0
- package/src/console/providers/catalog/store.ts +21 -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 +6 -18
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/operations.ts +31 -46
- package/src/console/shared/errors.ts +33 -6
- package/src/main.ts +2 -3
- package/src/network/pool/byte-accounting.ts +3 -2
- package/src/network/pool/selector.ts +182 -39
- package/src/network/pool-cooldown-store.ts +89 -14
- package/src/network/pool-health.ts +49 -61
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/ssrf.ts +6 -6
- 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 +15 -25
- 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 +8 -0
- package/src/persistence/ttl-cache.ts +20 -2
- package/src/protocol/codecs.ts +1 -1
- 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 +3 -4
- package/src/protocol/response/chat.ts +12 -20
- 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/providers/adapters/compatible-adapter.ts +71 -12
- package/src/providers/authentication/oauth-client.ts +1 -1
- package/src/providers/authentication/oauth-device-flow.ts +98 -8
- package/src/providers/authentication/oauth-refresh-service.ts +30 -49
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +61 -36
- package/src/providers/discovery/probe-phases.ts +14 -4
- package/src/providers/discovery/probing-service.ts +15 -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 +5 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
- package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
- package/src/providers/integrations/buddy/capabilities.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline.ts +2 -2
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
- package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
- package/src/providers/integrations/cursor/cursor-request.ts +26 -12
- package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
- package/src/providers/integrations/cursor/cursor.ts +89 -40
- 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/devin/protocol/devin-proto.ts +1 -27
- package/src/providers/integrations/factory/factory-oauth.ts +17 -44
- 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/grok/grok-oauth.ts +48 -45
- 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.ts → hermes/hermes.ts} +9 -9
- package/src/providers/integrations/hermes/index.ts +2 -0
- 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-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- 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/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-protocol.ts +0 -3
- package/src/providers/integrations/perch/perch-quota.ts +2 -16
- package/src/providers/integrations/perch/perch.ts +3 -13
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
- package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
- package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
- package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
- package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
- package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
- package/src/providers/integrations/qoder/qoder.ts +0 -1
- package/src/providers/integrations/trae/trae-protocol.ts +9 -5
- package/src/providers/integrations/trae/trae-quota.ts +6 -28
- package/src/providers/integrations/trae/trae-realm.ts +38 -0
- package/src/providers/integrations/trae/trae.ts +7 -24
- 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 +204 -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 +22 -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 +5 -51
- package/src/providers/operations/cooldown-rules.ts +11 -16
- 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 +102 -32
- 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/provider-contracts.ts +4 -2
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +40 -10
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- 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 +75 -18
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/transport/dispatch/attempt-finalize.ts +14 -13
- package/src/transport/dispatch/attempt-loop.ts +71 -17
- package/src/transport/dispatch/generation.ts +2 -8
- 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/streaming/finalize.ts +9 -6
- package/src/transport/dispatch/streaming/stream.ts +8 -36
- 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/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/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.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/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- 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-DIXLjojg.js} +0 -0
- /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
|
@@ -2,7 +2,6 @@ import { GatewayError, type GatewayErrorCode, type GatewayErrorOrigin } from "..
|
|
|
2
2
|
import { isRecord } from "../protocol/codecs";
|
|
3
3
|
import { resolveFallbackRetryBaseMs, resolveFallbackRetryCapMs } from "../config/timeouts";
|
|
4
4
|
import { parseUpstreamBackoff } from "../protocol/upstream-parsing";
|
|
5
|
-
import { resolveAccountQuotaCooldownMs } from "../config/cooldowns";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* How much of the upstream's error body is kept and surfaced.
|
|
@@ -260,31 +259,6 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
|
|
|
260
259
|
// DNS failure. Failing the client request instead of failing over is the wrong
|
|
261
260
|
// outcome for a whole class the table already names as platform-wide.
|
|
262
261
|
const statusCode = error.status;
|
|
263
|
-
// Qoder answers an exhausted quota by closing the stream with an upstream 499
|
|
264
|
-
// (nginx client-close style). That is a refusal from the upstream — the
|
|
265
|
-
// gateway's own aborts are `transport_closed`, produced locally, and never
|
|
266
|
-
// carry a qoder providerId — so a 499 scoped to qoder is quota, not a
|
|
267
|
-
// malformed request. Without this it fell through to `invalid_request`
|
|
268
|
-
// (non-retryable, no cooldown) and the account was re-dialled on every
|
|
269
|
-
// request inside the exhausted window until a late 429 finally classified it.
|
|
270
|
-
if (
|
|
271
|
-
error.origin === "upstream" &&
|
|
272
|
-
statusCode === 499 &&
|
|
273
|
-
(providerId === "qoder" || providerId === "qodercn")
|
|
274
|
-
) {
|
|
275
|
-
const cooldownMs = resolveAccountQuotaCooldownMs();
|
|
276
|
-
return {
|
|
277
|
-
retryable: true,
|
|
278
|
-
mutatesAccount: true,
|
|
279
|
-
category: "quota_exceeded",
|
|
280
|
-
scope: "account",
|
|
281
|
-
origin: error.origin,
|
|
282
|
-
...(providerCode ? { providerCode } : {}),
|
|
283
|
-
...(providerId ? { providerId } : {}),
|
|
284
|
-
cooldownMs,
|
|
285
|
-
statusCode,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
262
|
const retryableByStatus =
|
|
289
263
|
statusCode === 401 ||
|
|
290
264
|
statusCode === 403 ||
|
|
@@ -684,10 +658,8 @@ const QUOTA_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
|
|
|
684
658
|
// quota (a cooldown) instead of `auth_invalidated`.
|
|
685
659
|
"110",
|
|
686
660
|
// Qoder also reports quota exhaustion as `{"code":116,"error":"quota
|
|
687
|
-
// exceeded"}
|
|
688
|
-
//
|
|
689
|
-
// naming the code here makes any status that carries it classify as quota
|
|
690
|
-
// instead of `invalid_request`/`authentication_failed`.
|
|
661
|
+
// exceeded"}`. Naming the code here makes any status that carries it classify
|
|
662
|
+
// as quota instead of `invalid_request`/`authentication_failed`.
|
|
691
663
|
"116",
|
|
692
664
|
// InferHub publishes its own vocabulary in `error.code`. The two 403s below
|
|
693
665
|
// are deliberate absences: `account_blocked` means the account was blocked
|
|
@@ -846,6 +818,20 @@ export function classifyUpstreamError(
|
|
|
846
818
|
return { code: "proxy_auth_required", status: 407, origin: "network" };
|
|
847
819
|
if (normalized === "11140")
|
|
848
820
|
return { code: "policy_rejected", status: 403, origin: "upstream" };
|
|
821
|
+
// ZCode plan-lane envelope codes (verified live 2026-10-30 by the donor and
|
|
822
|
+
// mirrored by the zcode-start provider). `3009` is model concurrency
|
|
823
|
+
// (transient, retried in place), `3008` user concurrency (the desktop client
|
|
824
|
+
// is busy on that account — switchable), `3012` is the identity/prompt-shape
|
|
825
|
+
// gate, `1113` the paid-lane "insufficient balance or no resource package"
|
|
826
|
+
// (a request that was not recognized as start-plan traffic).
|
|
827
|
+
if (normalized === "3009")
|
|
828
|
+
return { code: "rate_limited", status: 429, origin: "upstream" };
|
|
829
|
+
if (normalized === "3008")
|
|
830
|
+
return { code: "accounts_rate_limited", status: 429, origin: "upstream" };
|
|
831
|
+
if (normalized === "3012")
|
|
832
|
+
return { code: "policy_rejected", status: 405, origin: "upstream" };
|
|
833
|
+
if (normalized === "1113")
|
|
834
|
+
return { code: "quota_exceeded", status: 429, origin: "upstream", rateLimitScope: "provider" };
|
|
849
835
|
if (normalized !== undefined && CONTEXT_LENGTH_CODES.has(normalized))
|
|
850
836
|
return { code: "context_length_exceeded", status: 413, origin: "upstream" };
|
|
851
837
|
if (normalized !== undefined && QUOTA_ERROR_IDENTIFIERS.has(normalized))
|
|
@@ -58,8 +58,14 @@ export function isProxyDispatchRoute(path: string | undefined): path is string {
|
|
|
58
58
|
return path !== undefined && isJsonProxyRoute(path);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The same canonical JSON-route membership as `isProxyDispatchRoute`, kept as
|
|
63
|
+
* a non-narrowing alias for callers that already hold a string path. One
|
|
64
|
+
* predicate, two spellings, so a route-table change cannot diverge between
|
|
65
|
+
* the body-read policy and the canonical-parse gate.
|
|
66
|
+
*/
|
|
61
67
|
export function isJsonProxyRoutePath(path: string): boolean {
|
|
62
|
-
return
|
|
68
|
+
return isProxyDispatchRoute(path);
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
interface IngressPolicyOptions {
|
|
@@ -77,8 +77,20 @@ export function createErrorNormalizationMiddleware(deps: {
|
|
|
77
77
|
.error(({ request, error, set }) => {
|
|
78
78
|
const state = deps.stateStore.get(request as Request);
|
|
79
79
|
const gateway = error instanceof GatewayError ? error : undefined;
|
|
80
|
+
// Elysia's built-in errors (NotFound, ValidationError, ParseError,
|
|
81
|
+
// InternalServerError) always carry a numeric `status` **and** a code.
|
|
82
|
+
// A raw runtime error can carry a `.code` on its own — Bun's fetch
|
|
83
|
+
// surfaces `TypeError` with `code: "ECONNRESET"` when an upstream socket
|
|
84
|
+
// dies mid-response — so a `code` alone is not evidence of an Elysia
|
|
85
|
+
// built-in. Requiring the numeric status keeps that raw error from
|
|
86
|
+
// leaking into the public envelope as a fake code/400; the dispatch loop
|
|
87
|
+
// already normalizes socket drops to `transport_unavailable`/502 before
|
|
88
|
+
// they reach here.
|
|
80
89
|
const builtin =
|
|
81
|
-
!gateway &&
|
|
90
|
+
!gateway &&
|
|
91
|
+
typeof error === "object" &&
|
|
92
|
+
error !== null &&
|
|
93
|
+
typeof (error as ElysiaBuiltinError).status === "number"
|
|
82
94
|
? (error as ElysiaBuiltinError)
|
|
83
95
|
: undefined;
|
|
84
96
|
const builtinStatus = typeof builtin?.status === "number" ? builtin.status : undefined;
|
|
@@ -12,7 +12,7 @@ import { resolveClientIdentity } from "../../security/ip-boundary";
|
|
|
12
12
|
import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
|
|
13
13
|
import { pushStructuredConsoleLog } from "../../observability/log-ring";
|
|
14
14
|
import { log } from "../../observability/logger";
|
|
15
|
-
import {
|
|
15
|
+
import { isProxyDispatchRoute } from "./body-policy";
|
|
16
16
|
import { isOpaqueBodyRoute } from "../contracts";
|
|
17
17
|
import { peerAddressUnavailableError } from "./guard-errors";
|
|
18
18
|
|
|
@@ -24,6 +24,24 @@ interface RequestApp {
|
|
|
24
24
|
): RequestApp;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Whether this request falls outside the canonical JSON-proxy pipeline the
|
|
29
|
+
* canonical-parse and route-preparation stages serve.
|
|
30
|
+
*
|
|
31
|
+
* Shared gate, applied in the exact order both middlewares historically
|
|
32
|
+
* checked: non-`/v1/` paths, GET/HEAD (which carry no body to parse), then
|
|
33
|
+
* the JSON route table with opaque-body routes excluded — `/v1/completions`
|
|
34
|
+
* needs no separate arm because it is already in the JSON routes table, and
|
|
35
|
+
* native routes (compact, System One, token count) read a body but are never
|
|
36
|
+
* parsed into a canonical request, their bodies being opaque by contract.
|
|
37
|
+
*/
|
|
38
|
+
function skipNonCanonicalProxyRequest(request: Request): boolean {
|
|
39
|
+
const path = fastPathname(request.url);
|
|
40
|
+
if (!path.startsWith("/v1/")) return true;
|
|
41
|
+
if (request.method === "GET" || request.method === "HEAD") return true;
|
|
42
|
+
return !isProxyDispatchRoute(path) || isOpaqueBodyRoute(path);
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
interface RequestContextDeps {
|
|
28
46
|
readonly stateStore: ProxyRequestStateStore;
|
|
29
47
|
readonly clock?: () => number;
|
|
@@ -141,15 +159,9 @@ export function createCanonicalRequestMiddleware(deps: {
|
|
|
141
159
|
}): Elysia {
|
|
142
160
|
return new Elysia()
|
|
143
161
|
.beforeHandle(async ({ request }) => {
|
|
144
|
-
const path = fastPathname(request.url);
|
|
145
|
-
if (!path.startsWith("/v1/")) return;
|
|
146
|
-
if (request.method === "GET" || request.method === "HEAD") return;
|
|
147
|
-
// `/v1/completions` needs no separate arm: it is already in
|
|
148
|
-
// the JSON routes table, so the body-policy predicate covers it.
|
|
149
|
-
// Native routes (compact, System One) read a body but are never parsed
|
|
150
|
-
// into a canonical request — their bodies are opaque by contract.
|
|
151
|
-
if (!isJsonProxyRoutePath(path) || isOpaqueBodyRoute(path)) return;
|
|
152
162
|
// Model discovery and other GET/multimodal routes must not go through canonical parsing.
|
|
163
|
+
if (skipNonCanonicalProxyRequest(request)) return;
|
|
164
|
+
const path = fastPathname(request.url);
|
|
153
165
|
const state = deps.stateStore.require(request);
|
|
154
166
|
// Single-read invariant: the ingress policy middleware already decoded the
|
|
155
167
|
// body once into `state.ingressBody`. Never re-read `request.body` here —
|
|
@@ -216,11 +228,7 @@ export function createProxyRoutePreparationMiddleware(deps: {
|
|
|
216
228
|
}): Elysia {
|
|
217
229
|
return new Elysia()
|
|
218
230
|
.beforeHandle(async ({ request }) => {
|
|
219
|
-
|
|
220
|
-
if (!path.startsWith("/v1/")) return;
|
|
221
|
-
if (request.method === "GET" || request.method === "HEAD") return;
|
|
222
|
-
const isJsonRoute = isJsonProxyRoutePath(path);
|
|
223
|
-
if (!isJsonRoute || isOpaqueBodyRoute(path)) return;
|
|
231
|
+
if (skipNonCanonicalProxyRequest(request)) return;
|
|
224
232
|
const state = deps.stateStore.require(request);
|
|
225
233
|
if (!state.authorization || !state.canonicalRequest)
|
|
226
234
|
throw new GatewayError("route_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served (request {requestId}). Resend the request. The bouncer stepped out — resend shortly 🚪");
|
|
@@ -5,7 +5,7 @@ import type { ApiKeyAdmissionService } from "../../security/admission/service";
|
|
|
5
5
|
import type { VisionHandoffCall, VisionHandoffResult } from "../vision-handoff";
|
|
6
6
|
import { isImageOrPlaceholderPart, lastImageCarryingUserTurnIndex } from "../vision-handoff";
|
|
7
7
|
import type { CallerIdentity } from "../../security/access-control";
|
|
8
|
-
import type { RouteCandidate as RouteCandidate, InMemoryRouteSnapshotService, RoutePlan } from "../routing/route-model";
|
|
8
|
+
import type { RouteCandidate as RouteCandidate, InMemoryRouteSnapshotService, RoutePlan, RouteSnapshot } from "../routing/route-model";
|
|
9
9
|
import type { RouteContinuationAffinity } from "../routing/route-model";
|
|
10
10
|
import { resolveAliasTarget, type RoutingEngine } from "../routing/router";
|
|
11
11
|
import { projectForRoute, routeCapabilitiesFor, deriveRequiredCapabilities } from "../../protocol/translation/capabilities";
|
|
@@ -80,7 +80,17 @@ function replaceImagePartsWithText(
|
|
|
80
80
|
}
|
|
81
81
|
const hasImage = message.content.some(isImageOrPlaceholderPart);
|
|
82
82
|
if (!hasImage) return message;
|
|
83
|
-
|
|
83
|
+
// Never delete images silently (proposal Option E): replace a dropped
|
|
84
|
+
// older image with a marker so the main model at least knows something
|
|
85
|
+
// was there, instead of the loss being invisible.
|
|
86
|
+
return {
|
|
87
|
+
...message,
|
|
88
|
+
content: message.content.flatMap((part) =>
|
|
89
|
+
isImageOrPlaceholderPart(part)
|
|
90
|
+
? [{ kind: "text" as const, text: "[image omitted]" }]
|
|
91
|
+
: [part],
|
|
92
|
+
),
|
|
93
|
+
};
|
|
84
94
|
});
|
|
85
95
|
const unchangedMessages = nextMessages.every(
|
|
86
96
|
(message, i) => message === request.messages[i],
|
|
@@ -350,6 +360,61 @@ export class ProxyRequestPreparer {
|
|
|
350
360
|
|
|
351
361
|
constructor(private readonly deps: ProxyRequestPreparerDeps) {}
|
|
352
362
|
|
|
363
|
+
/**
|
|
364
|
+
* The key's model-admission gate, shared by the canonical and native
|
|
365
|
+
* preparation paths: CLI-mapping eligibility → alias resolution → the
|
|
366
|
+
* model allowlist → the model-prefix gate, in that exact order. Rejects
|
|
367
|
+
* with the same `model_not_allowed` 403s (same code, status, message, and
|
|
368
|
+
* detail fields) on both paths, so a policy change here cannot silently
|
|
369
|
+
* split admission between the chat and native surfaces. Returns the state
|
|
370
|
+
* the gate computed that the caller still needs for routing: the resolved
|
|
371
|
+
* alias target and whether CLI mappings were eligible.
|
|
372
|
+
*/
|
|
373
|
+
#resolveAliasAndGateModel(
|
|
374
|
+
model: string,
|
|
375
|
+
authorization: ResolvedApiKey,
|
|
376
|
+
clientUserAgent: string | undefined,
|
|
377
|
+
snapshot: RouteSnapshot,
|
|
378
|
+
): { readonly resolvedTarget: string; readonly allowCliMappings: boolean } {
|
|
379
|
+
// Scope opts the key into the mapping table; the User-Agent decides whether
|
|
380
|
+
// *this* request may consume it. Without the UA gate, a Claude→DeepSeek
|
|
381
|
+
// remap would also rewrite a non-Claude caller that named `opus`.
|
|
382
|
+
const allowCliMappings = allowsCliToolMappings(authorization.scopes, {
|
|
383
|
+
...(clientUserAgent === undefined ? {} : { userAgent: clientUserAgent }),
|
|
384
|
+
});
|
|
385
|
+
// CLI source→target mappings are an explicit API-key capability. Ordinary
|
|
386
|
+
// tenant aliases remain available to every key; only selected keys may
|
|
387
|
+
// consume the CLI mapping table.
|
|
388
|
+
const resolvedTarget = resolveAliasTarget(
|
|
389
|
+
snapshot,
|
|
390
|
+
authorization.tenantId,
|
|
391
|
+
model,
|
|
392
|
+
allowCliMappings,
|
|
393
|
+
authorization.cliMappingOwnerId ?? authorization.id,
|
|
394
|
+
);
|
|
395
|
+
// Pass `model` as the requested name so an allowlisted alias / a CLI
|
|
396
|
+
// remapping can authorize the resolved target (see modelRejectionReason).
|
|
397
|
+
const allowedForKey = isModelAllowed(authorization.snapshot, resolvedTarget, undefined, model);
|
|
398
|
+
if (
|
|
399
|
+
authorization.modelPrefix &&
|
|
400
|
+
!model.startsWith(authorization.modelPrefix) &&
|
|
401
|
+
!resolvedTarget.startsWith(authorization.modelPrefix) &&
|
|
402
|
+
!allowedForKey
|
|
403
|
+
)
|
|
404
|
+
throw new GatewayError(
|
|
405
|
+
"model_not_allowed",
|
|
406
|
+
403,
|
|
407
|
+
`This key may only use models starting with '${authorization.modelPrefix}' (request {requestId}). Ask for one inside that prefix, or have an operator widen the key. The door's locked for this one — check the allowlist 🔒`,
|
|
408
|
+
{ model, required_prefix: authorization.modelPrefix },
|
|
409
|
+
);
|
|
410
|
+
if (!allowedForKey) {
|
|
411
|
+
throw new GatewayError("model_not_allowed", 403, "model is not allowed for this API key (request {requestId}). The door's locked for this one — check the allowlist 🔒", {
|
|
412
|
+
model,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return { resolvedTarget, allowCliMappings };
|
|
416
|
+
}
|
|
417
|
+
|
|
353
418
|
/** Injects the vision-handoff runner (called once from boot wiring). */
|
|
354
419
|
bindVisionHandoff(
|
|
355
420
|
runner: (input: VisionHandoffCall) => Promise<readonly VisionHandoffResult[]>,
|
|
@@ -391,47 +456,12 @@ export class ProxyRequestPreparer {
|
|
|
391
456
|
? initialRequest
|
|
392
457
|
: withThinkingSuffixIntent({ ...initialRequest, model: bareModel }, thinkingIntent);
|
|
393
458
|
const snapshot = await this.deps.snapshotService.getSnapshot();
|
|
394
|
-
|
|
395
|
-
// *this* request may consume it. Without the UA gate, a Claude→DeepSeek
|
|
396
|
-
// remap would also rewrite a non-Claude caller that named `opus`.
|
|
397
|
-
const allowCliMappings = allowsCliToolMappings(authorization.scopes, {
|
|
398
|
-
...(input.clientUserAgent === undefined ? {} : { userAgent: input.clientUserAgent }),
|
|
399
|
-
});
|
|
400
|
-
// CLI source→target mappings are an explicit API-key capability. Ordinary
|
|
401
|
-
// tenant aliases remain available to every key; only selected keys may
|
|
402
|
-
// consume the CLI mapping table.
|
|
403
|
-
const resolvedTarget = resolveAliasTarget(
|
|
404
|
-
snapshot,
|
|
405
|
-
authorization.tenantId,
|
|
406
|
-
request.model,
|
|
407
|
-
allowCliMappings,
|
|
408
|
-
authorization.cliMappingOwnerId ?? authorization.id,
|
|
409
|
-
);
|
|
410
|
-
// Pass `request.model` as the requested name so an allowlisted alias / a
|
|
411
|
-
// CLI remapping can authorize the resolved target (see modelRejectionReason).
|
|
412
|
-
const allowedForKey = isModelAllowed(
|
|
413
|
-
authorization.snapshot,
|
|
414
|
-
resolvedTarget,
|
|
415
|
-
undefined,
|
|
459
|
+
const { allowCliMappings } = this.#resolveAliasAndGateModel(
|
|
416
460
|
request.model,
|
|
461
|
+
authorization,
|
|
462
|
+
input.clientUserAgent,
|
|
463
|
+
snapshot,
|
|
417
464
|
);
|
|
418
|
-
if (
|
|
419
|
-
authorization.modelPrefix &&
|
|
420
|
-
!request.model.startsWith(authorization.modelPrefix) &&
|
|
421
|
-
!resolvedTarget.startsWith(authorization.modelPrefix) &&
|
|
422
|
-
!allowedForKey
|
|
423
|
-
)
|
|
424
|
-
throw new GatewayError(
|
|
425
|
-
"model_not_allowed",
|
|
426
|
-
403,
|
|
427
|
-
`This key may only use models starting with '${authorization.modelPrefix}' (request {requestId}). Ask for one inside that prefix, or have an operator widen the key. The door's locked for this one — check the allowlist 🔒`,
|
|
428
|
-
{ model: request.model, required_prefix: authorization.modelPrefix },
|
|
429
|
-
);
|
|
430
|
-
if (!allowedForKey) {
|
|
431
|
-
throw new GatewayError("model_not_allowed", 403, "model is not allowed for this API key (request {requestId}). The door's locked for this one — check the allowlist 🔒", {
|
|
432
|
-
model: request.model,
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
465
|
// An inline image that cannot survive the upstream is dropped here rather
|
|
436
466
|
// than dispatched: a provider rejects the whole request for one corrupt
|
|
437
467
|
// attachment, and the buddy family reports it with no field named, so the
|
|
@@ -474,6 +504,7 @@ export class ProxyRequestPreparer {
|
|
|
474
504
|
allowCliMappings,
|
|
475
505
|
authorization.cliMappingOwnerId ?? authorization.id,
|
|
476
506
|
continuationAffinity,
|
|
507
|
+
request.conversation_id ?? request.conversation?.conversation_id,
|
|
477
508
|
);
|
|
478
509
|
} catch (error) {
|
|
479
510
|
// `plan()` collapses "no candidate supports this capability" into
|
|
@@ -541,6 +572,7 @@ export class ProxyRequestPreparer {
|
|
|
541
572
|
allowCliMappings,
|
|
542
573
|
authorization.cliMappingOwnerId ?? authorization.id,
|
|
543
574
|
continuationAffinity,
|
|
575
|
+
request.conversation_id ?? request.conversation?.conversation_id,
|
|
544
576
|
);
|
|
545
577
|
plan = handoffPlan;
|
|
546
578
|
variantRequest = replaced;
|
|
@@ -725,42 +757,18 @@ export class ProxyRequestPreparer {
|
|
|
725
757
|
readonly clientUserAgent?: string;
|
|
726
758
|
readonly eligible: (candidate: RouteCandidate) => boolean;
|
|
727
759
|
readonly emptyMessage: string;
|
|
760
|
+
/** Session identity for sticky routing (conversation id when present). */
|
|
761
|
+
readonly conversationId?: string;
|
|
728
762
|
}): Promise<PreparedNativeRequest> {
|
|
729
763
|
if (input.signal?.aborted)
|
|
730
764
|
throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
|
|
731
765
|
const snapshot = await this.deps.snapshotService.getSnapshot();
|
|
732
|
-
const allowCliMappings =
|
|
733
|
-
...(input.clientUserAgent === undefined ? {} : { userAgent: input.clientUserAgent }),
|
|
734
|
-
});
|
|
735
|
-
const resolvedTarget = resolveAliasTarget(
|
|
736
|
-
snapshot,
|
|
737
|
-
input.authorization.tenantId,
|
|
738
|
-
input.model,
|
|
739
|
-
allowCliMappings,
|
|
740
|
-
input.authorization.cliMappingOwnerId ?? input.authorization.id,
|
|
741
|
-
);
|
|
742
|
-
const allowedForKey = isModelAllowed(
|
|
743
|
-
input.authorization.snapshot,
|
|
744
|
-
resolvedTarget,
|
|
745
|
-
undefined,
|
|
766
|
+
const { allowCliMappings } = this.#resolveAliasAndGateModel(
|
|
746
767
|
input.model,
|
|
768
|
+
input.authorization,
|
|
769
|
+
input.clientUserAgent,
|
|
770
|
+
snapshot,
|
|
747
771
|
);
|
|
748
|
-
if (
|
|
749
|
-
input.authorization.modelPrefix &&
|
|
750
|
-
!input.model.startsWith(input.authorization.modelPrefix) &&
|
|
751
|
-
!resolvedTarget.startsWith(input.authorization.modelPrefix) &&
|
|
752
|
-
!allowedForKey
|
|
753
|
-
)
|
|
754
|
-
throw new GatewayError(
|
|
755
|
-
"model_not_allowed",
|
|
756
|
-
403,
|
|
757
|
-
`This key may only use models starting with '${input.authorization.modelPrefix}' (request {requestId}). Ask for one inside that prefix, or have an operator widen the key. The door's locked for this one — check the allowlist 🔒`,
|
|
758
|
-
{ model: input.model, required_prefix: input.authorization.modelPrefix },
|
|
759
|
-
);
|
|
760
|
-
if (!allowedForKey)
|
|
761
|
-
throw new GatewayError("model_not_allowed", 403, "model is not allowed for this API key (request {requestId}). The door's locked for this one — check the allowlist 🔒", {
|
|
762
|
-
model: input.model,
|
|
763
|
-
});
|
|
764
772
|
const plan = await this.deps.routingEngine.plan(
|
|
765
773
|
input.model,
|
|
766
774
|
snapshot,
|
|
@@ -768,6 +776,8 @@ export class ProxyRequestPreparer {
|
|
|
768
776
|
undefined,
|
|
769
777
|
allowCliMappings,
|
|
770
778
|
input.authorization.cliMappingOwnerId ?? input.authorization.id,
|
|
779
|
+
undefined,
|
|
780
|
+
input.conversationId,
|
|
771
781
|
);
|
|
772
782
|
if (input.signal?.aborted)
|
|
773
783
|
throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
// Bounded LRU map: the one shared implementation for routing-layer caches.
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Evicts the oldest-inserted entry while `map.size > cap`. The shared
|
|
5
|
+
* insertion-order bound for caches whose read path must not refresh recency
|
|
6
|
+
* (unlike `BoundedLruMap`, whose `get` does). Eviction is one `keys().next()`
|
|
7
|
+
* and no bookkeeping arrays.
|
|
8
|
+
*/
|
|
9
|
+
export function evictOldestEntry<K>(map: Map<K, unknown>, cap: number): void {
|
|
10
|
+
while (map.size > cap) {
|
|
11
|
+
const oldest = map.keys().next().value;
|
|
12
|
+
if (oldest === undefined) break;
|
|
13
|
+
map.delete(oldest);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
/**
|
|
4
18
|
* A capacity-bounded LRU `Map`.
|
|
5
19
|
*
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Drain urgency and reserve scoring for candidate ordering.
|
|
2
|
+
//
|
|
3
|
+
// Ported from oh-my-pi's `windowRequiredDrain` and reserve semantics
|
|
4
|
+
// (`packages/ai/src/auth/usage-report.ts`, `policy.ts`): within the same rank
|
|
5
|
+
// group, an account whose remaining quota is about to expire unused should be
|
|
6
|
+
// burned first ("use it or lose it"), and an account parked in reserve (at or
|
|
7
|
+
// below the reserve percent) should yield to non-reserve siblings.
|
|
8
|
+
import {
|
|
9
|
+
totalRemainingPercent,
|
|
10
|
+
type ProviderQuotaWindow,
|
|
11
|
+
} from "../../providers/quota/quota-contracts";
|
|
12
|
+
|
|
13
|
+
/** Minimum time before reset that still counts as "some headroom" (1 minute). */
|
|
14
|
+
const MIN_REMAINING_MS = 60_000;
|
|
15
|
+
|
|
16
|
+
/** Usable fraction of a window: 0..1, with 0.5 neutral when unmeasured. */
|
|
17
|
+
function usedFraction(window: ProviderQuotaWindow): number {
|
|
18
|
+
const used = window.usedPercent;
|
|
19
|
+
if (typeof used === "number" && Number.isFinite(used)) {
|
|
20
|
+
return Math.min(1, Math.max(0, used / 100));
|
|
21
|
+
}
|
|
22
|
+
const remaining = window.remainingPercent;
|
|
23
|
+
if (typeof remaining === "number" && Number.isFinite(remaining)) {
|
|
24
|
+
return Math.min(1, Math.max(0, 1 - remaining / 100));
|
|
25
|
+
}
|
|
26
|
+
if (typeof window.used === "number" && Number.isFinite(window.used)) {
|
|
27
|
+
if (typeof window.limit === "number" && Number.isFinite(window.limit) && window.limit > 0) {
|
|
28
|
+
return Math.min(1, Math.max(0, window.used / window.limit));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (typeof window.remaining === "number" && Number.isFinite(window.remaining)) {
|
|
32
|
+
if (typeof window.limit === "number" && Number.isFinite(window.limit) && window.limit > 0) {
|
|
33
|
+
return Math.min(1, Math.max(0, 1 - window.remaining / window.limit));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return 0.5;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Reset instant (epoch ms) for a window, or undefined when it has no clock. */
|
|
40
|
+
function resetAtMs(window: ProviderQuotaWindow): number | undefined {
|
|
41
|
+
if (window.resetsAt === null || window.resetsAt === undefined) return undefined;
|
|
42
|
+
const at = Date.parse(window.resetsAt);
|
|
43
|
+
return Number.isFinite(at) ? at : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Drain score for the most-urgent relevant window: headroom fraction divided by
|
|
48
|
+
* remaining hours until reset. Higher means quota that is about to expire
|
|
49
|
+
* unused and should be burned first. Returns 0 when no window carries usable
|
|
50
|
+
* evidence (the candidate is neutral, never penalized).
|
|
51
|
+
*/
|
|
52
|
+
export function drainScore(
|
|
53
|
+
windows: readonly ProviderQuotaWindow[] | undefined,
|
|
54
|
+
nowMs = Date.now(),
|
|
55
|
+
): number {
|
|
56
|
+
if (windows === undefined || windows.length === 0) return 0;
|
|
57
|
+
let best = 0;
|
|
58
|
+
for (const window of windows) {
|
|
59
|
+
const headroom = 1 - usedFraction(window);
|
|
60
|
+
if (headroom <= 0) continue;
|
|
61
|
+
const resetAt = resetAtMs(window);
|
|
62
|
+
const durationMs = windowAccountingDurationMs(window);
|
|
63
|
+
let remainingMs = resetAt === undefined ? (durationMs ?? MIN_REMAINING_MS) : resetAt - nowMs;
|
|
64
|
+
if (durationMs !== undefined && durationMs > 0) {
|
|
65
|
+
remainingMs = Math.min(remainingMs, durationMs);
|
|
66
|
+
}
|
|
67
|
+
const remainingHours = Math.max(remainingMs, MIN_REMAINING_MS) / (60 * 60 * 1000);
|
|
68
|
+
best = Math.max(best, headroom / remainingHours);
|
|
69
|
+
}
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function windowAccountingDurationMs(window: ProviderQuotaWindow): number | undefined {
|
|
74
|
+
// Providers state window durations in the kind label (e.g. "5h", "7d") when
|
|
75
|
+
// they do not carry an explicit duration field. Parse the common spellings;
|
|
76
|
+
// an unparseable label contributes no clock.
|
|
77
|
+
const text = `${window.kind} ${window.label}`.toLowerCase();
|
|
78
|
+
const m = /(\d+(?:\.\d+)?)\s*(h|hour|hr|d|day|m|min|minute|s|sec|second)\b/.exec(text);
|
|
79
|
+
if (!m) return undefined;
|
|
80
|
+
const value = Number(m[1]);
|
|
81
|
+
if (!Number.isFinite(value)) return undefined;
|
|
82
|
+
const unit = m[2];
|
|
83
|
+
if (unit === "h" || unit === "hour" || unit === "hr") return value * 60 * 60 * 1000;
|
|
84
|
+
if (unit === "d" || unit === "day") return value * 24 * 60 * 60 * 1000;
|
|
85
|
+
if (unit === "m" || unit === "min" || unit === "minute") return value * 60 * 1000;
|
|
86
|
+
if (unit === "s" || unit === "sec" || unit === "second") return value * 1000;
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The account's remaining fraction (0..1) across its pooled credit and
|
|
92
|
+
* independent windows, or undefined when nothing is measured. Mirrors
|
|
93
|
+
* `totalRemainingPercent` (the router's existing aggregate) but normalized to a
|
|
94
|
+
* 0..1 fraction.
|
|
95
|
+
*/
|
|
96
|
+
export function remainingFraction(
|
|
97
|
+
windows: readonly ProviderQuotaWindow[] | undefined,
|
|
98
|
+
): number | undefined {
|
|
99
|
+
if (windows === undefined || windows.length === 0) return undefined;
|
|
100
|
+
const percent = totalRemainingPercent(windows);
|
|
101
|
+
if (percent === null) return undefined;
|
|
102
|
+
return Math.min(1, Math.max(0, percent / 100));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** True when the account's remaining fraction is at or below the reserve percent. */
|
|
106
|
+
export function isInReserve(
|
|
107
|
+
windows: readonly ProviderQuotaWindow[] | undefined,
|
|
108
|
+
reservePct: number,
|
|
109
|
+
): boolean {
|
|
110
|
+
if (reservePct <= 0) return false;
|
|
111
|
+
const fraction = remainingFraction(windows);
|
|
112
|
+
if (fraction === undefined) return false;
|
|
113
|
+
return fraction * 100 <= reservePct;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* True when a reserved account has healed above the reclaim threshold and may be
|
|
118
|
+
* reclaimed to full priority.
|
|
119
|
+
*/
|
|
120
|
+
export function isReclaimReady(
|
|
121
|
+
windows: readonly ProviderQuotaWindow[] | undefined,
|
|
122
|
+
reservePct: number,
|
|
123
|
+
reclaimAbovePct: number,
|
|
124
|
+
): boolean {
|
|
125
|
+
if (reservePct <= 0 || reclaimAbovePct <= reservePct) return false;
|
|
126
|
+
const fraction = remainingFraction(windows);
|
|
127
|
+
if (fraction === undefined) return false;
|
|
128
|
+
return fraction * 100 >= reclaimAbovePct;
|
|
129
|
+
}
|
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
* costs one refused request that the cooldown machinery already handles; a
|
|
18
18
|
* wrong paid match silently blackholes a model.
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The marker set that proves a plan string is free. Exported so every
|
|
22
|
+
* free-plan consumer — the routing plan gate here and the cooldown-tier
|
|
23
|
+
* derivation in `account-tier.ts` — reads the same list, so the two cannot
|
|
24
|
+
* drift apart about an account.
|
|
25
|
+
*/
|
|
26
|
+
export const FREE_PLAN_MARKERS: readonly string[] = ["free"];
|
|
21
27
|
|
|
22
28
|
export function isFreePlanAccount(plan: string | null | undefined): boolean {
|
|
23
29
|
if (plan === null || plan === undefined) return false;
|
|
@@ -36,3 +36,63 @@ const MAX_ROUND_ROBIN_ENTRIES = 1000;
|
|
|
36
36
|
export function createRoundRobinMap(): BoundedLruMap<string, RoundRobinState> {
|
|
37
37
|
return new BoundedLruMap<string, RoundRobinState>(MAX_ROUND_ROBIN_ENTRIES);
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** One session's sticky account: which provider account last served it, and when. */
|
|
41
|
+
export interface StickySession {
|
|
42
|
+
readonly accountId: string;
|
|
43
|
+
readonly lastUsedAtMs: number;
|
|
44
|
+
/** Set only by an explicit operator pin; automatic warm stickiness leaves it absent. */
|
|
45
|
+
readonly explicit?: true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Session → account stickiness per provider, bounded by an LRU. Used by the
|
|
50
|
+
* router to prefer the account that last served a session within the warm
|
|
51
|
+
* window — prompt-cache warmth is worth more than perfect load balance. A
|
|
52
|
+
* session id that is absent, evicted, or past the warm window simply carries
|
|
53
|
+
* no preference.
|
|
54
|
+
*/
|
|
55
|
+
export class StickySessionStore {
|
|
56
|
+
private readonly byProvider = new Map<string, BoundedLruMap<string, StickySession>>();
|
|
57
|
+
|
|
58
|
+
constructor(private readonly maxSessionsPerProvider: number) {}
|
|
59
|
+
|
|
60
|
+
private mapFor(providerId: string): BoundedLruMap<string, StickySession> {
|
|
61
|
+
let map = this.byProvider.get(providerId);
|
|
62
|
+
if (map === undefined) {
|
|
63
|
+
map = new BoundedLruMap<string, StickySession>(this.maxSessionsPerProvider);
|
|
64
|
+
this.byProvider.set(providerId, map);
|
|
65
|
+
}
|
|
66
|
+
return map;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The account last pinned to this session, or undefined. */
|
|
70
|
+
get(providerId: string, sessionId: string | undefined): StickySession | undefined {
|
|
71
|
+
if (sessionId === undefined) return undefined;
|
|
72
|
+
return this.mapFor(providerId).get(sessionId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Record that `accountId` served `sessionId` now (or pin it explicitly). */
|
|
76
|
+
set(providerId: string, sessionId: string | undefined, accountId: string, explicit = false): void {
|
|
77
|
+
if (sessionId === undefined) return;
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
this.mapFor(providerId).set(sessionId, {
|
|
80
|
+
accountId,
|
|
81
|
+
lastUsedAtMs: now,
|
|
82
|
+
...(explicit ? { explicit: true as const } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Drop one session's stickiness (e.g. after the account is invalidated). */
|
|
87
|
+
clear(providerId: string, sessionId: string | undefined): void {
|
|
88
|
+
if (sessionId === undefined) return;
|
|
89
|
+
this.mapFor(providerId).delete(sessionId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Total retained sticky entries across providers (for metrics/tests). */
|
|
93
|
+
size(): number {
|
|
94
|
+
let total = 0;
|
|
95
|
+
for (const map of this.byProvider.values()) total += map.size;
|
|
96
|
+
return total;
|
|
97
|
+
}
|
|
98
|
+
}
|