cartethyia 2.7.0 → 2.7.2
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 +107 -106
- package/README.md +25 -2
- package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-XaUmiBrC.js} +1 -1
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CqD7QiLM.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-0XUo4UMn.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-CXGS_Ct-.js} +1 -1
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-DAG5J9EY.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js +1 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +9 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-7P2n--Fv.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CxZ1fTkN.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +1 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-Dqf4zpS-.js} +1 -1
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-Byj2lAEm.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +56 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-ClCMCWWQ.js} +3 -3
- package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js +1 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-DFVFrnWW.js} +2 -2
- package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/providers/inception.svg +5 -0
- package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/{select-KIT0rC6G.js → select-DBNVLdUu.js} +1 -1
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
- package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
- package/package.json +96 -96
- package/scripts/cli/console-menu.ts +5 -2
- package/scripts/cli/tray.ts +21 -6
- package/src/boot/app.ts +684 -714
- package/src/boot/dependencies.ts +65 -62
- package/src/config/cooldowns.ts +16 -0
- package/src/config/providers.ts +13 -1
- package/src/config/resources.ts +70 -70
- package/src/config/security.ts +37 -37
- package/src/config/spec.ts +371 -358
- package/src/console/cli-tools/contracts.ts +10 -4
- package/src/console/console-router.ts +233 -225
- package/src/console/domain-registration.ts +398 -381
- package/src/console/providers/catalog/catalog-projections.ts +2 -6
- package/src/console/providers/catalog/contracts.ts +19 -0
- package/src/console/providers/catalog/provider-operations.ts +38 -0
- package/src/console/providers/catalog/routes.ts +14 -0
- package/src/console/providers/catalog/store-helpers.ts +326 -0
- package/src/console/providers/catalog/store.ts +1292 -1563
- package/src/console/quota/account-quota-global.ts +2 -2
- package/src/console/quota/account-quota-shared.ts +1 -1
- package/src/console/quota/account-quota-tenant.ts +35 -12
- package/src/console/quota/quota-view.ts +2 -2
- package/src/console/routing/pools/contracts.ts +41 -1
- package/src/console/routing/pools/operations.ts +183 -4
- package/src/console/routing/pools/relay-deploy.ts +17 -4
- package/src/console/routing/pools/routes.ts +30 -0
- package/src/console/routing/pools/store.ts +98 -8
- package/src/main.ts +183 -167
- package/src/network/outbound-fetch.ts +124 -208
- package/src/network/pool/agent.ts +15 -0
- package/src/network/pool/pool-state.ts +7 -0
- package/src/network/pool/selector.ts +110 -2
- package/src/network/pool-cooldown-store.ts +252 -0
- package/src/network/pool-health-machine.ts +5 -3
- package/src/network/pool-health.ts +36 -9
- package/src/observability/event-query.ts +57 -0
- package/src/observability/metrics.ts +467 -467
- package/src/observability/runtime-metrics.ts +269 -269
- package/src/persistence/health-event-flush.ts +2 -0
- package/src/persistence/schema.ts +19 -0
- package/src/persistence/usage-flush.ts +8 -3
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/dsml.ts +377 -0
- package/src/protocol/gateway-error.ts +232 -232
- package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
- package/src/protocol/request/chat.ts +7 -7
- package/src/protocol/request/codex.ts +2 -2
- package/src/protocol/request/gemini.ts +1 -1
- package/src/protocol/request/messages.ts +6 -6
- package/src/protocol/request/responses.ts +7 -7
- package/src/protocol/response/dsml.ts +12 -133
- package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
- package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
- package/src/{transport → protocol}/translation/capabilities.ts +2 -2
- package/src/{transport → protocol}/translation/normalize.ts +1 -1
- package/src/{transport → protocol}/translation/quirks.ts +1 -1
- package/src/{transport → protocol}/translation/search.ts +1 -1
- package/src/{transport → protocol}/translation/thinking.ts +23 -1
- package/src/{transport → protocol}/translation/tool-id.ts +1 -1
- package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream-parsing.ts +176 -0
- package/src/providers/adapters/compatible-adapter.ts +11 -2
- package/src/providers/authentication/oauth-refresh-service.ts +9 -5
- package/src/providers/default-registry.ts +22 -5
- package/src/providers/discovery/probe-phases.ts +16 -3
- package/src/providers/discovery/probing-service.ts +4 -1
- package/src/providers/integrations/anthropic.ts +1 -1
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
- package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
- package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
- package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
- package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
- package/src/providers/integrations/claude/claude.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +1 -1
- package/src/providers/integrations/factory/factory.ts +1 -1
- package/src/providers/integrations/github/github.ts +1 -1
- package/src/providers/integrations/grok/grok.ts +1 -1
- package/src/providers/integrations/inception/provider.ts +91 -0
- package/src/providers/integrations/inferhub.ts +1 -1
- package/src/providers/integrations/kiro/kiro-request.ts +1 -1
- package/src/providers/integrations/mistral.ts +193 -19
- package/src/providers/integrations/ollamacloud-models.ts +50 -0
- package/src/providers/integrations/opencode/fingerprint.ts +19 -2
- package/src/providers/integrations/opencode/prompt.ts +111 -0
- package/src/providers/integrations/opencode/provider.ts +101 -23
- package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
- package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -1
- package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
- package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
- package/src/providers/integrations/qoder/qoder.ts +315 -400
- package/src/providers/integrations/shared/install-id.ts +2 -1
- package/src/providers/integrations/trae/trae-cache.ts +110 -0
- package/src/providers/integrations/trae/trae-oauth.ts +29 -4
- package/src/providers/integrations/trae/trae-protocol.ts +2 -1
- package/src/providers/integrations/trae/trae.ts +106 -23
- package/src/providers/operations/account-error-classification.ts +1684 -0
- package/src/providers/operations/account-health-service.ts +27 -1503
- package/src/providers/operations/account-reset-service.ts +2 -0
- package/src/providers/operations/provider-catalog-service.ts +11 -1
- package/src/providers/provider-contracts.ts +7 -4
- package/src/providers/provider-metadata.ts +25 -4
- package/src/providers/provider-probe.ts +2 -2
- package/src/{console → providers}/quota/cache.ts +1 -1
- package/src/{console → providers}/quota/refresh.ts +5 -5
- package/src/security/cli-client-fingerprint.ts +9 -2
- package/src/transport/dispatch/attempt-finalize.ts +114 -12
- package/src/transport/dispatch/attempt-loop.ts +3 -0
- package/src/transport/dispatch/empty-stream.ts +9 -2
- package/src/transport/dispatch/generation.ts +37 -55
- package/src/transport/dispatch/handler-deps.ts +32 -0
- package/src/transport/dispatch/leases.ts +9 -4
- package/src/transport/dispatch/proxy-request.ts +6 -30
- package/src/transport/dispatch/responses-compact.ts +22 -43
- package/src/transport/dispatch/streaming/finalize.ts +3 -0
- package/src/transport/dispatch/streaming/stream.ts +0 -8
- package/src/transport/dispatch/systemone.ts +22 -42
- package/src/transport/dispatch/token-count.ts +23 -39
- package/src/transport/dispatch/upstream.ts +1 -3
- package/src/transport/dispatch/websearch.ts +22 -41
- package/src/transport/failure-policy.ts +92 -178
- package/src/transport/guards/abuse-guard.ts +10 -0
- package/src/transport/guards/auth-guard.ts +109 -109
- package/src/transport/guards/error-lifecycle.ts +1 -1
- package/src/transport/guards/pipeline.ts +220 -220
- package/src/transport/guards/request-context.ts +245 -238
- package/src/transport/request/degradation.ts +20 -1
- package/src/transport/request/inflight.ts +4 -114
- package/src/transport/request/preparer.ts +6 -6
- package/src/transport/request/state.ts +66 -38
- package/src/transport/request/tenant-preferences.ts +1 -1
- package/src/transport/routing/route-catalog.ts +75 -43
- package/src/transport/routing/router.ts +30 -2
- package/src/transport/surface/messages/parse.ts +1 -1
- package/src/transport/surface/responses/encode.ts +2 -3
- package/src/transport/surface/responses/parse.ts +49 -7
- package/src/types/undici.d.ts +33 -0
- package/src/workers/buddy-growth-tasks.ts +346 -425
- package/src/workers/daily-checkin.ts +17 -6
- package/src/workers/quota-refresh-sweep.ts +2 -2
- package/src/workers/scheduler.ts +17 -1
- package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
- package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
- package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
- package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
- package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
- package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
- package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
- package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
- package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
- package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
- package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
- package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
- package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
- package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
- package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
- package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
- package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
- package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
- package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
- package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
- package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
- package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* segment. `mapProviderRow` is the single `providers` row projection.
|
|
10
10
|
*/
|
|
11
11
|
import { isBundledProviderId } from "../../../providers/provider-contracts";
|
|
12
|
+
import { BUILTIN_DEFAULT_ENDPOINTS } from "../../../protocol/codecs";
|
|
12
13
|
|
|
13
14
|
import { providerDisplayName, providerServiceKinds } from "../../../providers/provider-metadata";
|
|
14
15
|
import type { CompatibilityProfile } from "../../../providers/provider-metadata";
|
|
@@ -26,12 +27,7 @@ export interface ProviderDiscoveryLookup {
|
|
|
26
27
|
hasModelDiscovery(providerId: string): boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export const DEFAULT_ENDPOINT_BY_WIRE_FAMILY: Record<WireFamily, string> =
|
|
30
|
-
chat: "/v1/chat/completions",
|
|
31
|
-
responses: "/v1/responses",
|
|
32
|
-
messages: "/v1/messages",
|
|
33
|
-
gemini: "/api/llm/g/v1/generate",
|
|
34
|
-
};
|
|
30
|
+
export const DEFAULT_ENDPOINT_BY_WIRE_FAMILY: Record<WireFamily, string> = BUILTIN_DEFAULT_ENDPOINTS;
|
|
35
31
|
export function endpointPathForProviderModel(
|
|
36
32
|
wireFamily: WireFamily,
|
|
37
33
|
compatibilityProfile: unknown,
|
|
@@ -557,6 +557,14 @@ export interface ProviderAccountExport {
|
|
|
557
557
|
accessToken: string;
|
|
558
558
|
/** Decrypted OAuth refresh token, omitted when the account has none. */
|
|
559
559
|
refreshToken?: string;
|
|
560
|
+
/**
|
|
561
|
+
* Non-secret upstream auth configuration carried on the account (the
|
|
562
|
+
* upstream user id, provisioned region, auth method, profile). Re-import
|
|
563
|
+
* must restore it: Trae refresh is addressed per upstream user id, and the
|
|
564
|
+
* data-plane host is region-scoped, so dropping it breaks re-imported
|
|
565
|
+
* accounts on their first refresh or dispatch.
|
|
566
|
+
*/
|
|
567
|
+
authState?: Readonly<Record<string, unknown>>;
|
|
560
568
|
createdAt: string;
|
|
561
569
|
/** Live routing admission count at export time; absent when unavailable. */
|
|
562
570
|
inflight?: number;
|
|
@@ -771,6 +779,17 @@ export interface ProviderCatalogStore {
|
|
|
771
779
|
syncModels(tenantId: string, providerId: string): Promise<{ synced: number }>;
|
|
772
780
|
listAccounts(tenantId: string, providerId: string): Promise<readonly ProviderAccountResponse[]>;
|
|
773
781
|
listAllAccounts(tenantId: string): Promise<readonly ProviderAccountResponse[]>;
|
|
782
|
+
/**
|
|
783
|
+
* Non-secret upstream auth configuration for the given owned accounts,
|
|
784
|
+
* keyed by account id. Used by the account-export route so a re-import can
|
|
785
|
+
* restore the upstream user id / region that refresh and dispatch are
|
|
786
|
+
* addressed by; never surfaced on the read models.
|
|
787
|
+
*/
|
|
788
|
+
listAccountAuthStates(
|
|
789
|
+
tenantId: string,
|
|
790
|
+
providerId: string,
|
|
791
|
+
accountIds: readonly string[],
|
|
792
|
+
): Promise<Readonly<Record<string, Readonly<Record<string, unknown>>>>>;
|
|
774
793
|
createAccount(
|
|
775
794
|
tenantId: string,
|
|
776
795
|
providerId: string,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
requireTenantAnyScope,
|
|
18
18
|
} from "../../shared/errors";
|
|
19
19
|
import type { AccessDecision } from "../../../security/access-control";
|
|
20
|
+
import type { ProviderUsageDayRow } from "../../../observability/event-query";
|
|
20
21
|
import { isBundledProviderId } from "../../../providers/provider-contracts";
|
|
21
22
|
import type { ProviderRegistry } from "../../../providers/provider-registry";
|
|
22
23
|
import { getProviderClientVersion, getRouteIdentityVersions } from "../../../providers/operations/client-versions";
|
|
@@ -146,6 +147,16 @@ export interface ProviderCatalogConfig {
|
|
|
146
147
|
readonly members: readonly string[];
|
|
147
148
|
}[];
|
|
148
149
|
}>;
|
|
150
|
+
/**
|
|
151
|
+
* Day × model usage aggregate feeding the provider's Activity Matrix card.
|
|
152
|
+
* Optional so hosts without telemetry reads (and existing test doubles) keep
|
|
153
|
+
* the route returning an empty matrix instead of failing.
|
|
154
|
+
*/
|
|
155
|
+
readonly providerUsageMatrix?: (
|
|
156
|
+
tenantId: string,
|
|
157
|
+
providerId: string,
|
|
158
|
+
days: number,
|
|
159
|
+
) => Promise<readonly ProviderUsageDayRow[]>;
|
|
149
160
|
/**
|
|
150
161
|
* Resolves the stored credential and optional OAuth refresh token for export,
|
|
151
162
|
* decrypting them without applying provider-specific dispatch codecs.
|
|
@@ -249,6 +260,19 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
|
|
|
249
260
|
throw new ConsoleDomainError("provider_not_found", 404, `Provider ${providerId} not found`);
|
|
250
261
|
return attachProviderCapabilities(sanitizeProviderResponse(rec, true), config.providerRegistry);
|
|
251
262
|
},
|
|
263
|
+
async getUsageMatrix(
|
|
264
|
+
access: AccessDecision | undefined,
|
|
265
|
+
providerId: string,
|
|
266
|
+
days: number,
|
|
267
|
+
): Promise<{ readonly days: number; readonly rows: readonly ProviderUsageDayRow[] }> {
|
|
268
|
+
const a = requireTenantAnyScope(access, PROVIDER_READ_SCOPES);
|
|
269
|
+
// Bounded window even on a hostile query string: the card reads a
|
|
270
|
+
// calendar, not history, and the aggregate scan cost grows with `days`.
|
|
271
|
+
const bounded = Math.min(Math.max(Math.trunc(days) || 1, 1), 366);
|
|
272
|
+
if (!config.providerUsageMatrix) return { days: bounded, rows: [] };
|
|
273
|
+
const rows = await config.providerUsageMatrix(a.tenantId, providerId, bounded);
|
|
274
|
+
return { days: bounded, rows };
|
|
275
|
+
},
|
|
252
276
|
async createProvider(
|
|
253
277
|
access: AccessDecision | undefined,
|
|
254
278
|
request: CreateProviderRequest,
|
|
@@ -639,6 +663,11 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
|
|
|
639
663
|
const selected = owned.filter(
|
|
640
664
|
(account) => wanted.has(account.id) && account.tenantId === a.tenantId,
|
|
641
665
|
);
|
|
666
|
+
const authStates = await config.store.listAccountAuthStates(
|
|
667
|
+
a.tenantId,
|
|
668
|
+
providerId,
|
|
669
|
+
selected.map((account) => account.id),
|
|
670
|
+
);
|
|
642
671
|
const accounts = await Promise.all(
|
|
643
672
|
selected.map(async (account): Promise<ProviderAccountExport> => {
|
|
644
673
|
let accessToken = "";
|
|
@@ -661,6 +690,15 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
|
|
|
661
690
|
status: account.status,
|
|
662
691
|
accessToken,
|
|
663
692
|
...(refreshToken === undefined ? {} : { refreshToken }),
|
|
693
|
+
// Non-secret auth state is part of the credential export: without
|
|
694
|
+
// it a re-import loses the upstream user id and region that Trae
|
|
695
|
+
// refresh and dispatch are addressed by.
|
|
696
|
+
...(() => {
|
|
697
|
+
const authState = authStates[account.id];
|
|
698
|
+
return authState !== undefined && Object.keys(authState).length > 0
|
|
699
|
+
? { authState }
|
|
700
|
+
: {};
|
|
701
|
+
})(),
|
|
664
702
|
...(account.inflight === undefined ? {} : { inflight: account.inflight }),
|
|
665
703
|
createdAt: account.createdAt,
|
|
666
704
|
...(account.cooldownUntil ? { cooldownUntil: account.cooldownUntil } : {}),
|
|
@@ -147,6 +147,9 @@ const deleteModelBody = t.Object({
|
|
|
147
147
|
const bulkDeleteModelsBody = t.Object({
|
|
148
148
|
items: t.Array(deleteModelBody, { minItems: 1, maxItems: 1000 }),
|
|
149
149
|
});
|
|
150
|
+
const usageMatrixQuery = t.Object({
|
|
151
|
+
days: t.Optional(t.Integer({ minimum: 1, maximum: 366 })),
|
|
152
|
+
});
|
|
150
153
|
export function createProviderCatalogRoutes(config: ProviderCatalogConfig): Elysia {
|
|
151
154
|
const factory = createProviderCatalogOperations(config);
|
|
152
155
|
const modelFactory = createModelCatalogOperations(config);
|
|
@@ -191,6 +194,17 @@ export function createProviderCatalogRoutes(config: ProviderCatalogConfig): Elys
|
|
|
191
194
|
.get("/:providerId", async ({ request, params }) => {
|
|
192
195
|
return await factory.getProviderDetail(config.accessResolver(request), params.providerId);
|
|
193
196
|
})
|
|
197
|
+
.get(
|
|
198
|
+
"/:providerId/usage/matrix",
|
|
199
|
+
{ query: usageMatrixQuery },
|
|
200
|
+
async ({ request, params, query }) => {
|
|
201
|
+
return await factory.getUsageMatrix(
|
|
202
|
+
config.accessResolver(request),
|
|
203
|
+
params.providerId,
|
|
204
|
+
query.days ?? 30,
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
)
|
|
194
208
|
.post("/", { body: createProviderBody }, async ({ request, body, set }) => {
|
|
195
209
|
set.status = 201;
|
|
196
210
|
return await factory.createProvider(
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the provider catalog store: credential parsing, generated
|
|
3
|
+
* labels, and row projections. These are the parts of the store that do not
|
|
4
|
+
* touch the database, kept here so the Drizzle store stays about persistence.
|
|
5
|
+
*/
|
|
6
|
+
import { decodeJwtPayload } from "../../../providers/authentication/oauth-flow-store";
|
|
7
|
+
import {
|
|
8
|
+
asRecord,
|
|
9
|
+
firstNonEmptyString as firstString,
|
|
10
|
+
tryParseJson as safeParseJson,
|
|
11
|
+
} from "../../../shared/coerce";
|
|
12
|
+
import type { CredentialMode, ModelCatalogEntry } from "./contracts";
|
|
13
|
+
import {
|
|
14
|
+
FALLBACK_CONTEXT_LIMIT,
|
|
15
|
+
FALLBACK_OUTPUT_LIMIT,
|
|
16
|
+
} from "../../../providers/provider-model-definition";
|
|
17
|
+
import { modelsDevCatalog } from "../../../providers/discovery/models-dev-catalog";
|
|
18
|
+
import { models } from "../../../persistence/schema";
|
|
19
|
+
/** Field names a pasted OAuth credential may carry its access token under. */
|
|
20
|
+
const OAUTH_ACCESS_FIELDS = ["accessToken", "access_token", "access", "token"] as const;
|
|
21
|
+
/** Field names a pasted OAuth credential may carry its refresh token under. */
|
|
22
|
+
const OAUTH_REFRESH_FIELDS = ["refreshToken", "refresh_token", "refresh"] as const;
|
|
23
|
+
|
|
24
|
+
/** Parses an expiry field that may be an ISO string, an epoch ms, or epoch seconds. */
|
|
25
|
+
function parseExpiry(value: unknown): Date | undefined {
|
|
26
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
27
|
+
// Heuristic: a 10-digit value is epoch seconds, a 13-digit value is ms.
|
|
28
|
+
const ms = value < 1e12 ? value * 1000 : value;
|
|
29
|
+
const date = new Date(ms);
|
|
30
|
+
return Number.isFinite(date.getTime()) ? date : undefined;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
33
|
+
const asNumber = Number(value);
|
|
34
|
+
if (Number.isFinite(asNumber)) return parseExpiry(asNumber);
|
|
35
|
+
const date = new Date(value);
|
|
36
|
+
return Number.isFinite(date.getTime()) ? date : undefined;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads the two claims a static token's own payload can answer: when it stops
|
|
42
|
+
* working, and who issued it.
|
|
43
|
+
*
|
|
44
|
+
* The issuer is kept because a static JWT carries no other hint of its origin:
|
|
45
|
+
* an operator who pasted one from the wrong surface has nothing in the console
|
|
46
|
+
* to check it against, and several issuers accept the same token shape.
|
|
47
|
+
*/
|
|
48
|
+
export function parseJwtCredential(
|
|
49
|
+
raw: string,
|
|
50
|
+
): { readonly expiresAt?: Date; readonly issuer?: string } | undefined {
|
|
51
|
+
const payload = decodeJwtPayload(raw.trim());
|
|
52
|
+
if (payload === undefined) return undefined;
|
|
53
|
+
const expiresAt = parseExpiry(payload["exp"]);
|
|
54
|
+
const iss = payload["iss"];
|
|
55
|
+
const issuer = typeof iss === "string" && iss.trim().length > 0 ? iss.trim() : undefined;
|
|
56
|
+
return {
|
|
57
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
58
|
+
...(issuer === undefined ? {} : { issuer }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Splits a pasted OAuth credential into its access token, refresh token, and
|
|
64
|
+
* expiry.
|
|
65
|
+
*
|
|
66
|
+
* The console accepts either a bare access token or the JSON export a client
|
|
67
|
+
* produces (`{accessToken, refreshToken, expiresAt, …}`, possibly nested under
|
|
68
|
+
* `data`). A bare token has no refresh token — the account can be dispatched
|
|
69
|
+
* with it but cannot be refreshed, so the caller stores it as a static
|
|
70
|
+
* API credential instead of sending the access token to a refresh endpoint.
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Middle segment of an auto-generated account label, from how the credential
|
|
74
|
+
* was actually detected.
|
|
75
|
+
*
|
|
76
|
+
* Only the two kinds that arrive with no name of their own are named here. An
|
|
77
|
+
* OAuth paste carries its own identity in `authState` (the account it was
|
|
78
|
+
* exported from), so it keeps whatever the export or the operator supplied —
|
|
79
|
+
* generating a name for it would overwrite an identity that was already there.
|
|
80
|
+
* An unrecognized mode falls back to `key`, matching the mode it is stored as.
|
|
81
|
+
*/
|
|
82
|
+
export function accountLabelKindSegment(credentialMode: CredentialMode): string {
|
|
83
|
+
return credentialMode === "jwt" ? "jwt" : "key";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* First unused `dcint-key-N` / `dcint-jwt-N` label for one provider.
|
|
88
|
+
*
|
|
89
|
+
* Polished tokens and plain JWTs are the credentials that arrive nameless: an
|
|
90
|
+
* operator pastes several into one provider and every row came out as
|
|
91
|
+
* "<providerId> account", so five accounts were five identical labels and the
|
|
92
|
+
* list could not tell them apart — which account failed, or is cooling, was
|
|
93
|
+
* unreadable.
|
|
94
|
+
*
|
|
95
|
+
* Numbering runs per kind, so the first JWT is `jwt-1` rather than inheriting
|
|
96
|
+
* whatever number the api_key rows reached (`jwt-3` with no `jwt-1` reads as
|
|
97
|
+
* two missing accounts). Counting is case-insensitive on the whole label, so a
|
|
98
|
+
* hand-typed `DCINT-KEY-2` still reserves its number.
|
|
99
|
+
*
|
|
100
|
+
* Pure, so the numbering can be pinned without a database.
|
|
101
|
+
*/
|
|
102
|
+
export function nextGeneratedAccountLabel(
|
|
103
|
+
providerId: string,
|
|
104
|
+
credentialMode: CredentialMode,
|
|
105
|
+
existingLabels: readonly string[],
|
|
106
|
+
): string {
|
|
107
|
+
const kind = accountLabelKindSegment(credentialMode);
|
|
108
|
+
const prefix = `${providerId}-${kind}-`;
|
|
109
|
+
const taken = new Set(
|
|
110
|
+
existingLabels.map((label) => label.trim().toLowerCase()),
|
|
111
|
+
);
|
|
112
|
+
for (let n = 1; ; n += 1) {
|
|
113
|
+
const candidate = `${prefix}${n}`;
|
|
114
|
+
if (!taken.has(candidate.toLowerCase())) return candidate;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parsePastedOAuthCredential(raw: string): {
|
|
119
|
+
readonly accessToken: string;
|
|
120
|
+
readonly refreshToken: string;
|
|
121
|
+
readonly expiresAt: Date | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Non-secret account configuration the export carried — the profile the
|
|
124
|
+
* account is bound to, how it authenticates, the region it was minted in.
|
|
125
|
+
* Empty for a bare token, which carries none.
|
|
126
|
+
*/
|
|
127
|
+
readonly authState: Readonly<Record<string, unknown>>;
|
|
128
|
+
} {
|
|
129
|
+
let record: Record<string, unknown> | undefined;
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
132
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
133
|
+
record = parsed as Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
record = undefined;
|
|
137
|
+
}
|
|
138
|
+
if (record === undefined) {
|
|
139
|
+
// Bare token: it is the access token, and there is no refresh token.
|
|
140
|
+
const jwt = parseJwtCredential(raw);
|
|
141
|
+
return {
|
|
142
|
+
accessToken: raw,
|
|
143
|
+
refreshToken: raw,
|
|
144
|
+
expiresAt: jwt?.expiresAt,
|
|
145
|
+
authState: {},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// A nested `data` object is unwrapped the same way the dashboard's parser does.
|
|
149
|
+
const nested =
|
|
150
|
+
record["data"] !== null && typeof record["data"] === "object" && !Array.isArray(record["data"])
|
|
151
|
+
? (record["data"] as Record<string, unknown>)
|
|
152
|
+
: undefined;
|
|
153
|
+
const accessToken =
|
|
154
|
+
firstString(record, OAUTH_ACCESS_FIELDS) ??
|
|
155
|
+
(nested ? firstString(nested, OAUTH_ACCESS_FIELDS) : undefined) ??
|
|
156
|
+
raw;
|
|
157
|
+
const refreshToken =
|
|
158
|
+
firstString(record, OAUTH_REFRESH_FIELDS) ??
|
|
159
|
+
(nested ? firstString(nested, OAUTH_REFRESH_FIELDS) : undefined) ??
|
|
160
|
+
accessToken;
|
|
161
|
+
const expiresAt =
|
|
162
|
+
parseExpiry(
|
|
163
|
+
record["expiresAt"] ??
|
|
164
|
+
record["expires_at"] ??
|
|
165
|
+
(nested ? (nested["expiresAt"] ?? nested["expires_at"]) : undefined) ??
|
|
166
|
+
record["expires"] ??
|
|
167
|
+
(nested ? nested["expires"] : undefined),
|
|
168
|
+
) ?? parseJwtCredential(accessToken)?.expiresAt;
|
|
169
|
+
return { accessToken, refreshToken, expiresAt, authState: pastedAuthState(record, nested) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The account configuration a pasted export carries beside its tokens.
|
|
174
|
+
*
|
|
175
|
+
* Kiro refuses a generation request that omits the profile the account is bound
|
|
176
|
+
* to, and exports from older installs carry that profile under a different key.
|
|
177
|
+
* Reading neither is how an imported account probes with no
|
|
178
|
+
* profile and is answered `400 profileArn is required`.
|
|
179
|
+
*/
|
|
180
|
+
function pastedAuthState(
|
|
181
|
+
record: Record<string, unknown>,
|
|
182
|
+
nested: Record<string, unknown> | undefined,
|
|
183
|
+
): Readonly<Record<string, unknown>> {
|
|
184
|
+
const sources = [record, nested, asRecord(record["providerSpecificData"]), asRecord(record["authState"]), asRecord(nested?.["authState"])];
|
|
185
|
+
const profileArn = firstStringFrom(sources, ["profileArn", "profile_arn"]);
|
|
186
|
+
const authMethod = firstStringFrom(sources, ["authMethod", "auth_method"]);
|
|
187
|
+
const region = firstStringFrom(sources, ["region"]);
|
|
188
|
+
// The upstream user id, carried on a re-import of an exported account. Trae
|
|
189
|
+
// refresh is addressed per upstream user id, so restoring it is what keeps a
|
|
190
|
+
// re-imported account refreshable.
|
|
191
|
+
const userId = firstStringFrom(sources, ["userId", "user_id", "uid", "UserID"]);
|
|
192
|
+
return {
|
|
193
|
+
...(profileArn === undefined ? {} : { profileArn }),
|
|
194
|
+
...(authMethod === undefined ? {} : { authMethod }),
|
|
195
|
+
...(region === undefined ? {} : { region }),
|
|
196
|
+
...(userId === undefined ? {} : { userId }),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Adds or removes the `image` input modality on a stored modalities blob.
|
|
202
|
+
*
|
|
203
|
+
* The blob is a JSON string whose shape varies by source (bundled seed,
|
|
204
|
+
* discovery, manual add), so it is normalized rather than pattern-matched: an
|
|
205
|
+
* unparseable blob is replaced with the minimal text shape rather than left to
|
|
206
|
+
* corrupt capability preflight.
|
|
207
|
+
*/
|
|
208
|
+
export function withImageModality(stored: string | null | undefined, image: boolean): string {
|
|
209
|
+
const parsed = asRecord(safeParseJson(stored));
|
|
210
|
+
const input = Array.isArray(parsed?.input)
|
|
211
|
+
? (parsed.input as unknown[]).filter((m): m is string => typeof m === "string")
|
|
212
|
+
: ["text"];
|
|
213
|
+
const output = Array.isArray(parsed?.output)
|
|
214
|
+
? (parsed.output as unknown[]).filter((m): m is string => typeof m === "string")
|
|
215
|
+
: ["text"];
|
|
216
|
+
const nextInput = image
|
|
217
|
+
? Array.from(new Set([...input, "image"]))
|
|
218
|
+
: input.filter((m) => m !== "image");
|
|
219
|
+
return JSON.stringify({
|
|
220
|
+
...(parsed ?? {}),
|
|
221
|
+
input: nextInput.length > 0 ? nextInput : ["text"],
|
|
222
|
+
output: output.length > 0 ? output : ["text"],
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Normalizes a text column read as `unknown` back to a string or null. */
|
|
227
|
+
export function asStoredText(value: unknown): string | null {
|
|
228
|
+
return typeof value === "string" ? value : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function firstStringFrom(
|
|
232
|
+
sources: readonly (Record<string, unknown> | undefined)[],
|
|
233
|
+
fields: readonly string[],
|
|
234
|
+
): string | undefined {
|
|
235
|
+
for (const source of sources) {
|
|
236
|
+
if (source === undefined) continue;
|
|
237
|
+
const value = firstString(source, fields);
|
|
238
|
+
if (value !== undefined) return value;
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The account's still-active per-model backoffs, or `undefined` when none are.
|
|
245
|
+
*
|
|
246
|
+
* Expired entries are dropped rather than returned: the sweep that clears them
|
|
247
|
+
* runs on a timer, so between the deadline passing and the sweep a raw read
|
|
248
|
+
* would report a cooldown that is not in force.
|
|
249
|
+
*/
|
|
250
|
+
export function liveModelCooldowns(
|
|
251
|
+
raw: unknown,
|
|
252
|
+
): { modelCooldowns: Record<string, string> } | undefined {
|
|
253
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
const live: Record<string, string> = {};
|
|
256
|
+
for (const [modelId, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
257
|
+
if (typeof value !== "string") continue;
|
|
258
|
+
const at = new Date(value).getTime();
|
|
259
|
+
if (Number.isFinite(at) && at > now) live[modelId] = value;
|
|
260
|
+
}
|
|
261
|
+
return Object.keys(live).length > 0 ? { modelCooldowns: live } : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Projects one persisted `models` row into the console's catalog entry.
|
|
267
|
+
*
|
|
268
|
+
* `disabled` is resolved by the caller (from `tenant_disabled_models`), because
|
|
269
|
+
* the bulk read has to look it up across every provider at once while the
|
|
270
|
+
* single-provider read looks it up for one; the projection itself is identical
|
|
271
|
+
* either way, so it lives here rather than being written twice.
|
|
272
|
+
*
|
|
273
|
+
* Exported so a test can pin the unsized-model substitution without a database:
|
|
274
|
+
* which rows get the fallback, and that they are the same rows marked estimated.
|
|
275
|
+
*/
|
|
276
|
+
export function mapModelRow(
|
|
277
|
+
row: typeof models.$inferSelect,
|
|
278
|
+
disabled: boolean,
|
|
279
|
+
): ModelCatalogEntry {
|
|
280
|
+
const modalities =
|
|
281
|
+
row.modalities && typeof row.modalities === "object"
|
|
282
|
+
? (row.modalities as { input?: unknown; output?: unknown })
|
|
283
|
+
: undefined;
|
|
284
|
+
const inputModalities = Array.isArray(modalities?.input) ? modalities.input : [];
|
|
285
|
+
const outputModalities = Array.isArray(modalities?.output) ? modalities.output : [];
|
|
286
|
+
// Nothing filed this model — not its own row, not the base catalog under this
|
|
287
|
+
// provider's id, not a majority across the catalog's other resellers. The
|
|
288
|
+
// fallback is substituted here rather than persisted: the stored `null` is the
|
|
289
|
+
// honest record of what upstream said, and substituting at the write would
|
|
290
|
+
// make a guess indistinguishable from a measurement on the next read. The
|
|
291
|
+
// marker rides along so the card can say which one it is showing.
|
|
292
|
+
const unfiled =
|
|
293
|
+
row.contextLimit === null &&
|
|
294
|
+
modelsDevCatalog.resolve(row.providerId, row.modelId) === undefined &&
|
|
295
|
+
modelsDevCatalog.majorityFor(row.modelId) === undefined;
|
|
296
|
+
return {
|
|
297
|
+
modelId: row.modelId,
|
|
298
|
+
route: row.endpointPath,
|
|
299
|
+
provider: row.providerId,
|
|
300
|
+
wireFamily: row.wireFamily,
|
|
301
|
+
serviceKind: row.serviceKind,
|
|
302
|
+
enabled: disabled ? false : row.enabled,
|
|
303
|
+
// A fetched model that published no context size read `n/a ctx · n/a out`
|
|
304
|
+
// while still being routable — a working model that looked unusable. These
|
|
305
|
+
// rows get the same ceilings a manual add is offered, flagged as estimated.
|
|
306
|
+
contextLimit: unfiled ? FALLBACK_CONTEXT_LIMIT : row.contextLimit,
|
|
307
|
+
outputLimit: unfiled ? FALLBACK_OUTPUT_LIMIT : row.outputLimit,
|
|
308
|
+
reasoning: row.reasoning,
|
|
309
|
+
toolCall: row.toolCall,
|
|
310
|
+
vision: inputModalities.includes("image"),
|
|
311
|
+
document: inputModalities.includes("document"),
|
|
312
|
+
audio: inputModalities.includes("audio"),
|
|
313
|
+
mediaGeneration: outputModalities.includes("image"),
|
|
314
|
+
cost: row.cost as ModelCatalogEntry["cost"],
|
|
315
|
+
source: row.source,
|
|
316
|
+
...(unfiled ? { limitsEstimated: true } : {}),
|
|
317
|
+
// Three-valued on the wire as it is in the column: only a row that claims
|
|
318
|
+
// `true` is pro-only. Absence means nobody established the tier, and the
|
|
319
|
+
// surface must render that as unknown rather than as free.
|
|
320
|
+
...(row.proOnly === true ? { proOnly: true } : {}),
|
|
321
|
+
// Same three-valued rule, from the provider's own directory. Qoder states
|
|
322
|
+
// `is_free` per row, so a plan change moves the pill without a code change.
|
|
323
|
+
...(row.freeTier === true ? { freeTier: true } : {}),
|
|
324
|
+
sourceUpdatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
|
|
325
|
+
} satisfies ModelCatalogEntry;
|
|
326
|
+
}
|