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
|
@@ -490,6 +490,31 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
490
490
|
// rows are served by `/v1/search`, never by the canonical chat pipeline.
|
|
491
491
|
{ id: "exa", displayName: "Exa", baseUrl: "https://mcp.exa.ai", serviceKinds: ["websearch"], requiresAccount: false, credentialHint: "No API key. Search goes through the public MCP." },
|
|
492
492
|
{ id: "parallel", displayName: "Parallel", baseUrl: "https://search.parallel.ai", serviceKinds: ["websearch"], requiresAccount: false, credentialUrl: "https://platform.parallel.ai/settings?tab=api-keys", credentialHint: "Optional. Leave empty for the public MCP, or paste an API key for the higher-limit search API." },
|
|
493
|
+
{
|
|
494
|
+
// Freebuff (Codebuff Desktop). OAuth code flow → bearer token; every
|
|
495
|
+
// dispatch carries the desktop identity group + Ed25519 device signature
|
|
496
|
+
// (see documentation/research/freebuff/). The chat surface is
|
|
497
|
+
// OpenAI-compatible on `/api/v1/chat/completions` but requires a stateful
|
|
498
|
+
// session admission + agent-runs preflight — handled by the custom adapter.
|
|
499
|
+
id: "freebuff",
|
|
500
|
+
displayName: "Freebuff",
|
|
501
|
+
baseUrl: "https://www.codebuff.com",
|
|
502
|
+
hasAdapterUserAgent: true,
|
|
503
|
+
credentialUrl: "https://www.codebuff.com/login",
|
|
504
|
+
credentialHint: "Sign in with your Freebuff account (Desktop/CLI code flow).",
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
// Zro (moonmath). Fast, private, multi-region inference for open models.
|
|
508
|
+
// Claude Code connects via the Messages wire with a plain bearer key; the
|
|
509
|
+
// provider's own Claude Code docs configure `ANTHROPIC_BASE_URL` +
|
|
510
|
+
// `ANTHROPIC_AUTH_TOKEN`. No quota endpoint exists — entitlement is the
|
|
511
|
+
// `/v1/models` list itself (a key only sees the models it may call).
|
|
512
|
+
id: "zro",
|
|
513
|
+
displayName: "Zro",
|
|
514
|
+
baseUrl: "https://zro.moonmath.ai",
|
|
515
|
+
credentialUrl: "https://zro.moonmath.ai/docs/key-controls",
|
|
516
|
+
credentialHint: "Create or copy an API key (sk-...) from the Zro key controls page (docs/key-controls).",
|
|
517
|
+
},
|
|
493
518
|
] as const;
|
|
494
519
|
|
|
495
520
|
/**
|
|
@@ -617,6 +642,11 @@ const ADAPTER_USER_AGENT_PROVIDER_IDS: ReadonlySet<string> = new Set(
|
|
|
617
642
|
BUNDLED_PROVIDER_METADATA.filter((provider) => provider.hasAdapterUserAgent).map((provider) => provider.id),
|
|
618
643
|
);
|
|
619
644
|
|
|
645
|
+
/** Bundled metadata keyed by provider id for O(1) lookups. */
|
|
646
|
+
const BUNDLED_PROVIDER_BY_ID: ReadonlyMap<BundledProviderId, BundledProviderMetadata> = new Map(
|
|
647
|
+
BUNDLED_PROVIDER_METADATA.map((definition) => [definition.id, definition]),
|
|
648
|
+
);
|
|
649
|
+
|
|
620
650
|
/** Whether the bundled adapter provides its own upstream User-Agent. */
|
|
621
651
|
export function providerHasAdapterUserAgent(providerId: string): boolean {
|
|
622
652
|
return ADAPTER_USER_AGENT_PROVIDER_IDS.has(providerId);
|
|
@@ -628,7 +658,7 @@ export function providerHasAdapterUserAgent(providerId: string): boolean {
|
|
|
628
658
|
* minted inside a device/browser flow with no dedicated key page).
|
|
629
659
|
*/
|
|
630
660
|
export function providerCredentialUrl(providerId: string): string | undefined {
|
|
631
|
-
return
|
|
661
|
+
return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.credentialUrl;
|
|
632
662
|
}
|
|
633
663
|
|
|
634
664
|
/**
|
|
@@ -636,18 +666,18 @@ export function providerCredentialUrl(providerId: string): string | undefined {
|
|
|
636
666
|
* `undefined` where the link alone is self-explanatory.
|
|
637
667
|
*/
|
|
638
668
|
export function providerCredentialHint(providerId: string): string | undefined {
|
|
639
|
-
return
|
|
669
|
+
return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.credentialHint;
|
|
640
670
|
}
|
|
641
671
|
|
|
642
672
|
/** Presentation-only limitation for the provider detail page. */
|
|
643
673
|
export function providerOperatorNotice(providerId: string): string | undefined {
|
|
644
|
-
return
|
|
674
|
+
return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.operatorNotice;
|
|
645
675
|
}
|
|
646
676
|
|
|
647
677
|
/** Canonical builtin display name; falls back to the provider id for unknowns. */
|
|
648
678
|
export function providerDisplayName(providerId: string): string {
|
|
649
679
|
return (
|
|
650
|
-
|
|
680
|
+
BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.displayName ??
|
|
651
681
|
providerId
|
|
652
682
|
);
|
|
653
683
|
}
|
|
@@ -679,7 +709,7 @@ export function providerDisplayName(providerId: string): string {
|
|
|
679
709
|
* gateway guessing it will not.
|
|
680
710
|
*/
|
|
681
711
|
export function providerServiceKinds(providerId: string): readonly ServiceKind[] {
|
|
682
|
-
const declared =
|
|
712
|
+
const declared = BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)
|
|
683
713
|
?.serviceKinds;
|
|
684
714
|
// Bundled providers are explicit: they say which surfaces they serve, and a
|
|
685
715
|
// bundled provider without the declaration serves LLM only.
|
|
@@ -708,7 +738,7 @@ export function customProviderServiceKinds(
|
|
|
708
738
|
|
|
709
739
|
/** Canonical builtin base URL used by adapters, discovery, and quota clients. */
|
|
710
740
|
export function providerBaseUrl(providerId: string): string {
|
|
711
|
-
const entry =
|
|
741
|
+
const entry = BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId);
|
|
712
742
|
if (!entry) throw new Error(`Unknown builtin provider "${providerId}"`);
|
|
713
743
|
return entry.baseUrl.replace(/\/+$/, "");
|
|
714
744
|
}
|
|
@@ -744,7 +774,7 @@ export const PROVIDER_COMPATIBILITY_PROFILES: Record<string, CompatibilityProfil
|
|
|
744
774
|
|
|
745
775
|
/** Default wire family for a builtin provider. */
|
|
746
776
|
export function providerDefaultWireFamily(providerId: string): WireFamily {
|
|
747
|
-
return
|
|
777
|
+
return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.wireFamilyDefault ?? "chat";
|
|
748
778
|
}
|
|
749
779
|
|
|
750
780
|
/**
|
|
@@ -754,12 +784,12 @@ export function providerDefaultWireFamily(providerId: string): WireFamily {
|
|
|
754
784
|
* OpenAI-compatible adapter.
|
|
755
785
|
*/
|
|
756
786
|
export function providerUsesBespokeWire(providerId: string): boolean {
|
|
757
|
-
return
|
|
787
|
+
return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.bespokeWire ?? false;
|
|
758
788
|
}
|
|
759
789
|
|
|
760
790
|
/** Whether a builtin provider requires a persisted account row. */
|
|
761
791
|
export function providerRequiresAccount(providerId: string): boolean {
|
|
762
|
-
return
|
|
792
|
+
return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.requiresAccount ?? true;
|
|
763
793
|
}
|
|
764
794
|
|
|
765
795
|
/**
|
|
@@ -769,7 +799,7 @@ export function providerRequiresAccount(providerId: string): boolean {
|
|
|
769
799
|
*/
|
|
770
800
|
export function providerJwtVerification(providerId: string): ProviderJwtVerification {
|
|
771
801
|
return (
|
|
772
|
-
|
|
802
|
+
BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.jwtVerification ?? {}
|
|
773
803
|
);
|
|
774
804
|
}
|
|
775
805
|
|
|
@@ -16,7 +16,21 @@ import { resolveDialect } from "../protocol/translation/dialect/resolver";
|
|
|
16
16
|
import type { CanonicalRequest } from "../protocol/canonical-model";
|
|
17
17
|
import type { ProviderDispatchContext } from "./provider-contracts";
|
|
18
18
|
import { providerBaseUrl, providerRateLimitIsIpScoped } from "./provider-metadata";
|
|
19
|
-
import type { ProviderDispatchTarget, ProviderId } from "./provider-contracts";
|
|
19
|
+
import type { ProviderDispatchTarget, ProviderId, ValidatedOutboundFetch } from "./provider-contracts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Probe fetch wrapper: merges `Request`-carried headers with per-call init
|
|
23
|
+
* headers before delegating to the validated fetch. Required wherever the
|
|
24
|
+
* fetch is handed to adapters as `outbound_fetch`, because adapters may call
|
|
25
|
+
* it with a `Request` whose headers would otherwise be dropped.
|
|
26
|
+
*/
|
|
27
|
+
export function createProbeFetch(fetcher: ValidatedOutboundFetch): ValidatedOutboundFetch {
|
|
28
|
+
return (input, init) => {
|
|
29
|
+
const headers = new Headers(input instanceof Request ? input.headers : undefined);
|
|
30
|
+
new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
|
|
31
|
+
return fetcher(input, { ...init, headers });
|
|
32
|
+
};
|
|
33
|
+
}
|
|
20
34
|
|
|
21
35
|
|
|
22
36
|
/**
|
|
@@ -58,6 +58,36 @@ export interface ProviderAuthentication {
|
|
|
58
58
|
readonly refresher?: OAuthTokenRefresher;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Provider implementation fields composed with canonical metadata. */
|
|
62
|
+
export type ProviderModuleCapabilities = Partial<Omit<ProviderModule, "loadAdapter">> &
|
|
63
|
+
Pick<ProviderModule, "loadAdapter">;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a lazy authentication loader from a dynamic import and client export.
|
|
67
|
+
* Most OAuth clients also refresh their own tokens.
|
|
68
|
+
*/
|
|
69
|
+
export function oauthCapability<M, K extends keyof M>(
|
|
70
|
+
loader: () => Promise<M>,
|
|
71
|
+
exportName: K,
|
|
72
|
+
options: { readonly withRefresher?: boolean } = {},
|
|
73
|
+
): () => Promise<ProviderAuthentication> {
|
|
74
|
+
return async () => {
|
|
75
|
+
const module = await loader();
|
|
76
|
+
const client = (module as Record<string, unknown>)[exportName as string] as OAuthLoginClient & { refresh: unknown };
|
|
77
|
+
return options.withRefresher
|
|
78
|
+
? ({ client, refresher: client } as ProviderAuthentication)
|
|
79
|
+
: ({ client } as ProviderAuthentication);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Creates a lazy quota collector loader from a dynamic import and export name. */
|
|
84
|
+
export function quotaCapability<M, K extends keyof M>(
|
|
85
|
+
loader: () => Promise<M>,
|
|
86
|
+
exportName: K,
|
|
87
|
+
): () => Promise<QuotaFetcher> {
|
|
88
|
+
return () => loader().then((module) => (module as Record<string, unknown>)[exportName as string] as QuotaFetcher);
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
/**
|
|
62
92
|
* Derives a registry registration from a fully populated provider module.
|
|
63
93
|
* Keeps the registration shape in one place so the default registry and any
|
|
@@ -95,12 +125,6 @@ function assertAdapterIdentity(expected: ProviderId, adapter: ProviderAdapter):
|
|
|
95
125
|
});
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
/** Result of loading the configured provider registrations. */
|
|
99
|
-
export interface ProviderRegistrySnapshot {
|
|
100
|
-
readonly revision: number;
|
|
101
|
-
readonly adapters: ReadonlyMap<ProviderId, ProviderAdapter>;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
128
|
/**
|
|
105
129
|
* Single provider-registration authority. Every registration loads on
|
|
106
130
|
* demand through its own loader; registrations that are not added are
|
|
@@ -116,7 +140,6 @@ export class ProviderRegistry {
|
|
|
116
140
|
#quotaLoading = new Map<ProviderId, Promise<QuotaFetcher>>();
|
|
117
141
|
#modelDiscovery = new Map<ProviderId, ProviderModelDiscovery>();
|
|
118
142
|
#modelDiscoveryLoading = new Map<ProviderId, Promise<ProviderModelDiscovery>>();
|
|
119
|
-
#revision = 0;
|
|
120
143
|
|
|
121
144
|
/** Adds one registration and rejects duplicate canonical IDs. */
|
|
122
145
|
register(registration: ProviderRegistration): void {
|
|
@@ -132,7 +155,6 @@ export class ProviderRegistry {
|
|
|
132
155
|
});
|
|
133
156
|
}
|
|
134
157
|
this.#registrations.set(providerId, registration);
|
|
135
|
-
this.#revision += 1;
|
|
136
158
|
}
|
|
137
159
|
/** Removes one registration and any cached adapter/auth/quota/discovery state. */
|
|
138
160
|
unregister(providerId: string): boolean {
|
|
@@ -142,7 +164,6 @@ export class ProviderRegistry {
|
|
|
142
164
|
this.#authentication.delete(normalized);
|
|
143
165
|
this.#quotaCollectors.delete(normalized);
|
|
144
166
|
this.#modelDiscovery.delete(normalized);
|
|
145
|
-
if (removed) this.#revision += 1;
|
|
146
167
|
return removed;
|
|
147
168
|
}
|
|
148
169
|
|
|
@@ -170,28 +191,25 @@ export class ProviderRegistry {
|
|
|
170
191
|
this.#authentication.delete(providerId);
|
|
171
192
|
this.#quotaCollectors.delete(providerId);
|
|
172
193
|
this.#modelDiscovery.delete(providerId);
|
|
173
|
-
this.#revision += 1;
|
|
174
194
|
return replaced;
|
|
175
195
|
}
|
|
176
196
|
|
|
177
|
-
/** Loads one registered provider, or `undefined` when it is not registered. */
|
|
178
|
-
async loadOne(providerId: string): Promise<ProviderAdapter | undefined> {
|
|
179
|
-
const normalized = parseProviderId(providerId);
|
|
180
|
-
const registration = this.#registrations.get(normalized);
|
|
181
|
-
if (registration === undefined) return undefined;
|
|
182
|
-
return this.#loadRegistration(normalized, registration);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
197
|
/**
|
|
186
198
|
* Resolves one adapter on demand, returning the cached instance when it has
|
|
187
|
-
* already loaded
|
|
188
|
-
*
|
|
189
|
-
*
|
|
199
|
+
* already loaded — unless `force` is set, which rebuilds the adapter from
|
|
200
|
+
* the registration (single-flight, and the fresh instance replaces the
|
|
201
|
+
* cache). This never evaluates a provider's module graph until that provider
|
|
202
|
+
* is actually dispatched to, so heavy bespoke adapters (protobuf-based
|
|
203
|
+
* providers) stay out of the startup path. `force` is used by the probe
|
|
204
|
+
* path, which wants the adapter rebuilt against the latest registration
|
|
205
|
+
* rather than a cached instance.
|
|
190
206
|
*/
|
|
191
|
-
async resolve(providerId: string): Promise<ProviderAdapter | undefined> {
|
|
207
|
+
async resolve(providerId: string, opts: { readonly force?: boolean } = {}): Promise<ProviderAdapter | undefined> {
|
|
192
208
|
const normalized = parseProviderId(providerId);
|
|
193
|
-
|
|
194
|
-
|
|
209
|
+
if (!opts.force) {
|
|
210
|
+
const cached = this.#loaded.get(normalized);
|
|
211
|
+
if (cached !== undefined) return cached;
|
|
212
|
+
}
|
|
195
213
|
const registration = this.#registrations.get(normalized);
|
|
196
214
|
if (registration === undefined) return undefined;
|
|
197
215
|
return this.#loadRegistration(normalized, registration);
|
|
@@ -306,7 +324,6 @@ export class ProviderRegistry {
|
|
|
306
324
|
metrics.proxy_provider_adapter_load_ms.observe(durationMs);
|
|
307
325
|
trackAdapterLoad(providerId, durationMs);
|
|
308
326
|
this.#loaded.set(providerId, adapter);
|
|
309
|
-
this.#revision += 1;
|
|
310
327
|
return adapter;
|
|
311
328
|
})().finally(() => {
|
|
312
329
|
this.#loading.delete(providerId);
|
|
@@ -315,14 +332,6 @@ export class ProviderRegistry {
|
|
|
315
332
|
return task;
|
|
316
333
|
}
|
|
317
334
|
|
|
318
|
-
/** Returns the immutable registry snapshot; disabled optional providers are absent. */
|
|
319
|
-
snapshot(): ProviderRegistrySnapshot {
|
|
320
|
-
return {
|
|
321
|
-
revision: this.#revision,
|
|
322
|
-
adapters: new Map(this.#loaded),
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
335
|
/** Returns registration metadata without loading optional code. */
|
|
327
336
|
registrations(): readonly ProviderRegistration[] {
|
|
328
337
|
return [...this.#registrations.values()];
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { ProviderQuotaWindow } from "./quota-contracts";
|
|
12
12
|
import { isoDate, number, percentWindow, record, text } from "./quota-contracts";
|
|
13
|
+
import { resolveWindowEmission } from "./quota-window-parser";
|
|
13
14
|
|
|
14
15
|
/** Where one provider's limit entries and their fields live. */
|
|
15
16
|
export interface LimitWindowShape {
|
|
@@ -77,14 +78,24 @@ export function parseLimitWindows(
|
|
|
77
78
|
const kind = text(readFirst(limit, shape.kindPaths)) ?? `window-${index + 1}`;
|
|
78
79
|
const percent = number(readFirst(limit, shape.percentPaths));
|
|
79
80
|
const reset = isoDate(readFirst(limit, shape.resetPaths));
|
|
80
|
-
|
|
81
|
+
const rawUsed = shape.usedPaths ? number(readFirst(limit, shape.usedPaths)) : null;
|
|
81
82
|
const limitValue = shape.limitPaths
|
|
82
83
|
? number(readFirst(limit, shape.limitPaths))
|
|
83
84
|
: null;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
null
|
|
85
|
+
const { emit } = resolveWindowEmission({
|
|
86
|
+
explicitPercent: null,
|
|
87
|
+
usedPercent: percent,
|
|
88
|
+
remainingPercent: null,
|
|
89
|
+
used: rawUsed,
|
|
90
|
+
limit: limitValue,
|
|
91
|
+
reset,
|
|
92
|
+
deriveFromUsedLimit: false,
|
|
93
|
+
// The array walker has always emitted a reset-only window (percent null,
|
|
94
|
+
// reset present); only an entry with neither is dropped.
|
|
95
|
+
emitWithoutPercent: true,
|
|
96
|
+
});
|
|
97
|
+
if (!emit) continue;
|
|
98
|
+
const usedValue = rawUsed ?? shape.deriveUsed?.(limit, percent, limitValue) ?? null;
|
|
88
99
|
const label =
|
|
89
100
|
shape.labelFor?.(kind, limit) ?? shape.fallbackLabel?.(kind) ?? humanize(kind);
|
|
90
101
|
windows.push(percentWindow(kind, label, percent, reset, usedValue, limitValue, "independent"));
|
|
@@ -3,7 +3,7 @@ import { cleanError, unsupportedQuota } from "./quota-contracts";
|
|
|
3
3
|
import type { ProviderRegistry } from "../provider-registry";
|
|
4
4
|
import { parseProviderId } from "../provider-contracts";
|
|
5
5
|
import { providerBaseUrl } from "../provider-metadata";
|
|
6
|
-
import { createProbeFetch } from "../
|
|
6
|
+
import { createProbeFetch } from "../provider-probe";
|
|
7
7
|
/**
|
|
8
8
|
* Verifies an API key against the provider's OpenAI-compatible `/models`
|
|
9
9
|
* endpoint. For providers that expose no billing/quota surface, a reachable
|
|
@@ -98,6 +98,75 @@ function findReset(root: unknown, mapping: QuotaWindowMapping): string | null {
|
|
|
98
98
|
return mapping.resetFallback ?? null;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The raw per-window candidates the shared emission core resolves.
|
|
103
|
+
*
|
|
104
|
+
* Both quota walkers (`parseQuotaWindows` and `parseLimitWindows`) feed their
|
|
105
|
+
* independently-read fields into one resolution so the percent fallback chain
|
|
106
|
+
* and the skip rule cannot drift between them.
|
|
107
|
+
*/
|
|
108
|
+
export interface WindowFieldCandidates {
|
|
109
|
+
/** Provider-preferred percent (e.g. a `derivePercent` result). */
|
|
110
|
+
readonly explicitPercent: number | null;
|
|
111
|
+
/** Raw used-percent read from the payload. */
|
|
112
|
+
readonly usedPercent: number | null;
|
|
113
|
+
/** Raw remaining-percent read from the payload. */
|
|
114
|
+
readonly remainingPercent: number | null;
|
|
115
|
+
/** Absolute used value. */
|
|
116
|
+
readonly used: number | null;
|
|
117
|
+
/** Absolute limit value. */
|
|
118
|
+
readonly limit: number | null;
|
|
119
|
+
/** Reset timestamp; required to emit a reset-only window. */
|
|
120
|
+
readonly reset: string | null;
|
|
121
|
+
/**
|
|
122
|
+
* Whether percent may be derived from absolute used/limit when no percentage
|
|
123
|
+
* was reported. Only `parseQuotaWindows` does this — `parseLimitWindows`
|
|
124
|
+
* reports percentages and resets the upstream gave it, never ones it
|
|
125
|
+
* invented, so Cline entries stay reset-only instead of gaining a derived
|
|
126
|
+
* percent.
|
|
127
|
+
*/
|
|
128
|
+
readonly deriveFromUsedLimit: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* When true, a reset-only window (percent null, reset present) is still
|
|
131
|
+
* emitted. When false/absent, an entry with neither percent nor reset is
|
|
132
|
+
* dropped — never rendered as 0% — and a reset-only window is dropped too.
|
|
133
|
+
*/
|
|
134
|
+
readonly emitWithoutPercent?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Shared per-window field resolution: the percent fallback chain plus the
|
|
139
|
+
* "emit or skip" decision.
|
|
140
|
+
*
|
|
141
|
+
* Percent resolution order is exact: explicit percent, then used-percent, then
|
|
142
|
+
* remaining-percent inverted (100 − remaining), then derived from absolute
|
|
143
|
+
* used/limit (only when the limit is positive and `deriveFromUsedLimit` is
|
|
144
|
+
* set). The result is never coerced to 0: a missing percentage stays `null`,
|
|
145
|
+
* so a window can be omitted or rendered as a countdown depending on
|
|
146
|
+
* `emitWithoutPercent`.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveWindowEmission(candidates: WindowFieldCandidates): {
|
|
149
|
+
readonly emit: boolean;
|
|
150
|
+
readonly percent: number | null;
|
|
151
|
+
} {
|
|
152
|
+
let percent = candidates.explicitPercent;
|
|
153
|
+
if (percent === null && candidates.usedPercent !== null) percent = candidates.usedPercent;
|
|
154
|
+
if (percent === null && candidates.remainingPercent !== null) {
|
|
155
|
+
percent = Math.max(0, 100 - candidates.remainingPercent);
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
percent === null &&
|
|
159
|
+
candidates.deriveFromUsedLimit &&
|
|
160
|
+
candidates.used !== null &&
|
|
161
|
+
candidates.limit !== null &&
|
|
162
|
+
candidates.limit > 0
|
|
163
|
+
) {
|
|
164
|
+
percent = (candidates.used / candidates.limit) * 100;
|
|
165
|
+
}
|
|
166
|
+
const emit = percent !== null || (candidates.emitWithoutPercent === true && candidates.reset !== null);
|
|
167
|
+
return { emit, percent };
|
|
168
|
+
}
|
|
169
|
+
|
|
101
170
|
/** Parses a generic quota payload using the provided window mappings. */
|
|
102
171
|
export function parseQuotaWindows(
|
|
103
172
|
body: unknown,
|
|
@@ -110,27 +179,30 @@ export function parseQuotaWindows(
|
|
|
110
179
|
for (const mapping of windowMappings) {
|
|
111
180
|
const used = findNumber(payload, mapping.usedPaths);
|
|
112
181
|
const limit = findNumber(payload, mapping.limitPaths);
|
|
182
|
+
const reset = findReset(payload, mapping);
|
|
113
183
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
if (percent !== null && mapping.valueMultiplier !== undefined && mapping.valueMultiplier !== 0) {
|
|
127
|
-
percent = Math.min(100, Math.max(0, percent * mapping.valueMultiplier));
|
|
128
|
-
}
|
|
184
|
+
const { emit, percent } = resolveWindowEmission({
|
|
185
|
+
explicitPercent: mapping.derivePercent ? mapping.derivePercent(payload) : null,
|
|
186
|
+
usedPercent: findNumber(payload, mapping.usedPercentPaths),
|
|
187
|
+
remainingPercent: findNumber(payload, mapping.remainingPercentPaths),
|
|
188
|
+
used,
|
|
189
|
+
limit,
|
|
190
|
+
reset,
|
|
191
|
+
deriveFromUsedLimit: true,
|
|
192
|
+
...(mapping.emitWithoutPercent === undefined ? {} : { emitWithoutPercent: mapping.emitWithoutPercent }),
|
|
193
|
+
});
|
|
194
|
+
if (!emit) continue;
|
|
129
195
|
|
|
130
|
-
|
|
131
|
-
if (
|
|
196
|
+
let resolvedPercent = percent;
|
|
197
|
+
if (
|
|
198
|
+
resolvedPercent !== null &&
|
|
199
|
+
mapping.valueMultiplier !== undefined &&
|
|
200
|
+
mapping.valueMultiplier !== 0
|
|
201
|
+
) {
|
|
202
|
+
resolvedPercent = Math.min(100, Math.max(0, resolvedPercent * mapping.valueMultiplier));
|
|
203
|
+
}
|
|
132
204
|
|
|
133
|
-
windows.push(percentWindow(mapping.kind, mapping.label,
|
|
205
|
+
windows.push(percentWindow(mapping.kind, mapping.label, resolvedPercent, reset, used, limit, "independent"));
|
|
134
206
|
}
|
|
135
207
|
|
|
136
208
|
if (options.extraWindows) windows.push(...options.extraWindows(payload));
|
|
@@ -18,6 +18,7 @@ import type { ProviderId } from "../provider-contracts";
|
|
|
18
18
|
import type { ProviderRegistry } from "../provider-registry";
|
|
19
19
|
import { quotaLens, setCachedQuota } from "./cache";
|
|
20
20
|
import { persistQuotaSnapshot } from "./snapshot";
|
|
21
|
+
import { applyAccountVerdict } from "../operations/account-health-service";
|
|
21
22
|
import { asJsonbRecord } from "../../shared/coerce";
|
|
22
23
|
import { KIRO_DEFAULT_LABELS } from "../integrations/kiro/kiro-oauth";
|
|
23
24
|
|
|
@@ -150,12 +151,52 @@ export async function recordAccountCheck(
|
|
|
150
151
|
db: CartethyiaDatabase,
|
|
151
152
|
accountId: string,
|
|
152
153
|
outcome: { ok: boolean; error?: string | null; category?: string | null },
|
|
154
|
+
snapshotInvalidator?: { invalidate(): unknown },
|
|
153
155
|
): Promise<void> {
|
|
154
|
-
const activeOnly = and(
|
|
155
|
-
eq(providerAccounts.id, accountId),
|
|
156
|
-
eq(providerAccounts.status, "active"),
|
|
157
|
-
);
|
|
158
156
|
if (outcome.ok) {
|
|
157
|
+
// A successful quota refresh is the freshest evidence about the account's
|
|
158
|
+
// health: heal a cooldown that the refresh shows is no longer justified, so
|
|
159
|
+
// the account returns to rotation immediately instead of waiting out the
|
|
160
|
+
// remainder of a stale fallback window. Only healable scopes heal — an
|
|
161
|
+
// auth/policy cooldown must never be lifted by a quota report (the
|
|
162
|
+
// credential is still refused). `lastErrorCategory` carries the category
|
|
163
|
+
// that parked the account, so the heal is scoped by the same verdict the
|
|
164
|
+
// health machine wrote.
|
|
165
|
+
const healableCategories = new Set([
|
|
166
|
+
"quota_exhausted",
|
|
167
|
+
"rate_limit_transient",
|
|
168
|
+
"model_capacity",
|
|
169
|
+
"model_unreachable",
|
|
170
|
+
"unknown",
|
|
171
|
+
"check_failed",
|
|
172
|
+
]);
|
|
173
|
+
const rows = await db
|
|
174
|
+
.select({ status: providerAccounts.status, lastErrorCategory: providerAccounts.lastErrorCategory })
|
|
175
|
+
.from(providerAccounts)
|
|
176
|
+
.where(eq(providerAccounts.id, accountId))
|
|
177
|
+
.limit(1);
|
|
178
|
+
const account = rows[0];
|
|
179
|
+
const healable =
|
|
180
|
+
account?.status === "cooldown" &&
|
|
181
|
+
(account.lastErrorCategory === null || healableCategories.has(account.lastErrorCategory));
|
|
182
|
+
if (healable) {
|
|
183
|
+
const healed = await db
|
|
184
|
+
.update(providerAccounts)
|
|
185
|
+
.set({
|
|
186
|
+
status: "active",
|
|
187
|
+
lastSuccessAt: new Date(),
|
|
188
|
+
lastError: null,
|
|
189
|
+
lastErrorCategory: null,
|
|
190
|
+
lastErrorAt: null,
|
|
191
|
+
consecutiveFailures: 0,
|
|
192
|
+
cooldownUntil: null,
|
|
193
|
+
lastRecoveredAt: new Date(),
|
|
194
|
+
})
|
|
195
|
+
.where(eq(providerAccounts.id, accountId))
|
|
196
|
+
.returning({ id: providerAccounts.id });
|
|
197
|
+
if (healed.length > 0) await snapshotInvalidator?.invalidate();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
159
200
|
await db
|
|
160
201
|
.update(providerAccounts)
|
|
161
202
|
.set({
|
|
@@ -165,7 +206,7 @@ export async function recordAccountCheck(
|
|
|
165
206
|
lastErrorAt: null,
|
|
166
207
|
consecutiveFailures: 0,
|
|
167
208
|
})
|
|
168
|
-
.where(
|
|
209
|
+
.where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.status, "active")));
|
|
169
210
|
return;
|
|
170
211
|
}
|
|
171
212
|
const transient = isTransientTimeout(outcome.error);
|
|
@@ -186,7 +227,7 @@ export async function recordAccountCheck(
|
|
|
186
227
|
lastErrorAt: new Date(),
|
|
187
228
|
...(transient ? {} : { consecutiveFailures: sql`${providerAccounts.consecutiveFailures} + 1` }),
|
|
188
229
|
})
|
|
189
|
-
.where(
|
|
230
|
+
.where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.status, "active")))
|
|
190
231
|
.returning({ consecutiveFailures: providerAccounts.consecutiveFailures });
|
|
191
232
|
if (transient) return;
|
|
192
233
|
const failures = failing[0]?.consecutiveFailures ?? 0;
|
|
@@ -196,13 +237,18 @@ export async function recordAccountCheck(
|
|
|
196
237
|
// pass. The cooldown clears itself; a success before it elapses resets the
|
|
197
238
|
// count above.
|
|
198
239
|
if (failures >= QUOTA_FAILURE_PARK_THRESHOLD) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.
|
|
240
|
+
// Converge on the health application seam (proposal §3.5): quota refresh
|
|
241
|
+
// parking is a producer into `applyAccountVerdict`, not a separate state
|
|
242
|
+
// machine.
|
|
243
|
+
const parked = await applyAccountVerdict(db, {
|
|
244
|
+
accountId,
|
|
245
|
+
status: "cooldown",
|
|
246
|
+
reason: outcome.error ?? "Account check failed",
|
|
247
|
+
category: outcome.category ?? "quota_fetch_failed",
|
|
248
|
+
cooldownUntil: new Date(Date.now() + QUOTA_FAILURE_COOLDOWN_MS),
|
|
249
|
+
fromStatus: "active",
|
|
250
|
+
});
|
|
251
|
+
if (parked) await snapshotInvalidator?.invalidate();
|
|
206
252
|
}
|
|
207
253
|
}
|
|
208
254
|
|
|
@@ -427,7 +473,13 @@ async function runQuotaRefresh(
|
|
|
427
473
|
try {
|
|
428
474
|
await deps.db
|
|
429
475
|
.update(providerAccounts)
|
|
430
|
-
.set({
|
|
476
|
+
.set({
|
|
477
|
+
quotaPlan: quota.plan,
|
|
478
|
+
// Freshness stamp for the plan column: routing uses it to downgrade
|
|
479
|
+
// a plan older than the freshness horizon to `unknown` instead of
|
|
480
|
+
// trusting a weeks-old string.
|
|
481
|
+
quotaPlanFetchedAt: new Date(),
|
|
482
|
+
})
|
|
431
483
|
.where(eq(providerAccounts.id, target.accountId));
|
|
432
484
|
} catch (error) {
|
|
433
485
|
log.warn(`[quota] failed to record plan for account=${target.accountId}`, error as Error);
|
|
@@ -469,10 +521,15 @@ async function runQuotaRefresh(
|
|
|
469
521
|
}
|
|
470
522
|
}
|
|
471
523
|
try {
|
|
472
|
-
await recordAccountCheck(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
524
|
+
await recordAccountCheck(
|
|
525
|
+
deps.db,
|
|
526
|
+
target.accountId,
|
|
527
|
+
{
|
|
528
|
+
ok: quota.error === null,
|
|
529
|
+
...(quota.error === null ? {} : { error: quota.error, category: failure ?? "quota_fetch_failed" }),
|
|
530
|
+
},
|
|
531
|
+
deps.snapshotInvalidator,
|
|
532
|
+
);
|
|
476
533
|
} catch (error) {
|
|
477
534
|
// A stamp failure must not lose the freshly fetched quota.
|
|
478
535
|
log.warn(`[quota] failed to stamp check outcome for account=${target.accountId}`, error as Error);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Bounded process-local authorization cache; shared-cache I/O stays in the auth service. */
|
|
2
2
|
export const AUTH_CACHE_TTL_MS = 3_000;
|
|
3
3
|
const AUTH_CACHE_MAX_ENTRIES = 10_000;
|
|
4
|
-
const AUTH_NEGATIVE_CACHE_TTL_MS = 1_500;
|
|
4
|
+
export const AUTH_NEGATIVE_CACHE_TTL_MS = 1_500;
|
|
5
5
|
const AUTH_FAILURE_COOLDOWN_MS = 1_000;
|
|
6
6
|
|
|
7
7
|
interface CacheEntry<T> {
|
|
@@ -21,8 +21,12 @@ import type { ApiKeyModelAccessMode } from "../persistence/schema";
|
|
|
21
21
|
* snapshot's consumed value is a reference point, so a brief stale window
|
|
22
22
|
* cannot meaningfully over-consume a budget.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* a
|
|
24
|
+
* Unknown or revoked tokens are negative-cached for
|
|
25
|
+
* `AUTH_NEGATIVE_CACHE_TTL_MS` (1.5s): a token rejected within that window
|
|
26
|
+
* stays refused even if its key is minted or un-revoked in between. A newly
|
|
27
|
+
* minted key is usable immediately only because `invalidateApiKeyCache`
|
|
28
|
+
* clears the negative entries when a mutation lands — not because negatives
|
|
29
|
+
* are absent.
|
|
26
30
|
*/
|
|
27
31
|
const authorizationCache = createApiKeyAuthorizationCache<ResolvedApiKey>();
|
|
28
32
|
|