cartethyia 2.7.9 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -5
- package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
- package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
- package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
- package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
- package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
- package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
- package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
- package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
- package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
- package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
- package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
- package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
- package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
- package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
- package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
- package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
- package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
- package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
- package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
- package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
- package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
- package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
- package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
- package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
- package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
- package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
- package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
- package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
- package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
- package/dist/dashboard/assets/providers/sensenova.png +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
- package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
- package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
- package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
- package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
- package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
- package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
- package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
- package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
- package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
- package/package.json +1 -1
- package/src/boot/app.ts +11 -14
- package/src/boot/dependencies.ts +5 -7
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/providers.ts +6 -5
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +28 -8
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +40 -28
- package/src/console/auth/account-reset.ts +7 -9
- package/src/console/backup/contracts.ts +4 -0
- package/src/console/backup/export-endpoint.ts +7 -5
- package/src/console/backup/nine-router.ts +71 -10
- package/src/console/backup/service.ts +8 -1
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +1 -1
- package/src/console/domain-registration.ts +7 -4
- package/src/console/inferhub/client.ts +1 -1
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +7 -13
- package/src/console/observability/live.ts +2 -2
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/contracts.ts +9 -3
- package/src/console/providers/catalog/model-operations.ts +2 -3
- package/src/console/providers/catalog/provider-operations.ts +1 -1
- package/src/console/providers/catalog/routes.ts +3 -2
- package/src/console/providers/catalog/store.ts +1914 -1902
- package/src/console/providers/detail/routes.ts +1 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/providers/oauth/callback-listener.ts +30 -11
- package/src/console/providers/oauth/device-egress.ts +123 -0
- package/src/console/providers/oauth/routes.ts +123 -16
- package/src/console/quota/account-quota-global.ts +14 -24
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +11 -23
- package/src/console/quota/account-test-egress.ts +2 -2
- package/src/console/quota/daily-growth-job.ts +1 -1
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/contracts.ts +1 -1
- package/src/console/routing/pools/operations.ts +32 -47
- package/src/console/routing/pools/probe-verdict.ts +2 -2
- package/src/console/routing/pools/relay-deploy.ts +1 -1
- package/src/console/routing/pools/store.ts +4 -4
- package/src/console/shared/errors.ts +33 -6
- package/src/main.ts +2 -3
- package/src/network/client.ts +67 -0
- package/src/network/egress/bidi-session.ts +322 -0
- package/src/network/egress/dial.ts +201 -0
- package/src/network/egress/factory.ts +191 -0
- package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
- package/src/network/egress/tls-profile.ts +123 -0
- package/src/network/egress/ws.ts +175 -0
- package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
- package/src/network/{types.ts → pool/agent-config.ts} +1 -1
- package/src/network/pool/agent.ts +2 -2
- package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
- package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
- package/src/network/pool/resolver.ts +10 -849
- package/src/network/pool/selector.ts +184 -41
- package/src/network/ssrf.ts +7 -7
- package/src/network/{retry.ts → util/retry.ts} +1 -1
- package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
- package/src/observability/event-log.ts +7 -361
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +16 -26
- package/src/observability/telemetry-reconciliation.ts +9 -37
- package/src/observability/telemetry-row.ts +125 -0
- package/src/persistence/db-mode.ts +4 -1
- package/src/persistence/dialect.ts +18 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +17 -0
- package/src/persistence/ttl-cache.ts +21 -3
- package/src/protocol/codecs.ts +59 -2
- package/src/protocol/dsml.ts +34 -86
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/request/chat.ts +10 -10
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +34 -22
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/messages.ts +34 -7
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +15 -0
- package/src/protocol/upstream/send-claude-messages.ts +2 -2
- package/src/providers/adapters/compatible-adapter.ts +76 -12
- package/src/providers/authentication/oauth-client.ts +40 -6
- package/src/providers/authentication/oauth-device-flow.ts +105 -11
- package/src/providers/authentication/oauth-flow-store.ts +82 -1
- package/src/providers/authentication/oauth-refresh-service.ts +766 -758
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +137 -36
- package/src/providers/discovery/discovery-types.ts +7 -0
- package/src/providers/discovery/probe-phases.ts +20 -6
- package/src/providers/discovery/probing-service.ts +22 -2
- package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
- package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
- package/src/providers/integrations/buddy/account.ts +969 -0
- package/src/providers/integrations/buddy/capabilities.ts +12 -12
- package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
- package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
- package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
- package/src/providers/integrations/buddy/headers.ts +311 -0
- package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
- package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline-oauth.ts +10 -5
- package/src/providers/integrations/cline/cline.ts +5 -6
- package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
- package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
- package/src/providers/integrations/clixad/provider.ts +145 -0
- package/src/providers/integrations/codex/codex-oauth.ts +8 -5
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
- package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
- package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
- package/src/providers/integrations/cursor/cursor-request.ts +26 -12
- package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
- package/src/providers/integrations/cursor/cursor.ts +574 -110
- package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
- package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
- package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
- package/src/providers/integrations/deepai/deepai-common.ts +145 -0
- package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
- package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
- package/src/providers/integrations/deepai/provider.ts +42 -0
- package/src/providers/integrations/devin/devin-auth.ts +7 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
- package/src/providers/integrations/factory/factory-oauth.ts +33 -51
- package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
- package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
- package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
- package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
- package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
- package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
- package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
- package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
- package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
- package/src/providers/integrations/freebuff/freebuff.ts +682 -0
- package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
- package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
- package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
- package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
- package/src/providers/integrations/freepi/freepi-session.ts +158 -0
- package/src/providers/integrations/freepi/freepi.ts +80 -54
- package/src/providers/integrations/github/github-discovery.ts +30 -13
- package/src/providers/integrations/github/github-oauth.ts +217 -44
- package/src/providers/integrations/github/github-quota.ts +22 -5
- package/src/providers/integrations/github/github.ts +40 -32
- package/src/providers/integrations/grok/grok-oauth.ts +52 -49
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
- package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
- package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
- package/src/providers/integrations/kilo/kilo.ts +1 -1
- package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
- package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
- package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
- package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/muse/muse-oauth.ts +5 -5
- package/src/providers/integrations/ollamacloud/index.ts +2 -0
- package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
- package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/openrouter/provider.ts +1 -1
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-protocol.ts +0 -6
- package/src/providers/integrations/perch/perch-quota.ts +2 -16
- package/src/providers/integrations/perch/perch.ts +6 -16
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
- package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
- package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
- package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
- package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
- package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
- package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
- package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
- package/src/providers/integrations/qoder/qoder-types.ts +9 -0
- package/src/providers/integrations/qoder/qoder.ts +3 -26
- package/src/providers/integrations/sensenova/provider.ts +144 -0
- package/src/providers/integrations/trae/trae-protocol.ts +9 -5
- package/src/providers/integrations/trae/trae-quota.ts +21 -29
- package/src/providers/integrations/trae/trae-realm.ts +38 -0
- package/src/providers/integrations/trae/trae.ts +7 -24
- package/src/providers/integrations/vercel/provider.ts +117 -0
- package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
- package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
- package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
- package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
- package/src/providers/integrations/zro/zro.ts +205 -0
- package/src/providers/operations/account-error-classification.ts +199 -307
- package/src/providers/operations/account-health-service.ts +82 -1
- package/src/providers/operations/account-refresh-status.ts +36 -37
- package/src/providers/operations/account-tier.ts +15 -16
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +100 -81
- package/src/providers/operations/cooldown-rules.ts +11 -16
- package/src/providers/operations/oauth-refresh-lead.ts +46 -22
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +118 -51
- package/src/providers/operations/provider-rules/github.ts +48 -0
- package/src/providers/operations/provider-rules/grok.ts +33 -0
- package/src/providers/operations/provider-rules/inception.ts +49 -0
- package/src/providers/operations/provider-rules/index.ts +32 -0
- package/src/providers/operations/provider-rules/perch.ts +37 -0
- package/src/providers/operations/provider-rules/qoder.ts +70 -0
- package/src/providers/operations/provider-rules/trae.ts +81 -0
- package/src/providers/operations/session-resolution.ts +17 -1
- package/src/providers/provider-contracts.ts +6 -4
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +70 -10
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-contracts.ts +2 -0
- package/src/providers/quota/quota-limit-windows.ts +16 -5
- package/src/providers/quota/quota-support.ts +1 -1
- package/src/providers/quota/quota-window-parser.ts +90 -18
- package/src/providers/quota/refresh.ts +100 -21
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/security/ip-boundary.ts +1 -1
- package/src/transport/dispatch/attempt-finalize.ts +15 -14
- package/src/transport/dispatch/attempt-loop.ts +73 -19
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/handler-deps.ts +1 -1
- package/src/transport/dispatch/leases.ts +1 -1
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +8 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/retry-policy.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +10 -7
- package/src/transport/dispatch/streaming/stream.ts +9 -37
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +63 -0
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +16 -30
- package/src/transport/guards/body-policy.ts +7 -1
- package/src/transport/guards/error-lifecycle.ts +13 -1
- package/src/transport/guards/request-context.ts +22 -14
- package/src/transport/request/preparer.ts +81 -71
- package/src/transport/routing/bounded-lru.ts +14 -0
- package/src/transport/routing/drain-reserve.ts +129 -0
- package/src/transport/routing/plan-gate.ts +7 -1
- package/src/transport/routing/round-robin.ts +60 -0
- package/src/transport/routing/route-catalog.ts +123 -111
- package/src/transport/routing/route-model.ts +51 -9
- package/src/transport/routing/router.ts +393 -32
- package/src/transport/shutdown-notice.ts +1 -1
- package/src/transport/surface/adapters.ts +11 -2
- package/src/transport/surface/chat/adapter.ts +2 -2
- package/src/transport/surface/chat/encode.ts +47 -4
- package/src/transport/surface/chat/parse.ts +2 -12
- package/src/transport/surface/completion.ts +44 -33
- package/src/transport/surface/content-parts.ts +21 -0
- package/src/transport/surface/messages/adapter.ts +7 -1
- package/src/transport/surface/messages/encode.ts +11 -3
- package/src/transport/surface/messages/parse.ts +2 -1
- package/src/transport/surface/messages/stream.ts +33 -17
- package/src/transport/surface/responses/encode.ts +22 -5
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/buddy-growth-tasks.ts +8 -8
- package/src/workers/daily-checkin.ts +4 -4
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/zcode-gift-claim.ts +1 -1
- package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
- package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
- package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
- package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
- package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
- package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
- package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
- package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
- package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
- package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
- package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
- package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
- package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
- package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
- package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
- package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
- package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
- package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
- package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
- package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
- package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
- package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
- package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
- package/src/network/pool-health.ts +0 -210
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
- package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
- package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
- package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
- package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
- package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
- package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
- package/src/providers/integrations/qoder/chat-proto.ts +0 -328
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
- package/src/providers/operations/model-discovery-cache.ts +0 -34
- package/src/providers/operations/probe-fetch.ts +0 -15
- package/src/providers/operations/provider-version-cache.ts +0 -30
- package/src/providers/provider-capabilities.ts +0 -33
- package/src/providers/provider-credential-envelope.ts +0 -43
- /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
- /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
- /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
- /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
- /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
- /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
- /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
- /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
|
@@ -78,6 +78,38 @@ function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOpt
|
|
|
78
78
|
return { limit, offset, sortBy, order, status };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Drops the auth-cache snapshot for a key family: the parent first, then
|
|
83
|
+
* every child.
|
|
84
|
+
*
|
|
85
|
+
* Parent policy is live-inherited by children at auth time, so their cached
|
|
86
|
+
* snapshots must die with the parent — otherwise a denylist / budget edit
|
|
87
|
+
* looks like a no-op for up to AUTH_CACHE_TTL_MS per child.
|
|
88
|
+
*/
|
|
89
|
+
function invalidateFamilyCaches(keyId: string, children: readonly ApiKeyRecord[]): void {
|
|
90
|
+
invalidateApiKeyCache(keyId);
|
|
91
|
+
for (const child of children) invalidateApiKeyCache(child.id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Purges admission counters for a key family: the parent first, then every
|
|
96
|
+
* child.
|
|
97
|
+
*
|
|
98
|
+
* Share children admit under the parent id, so the parent's counter must die
|
|
99
|
+
* with a revoked/regenerated parent too — otherwise a family budget can be
|
|
100
|
+
* bypassed by a counter seeded under the old credential.
|
|
101
|
+
*/
|
|
102
|
+
async function purgeFamilyAdmission(
|
|
103
|
+
admissionService: { purgeKey(apiKeyId: string): Promise<void> },
|
|
104
|
+
keyId: string,
|
|
105
|
+
children: readonly ApiKeyRecord[],
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
await Promise.all([
|
|
108
|
+
admissionService.purgeKey(keyId),
|
|
109
|
+
...children.map((child) => admissionService.purgeKey(child.id)),
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
|
|
81
113
|
export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
82
114
|
const operations = {
|
|
83
115
|
async listKeys(access: AccessDecision | undefined, includeChildren = false): Promise<ApiKeyResponse[]> {
|
|
@@ -92,17 +124,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
92
124
|
const visible = includeChildren
|
|
93
125
|
? records.filter((record) => record.revokedAt === undefined)
|
|
94
126
|
: topLevel;
|
|
95
|
-
|
|
96
|
-
// every recipient commit adds to it (see ApiKeyAdmissionService
|
|
97
|
-
// commitUsage), so reporting it raw gives the enforced family total.
|
|
98
|
-
// No telemetry add-on is needed, and adding it here would double-count.
|
|
99
|
-
return Promise.all(
|
|
100
|
-
visible.map(async (record) => {
|
|
101
|
-
const response = sanitizeApiKeyResponse(record);
|
|
102
|
-
if (record.keyMode !== "share") return response;
|
|
103
|
-
return response;
|
|
104
|
-
}),
|
|
105
|
-
);
|
|
127
|
+
return visible.map(sanitizeApiKeyResponse);
|
|
106
128
|
},
|
|
107
129
|
async listActiveBannedKeys(access: AccessDecision | undefined): Promise<ApiKeyResponse[]> {
|
|
108
130
|
const authorized = requireTenantScope(access, "dashboard:read");
|
|
@@ -343,11 +365,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
343
365
|
});
|
|
344
366
|
if (!updated) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
345
367
|
const children = await config.store.listChildren(authorized.tenantId, keyId);
|
|
346
|
-
|
|
347
|
-
// cached snapshots must die with the parent — otherwise a denylist /
|
|
348
|
-
// budget edit looks like a no-op for up to AUTH_CACHE_TTL_MS per child.
|
|
349
|
-
invalidateApiKeyCache(keyId);
|
|
350
|
-
for (const child of children) invalidateApiKeyCache(child.id);
|
|
368
|
+
invalidateFamilyCaches(keyId, children);
|
|
351
369
|
// The allow/deny list and prefix decide which models `/v1/models`
|
|
352
370
|
// returns, which is a different cache from the auth snapshot: the
|
|
353
371
|
// listing is cached per (tenant, key, mode, list, prefix), so editing
|
|
@@ -397,10 +415,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
397
415
|
// Drop stale admission counters so a newly lowered one-time/recurring
|
|
398
416
|
// budget cannot be bypassed by a counter seeded under the looser limit.
|
|
399
417
|
// Share children admit under the parent id, so purge the parent too.
|
|
400
|
-
await
|
|
401
|
-
config.admissionService.purgeKey(keyId),
|
|
402
|
-
...children.map((child) => config.admissionService.purgeKey(child.id)),
|
|
403
|
-
]);
|
|
418
|
+
await purgeFamilyAdmission(config.admissionService, keyId, children);
|
|
404
419
|
}
|
|
405
420
|
// Adding a daily/monthly ceiling to a key that already spent in the
|
|
406
421
|
// current bucket must not hand it a full fresh budget: seed the counter
|
|
@@ -447,12 +462,8 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
447
462
|
const children = await config.store.listChildren(authorized.tenantId, keyId);
|
|
448
463
|
const removed = await config.store.remove(authorized.tenantId, keyId);
|
|
449
464
|
if (!removed) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
await Promise.all([
|
|
453
|
-
config.admissionService.purgeKey(keyId),
|
|
454
|
-
...children.map((child) => config.admissionService.purgeKey(child.id)),
|
|
455
|
-
]);
|
|
465
|
+
invalidateFamilyCaches(keyId, children);
|
|
466
|
+
await purgeFamilyAdmission(config.admissionService, keyId, children);
|
|
456
467
|
await config.auditSink?.record({
|
|
457
468
|
access: authorized,
|
|
458
469
|
action: "api_key.deleted",
|
|
@@ -626,8 +637,9 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
626
637
|
keyPrefix: generated.prefix,
|
|
627
638
|
});
|
|
628
639
|
if (!updated) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
629
|
-
|
|
630
|
-
|
|
640
|
+
// Regeneration is personal-key only, so the family is the key itself.
|
|
641
|
+
invalidateFamilyCaches(keyId, []);
|
|
642
|
+
await purgeFamilyAdmission(config.admissionService, keyId, []);
|
|
631
643
|
const shareStore = config.shareStore;
|
|
632
644
|
let share: ShareKeyResponse | null = null;
|
|
633
645
|
if (shareStore) {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* the operator to type it rather than press Enter on a menu row.
|
|
24
24
|
*/
|
|
25
25
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
26
|
-
import {
|
|
26
|
+
import { requireLoopbackAdmin } from "../../transport/drain-endpoint";
|
|
27
27
|
|
|
28
28
|
interface ConsoleAccountResetDeps {
|
|
29
29
|
/** Shared secret; the route is registered only when this is set. */
|
|
@@ -40,14 +40,12 @@ export function createConsoleAccountResetHandler(
|
|
|
40
40
|
deps: ConsoleAccountResetDeps,
|
|
41
41
|
): (context: { request: Request; body: unknown }) => Promise<Response> {
|
|
42
42
|
return async ({ request, body }) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
|
|
50
|
-
throw new GatewayError("authentication_failed", 401, "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑");
|
|
43
|
+
requireLoopbackAdmin(request, {
|
|
44
|
+
peer: deps.resolvePeerAddress(request),
|
|
45
|
+
token: deps.token,
|
|
46
|
+
policyRejectedMessage: "console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
|
|
47
|
+
authenticationFailedMessage: "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑",
|
|
48
|
+
});
|
|
51
49
|
const confirm =
|
|
52
50
|
body !== null && typeof body === "object" && "confirm" in body
|
|
53
51
|
? body.confirm
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* as sensitive as the database.
|
|
21
21
|
*/
|
|
22
22
|
import { getTableColumns, getTableName, type Column, type Table } from "drizzle-orm";
|
|
23
|
+
import type { BackupInspection } from "./service";
|
|
24
|
+
import type { DeleteAllScope } from "./store";
|
|
23
25
|
import { resolveMaxBodyBytes } from "../../config/runtime";
|
|
24
26
|
import {
|
|
25
27
|
apiKeys,
|
|
@@ -393,6 +395,8 @@ export type BackupSection = "config" | "telemetry";
|
|
|
393
395
|
|
|
394
396
|
export const BACKUP_SECTIONS: readonly BackupSection[] = ["config", "telemetry"];
|
|
395
397
|
|
|
398
|
+
export type { BackupInspection, DeleteAllScope };
|
|
399
|
+
|
|
396
400
|
export interface BackupPayload {
|
|
397
401
|
readonly app: typeof BACKUP_APP;
|
|
398
402
|
readonly version: number;
|
|
@@ -22,7 +22,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
25
|
-
import {
|
|
25
|
+
import { requireLoopbackAdmin } from "../../transport/drain-endpoint";
|
|
26
26
|
import { tablesForSection } from "./contracts";
|
|
27
27
|
import { compressBackupStream, exportBackupStream } from "./store";
|
|
28
28
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
@@ -52,10 +52,12 @@ export function createBackupExportHandler(
|
|
|
52
52
|
// both: neither refusal is about the body. The first is where the request
|
|
53
53
|
// came from, the second is the credential it carried — and `invalid_request`
|
|
54
54
|
// told an operator to fix a body that was never the problem.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
requireLoopbackAdmin(request, {
|
|
56
|
+
peer: deps.resolvePeerAddress(request),
|
|
57
|
+
token: deps.token,
|
|
58
|
+
policyRejectedMessage: "backup export is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
|
|
59
|
+
authenticationFailedMessage: "invalid export token (request {requestId}). Wrong key, right door — check the token 🔑",
|
|
60
|
+
});
|
|
59
61
|
|
|
60
62
|
const directory =
|
|
61
63
|
body !== null && typeof body === "object" && "directory" in body
|
|
@@ -23,6 +23,7 @@ import type { BackupPayload, BackupRow } from "./contracts";
|
|
|
23
23
|
import { BACKUP_APP, BACKUP_VERSION } from "./contracts";
|
|
24
24
|
import { isRecord } from "../../protocol/codecs";
|
|
25
25
|
import { hashSecret } from "../../security/crypto";
|
|
26
|
+
import { resolveGithubCopilotMode, resolveGithubEnterpriseDomain } from "../../config/providers";
|
|
26
27
|
import {
|
|
27
28
|
AUTH_STATE_KEYS,
|
|
28
29
|
COMBO_STRATEGIES,
|
|
@@ -100,6 +101,24 @@ interface ResolvedConnectionCredential {
|
|
|
100
101
|
readonly value: string;
|
|
101
102
|
readonly kind: "api_key" | "oauth";
|
|
102
103
|
}
|
|
104
|
+
|
|
105
|
+
/** The provider-specific rebinding a router export may need before it becomes our envelope. */
|
|
106
|
+
export interface ImportedProviderCredential {
|
|
107
|
+
readonly credential: string;
|
|
108
|
+
readonly profile: "opencode-copilot" | "vscode-client-real";
|
|
109
|
+
readonly refreshToken?: string;
|
|
110
|
+
readonly expiresAt: Date;
|
|
111
|
+
readonly requiresRefresh: boolean;
|
|
112
|
+
readonly warnings: readonly string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ImportedProviderCredentialBinder = (
|
|
116
|
+
value: string,
|
|
117
|
+
refreshValue: string | null | undefined,
|
|
118
|
+
profile: "opencode-copilot" | "vscode-client-real",
|
|
119
|
+
domain: string,
|
|
120
|
+
expiresAt?: Date,
|
|
121
|
+
) => ImportedProviderCredential;
|
|
103
122
|
function credentialOf(entry: Row, provider: string): ResolvedConnectionCredential | null {
|
|
104
123
|
const apiKey = text(entry.apiKey);
|
|
105
124
|
const accessToken = text(entry.accessToken);
|
|
@@ -128,7 +147,7 @@ function credentialOf(entry: Row, provider: string): ResolvedConnectionCredentia
|
|
|
128
147
|
* we have no such provider — a set entry with no provider behind it would
|
|
129
148
|
* classify a row that can never import.
|
|
130
149
|
*/
|
|
131
|
-
const OAUTH_PROVIDERS = new Set(["claude", "codex", "antigravity", "kimi", "cline"]);
|
|
150
|
+
const OAUTH_PROVIDERS = new Set(["claude", "codex", "antigravity", "kimi", "cline", "github"]);
|
|
132
151
|
|
|
133
152
|
/** The non-secret per-account config, as our `auth_state` bag. */
|
|
134
153
|
function authStateOf(entry: Row): Record<string, unknown> {
|
|
@@ -223,7 +242,11 @@ function modelReference(
|
|
|
223
242
|
* leaving them null would make them global, i.e. visible to every tenant, which
|
|
224
243
|
* an import must never do implicitly.
|
|
225
244
|
*/
|
|
226
|
-
export function convert9RouterBackup(
|
|
245
|
+
export function convert9RouterBackup(
|
|
246
|
+
input: unknown,
|
|
247
|
+
tenantId: string,
|
|
248
|
+
importBinders: Readonly<Record<string, ImportedProviderCredentialBinder>> = {},
|
|
249
|
+
): ConversionResult {
|
|
227
250
|
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
228
251
|
throw new Error("router backup must be a JSON object");
|
|
229
252
|
}
|
|
@@ -273,6 +296,32 @@ export function convert9RouterBackup(input: unknown, tenantId: string): Conversi
|
|
|
273
296
|
}
|
|
274
297
|
const accountId = crypto.randomUUID();
|
|
275
298
|
const kind = credential.kind;
|
|
299
|
+
const specific = isRecord(entry.providerSpecificData) ? entry.providerSpecificData : {};
|
|
300
|
+
const importedRefresh = text(entry.refreshToken);
|
|
301
|
+
const sourceExpiry = typeof entry.expiresAt === "string" && Number.isFinite(Date.parse(entry.expiresAt))
|
|
302
|
+
? new Date(entry.expiresAt)
|
|
303
|
+
: undefined;
|
|
304
|
+
const importBinder = provider === "github" && kind === "oauth" ? importBinders.github : undefined;
|
|
305
|
+
let githubBinding: ImportedProviderCredential | undefined;
|
|
306
|
+
if (importBinder !== undefined) {
|
|
307
|
+
try {
|
|
308
|
+
githubBinding = importBinder(
|
|
309
|
+
credential.value,
|
|
310
|
+
importedRefresh,
|
|
311
|
+
resolveGithubCopilotMode(),
|
|
312
|
+
resolveGithubEnterpriseDomain() || "github.com",
|
|
313
|
+
sourceExpiry,
|
|
314
|
+
);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
// One malformed row must not take the rest of the backup down with it.
|
|
317
|
+
// The row is reported and left out; the operator can re-authenticate it.
|
|
318
|
+
skipped.push(`${label}: GitHub Copilot credential could not be bound: ${error instanceof Error ? error.message : String(error)}`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const storedCredential = githubBinding?.credential ?? credential.value;
|
|
323
|
+
const storedRefresh = githubBinding?.refreshToken ?? importedRefresh;
|
|
324
|
+
const storedAuthState = authStateOf(entry).auth_state;
|
|
276
325
|
accounts.push({
|
|
277
326
|
id: accountId,
|
|
278
327
|
provider_id: provider,
|
|
@@ -282,7 +331,7 @@ export function convert9RouterBackup(input: unknown, tenantId: string): Conversi
|
|
|
282
331
|
// `SEALED_COLUMNS`. The store re-seals it under this instance's key on the
|
|
283
332
|
// way in; sealing it here would put ciphertext in a payload whose only
|
|
284
333
|
// portable form is the plaintext, and the restore would drop it.
|
|
285
|
-
access_token:
|
|
334
|
+
access_token: storedCredential,
|
|
286
335
|
credential_kind: kind,
|
|
287
336
|
status: bool(entry.isActive, true) ? "active" : "disabled",
|
|
288
337
|
consecutive_failures: 0,
|
|
@@ -292,29 +341,41 @@ export function convert9RouterBackup(input: unknown, tenantId: string): Conversi
|
|
|
292
341
|
// rather than invented — a lower number means the operator wants it used
|
|
293
342
|
// first, and the column means the same thing.
|
|
294
343
|
sort_index: typeof entry.priority === "number" && Number.isFinite(entry.priority) ? entry.priority : 0,
|
|
295
|
-
...(
|
|
344
|
+
...(githubBinding === undefined
|
|
345
|
+
? (storedAuthState === undefined ? {} : { auth_state: storedAuthState })
|
|
346
|
+
: {
|
|
347
|
+
auth_state: {
|
|
348
|
+
...(isRecord(storedAuthState) ? storedAuthState : {}),
|
|
349
|
+
github_copilot_profile: githubBinding.profile,
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
296
352
|
...(emailOf(entry)),
|
|
297
353
|
created_at: { __date: isoDate(entry.createdAt, now) },
|
|
298
354
|
});
|
|
299
355
|
connectedProviders.add(provider);
|
|
300
356
|
// The refresh token is a second secret on the same account, so it gets its
|
|
301
357
|
// own sealed row rather than being folded into the credential.
|
|
302
|
-
const specific = isRecord(entry.providerSpecificData) ? entry.providerSpecificData : {};
|
|
303
|
-
const refresh = text(entry.refreshToken);
|
|
304
358
|
const clientSecret = text(specific.clientSecret);
|
|
305
|
-
|
|
359
|
+
for (const warning of githubBinding?.warnings ?? []) {
|
|
360
|
+
warnings.push(`account "${label}": ${warning}`);
|
|
361
|
+
}
|
|
362
|
+
if (storedRefresh !== null || clientSecret !== null || githubBinding?.requiresRefresh === true) {
|
|
306
363
|
// A companion secret authorizes a refresh, so on its own it has nothing to
|
|
307
364
|
// authorize. It is still carried rather than dropped, and the report says
|
|
308
365
|
// why it is inert — a secret lost without a word is how an operator
|
|
309
366
|
// discovers the loss from a failed refresh days later.
|
|
310
|
-
if (clientSecret !== null &&
|
|
367
|
+
if (clientSecret !== null && storedRefresh === null) {
|
|
311
368
|
warnings.push(`account "${label}": carried a client secret with no refresh token; it cannot authorize a refresh`);
|
|
312
369
|
}
|
|
313
370
|
oauthStates.push({
|
|
314
371
|
provider_account_id: accountId,
|
|
315
|
-
...(
|
|
372
|
+
...(storedRefresh === null ? {} : { refresh_token: storedRefresh }),
|
|
316
373
|
...(clientSecret === null ? {} : { client_secret: clientSecret }),
|
|
317
|
-
...(
|
|
374
|
+
...(githubBinding !== undefined
|
|
375
|
+
? { expires_at: { __date: githubBinding.expiresAt.toISOString() } }
|
|
376
|
+
: typeof entry.expiresAt === "string"
|
|
377
|
+
? { expires_at: { __date: isoDate(entry.expiresAt, now) } }
|
|
378
|
+
: {}),
|
|
318
379
|
});
|
|
319
380
|
}
|
|
320
381
|
});
|
|
@@ -311,7 +311,14 @@ export class BackupService {
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
if (detected.kind === "nine_router") {
|
|
314
|
-
|
|
314
|
+
// dynamic-import: lazy OAuth graph — pulling the Github OAuth client into
|
|
315
|
+
// the console boot graph would make every console route pay for it.
|
|
316
|
+
const { bindImportedGithubCredential } = await import(
|
|
317
|
+
"../../providers/integrations/github/github-oauth"
|
|
318
|
+
);
|
|
319
|
+
const { payload: converted, report } = convert9RouterBackup(detected.payload, tenantId, {
|
|
320
|
+
github: bindImportedGithubCredential,
|
|
321
|
+
});
|
|
315
322
|
const result = await this.apply(converted, tenantId, selection);
|
|
316
323
|
return { ...result, report, format: "nine_router" };
|
|
317
324
|
}
|
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
CliMappingSettings,
|
|
9
9
|
DownloadResult,
|
|
10
10
|
ToolDef,
|
|
11
|
-
ToolInjector,
|
|
12
11
|
ToolStatus,
|
|
13
12
|
} from "./contracts";
|
|
14
13
|
import { TOOL_IDS, TOOL_REGISTRY, type ToolId, type ToolRegistryEntry } from "./contracts";
|
|
@@ -47,11 +46,6 @@ function toToolRegistryEntry(def: ToolDef): ToolRegistryEntry {
|
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
function injectorFor(toolId: string): ToolInjector | null {
|
|
51
|
-
const injector = INJECTORS[toolId as ToolId];
|
|
52
|
-
return injector ?? null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
49
|
/** Operator-facing summary of what an apply actually did. */
|
|
56
50
|
function applyMessage(outcome: ApplyOutcome, toolId: string, fileMessage: string): string {
|
|
57
51
|
switch (outcome) {
|
|
@@ -139,8 +133,8 @@ export class CliToolService {
|
|
|
139
133
|
}
|
|
140
134
|
|
|
141
135
|
async getStatus(toolId: string): Promise<ToolStatus | null> {
|
|
142
|
-
const injector =
|
|
143
|
-
if (injector ===
|
|
136
|
+
const injector = INJECTORS[toolId as ToolId];
|
|
137
|
+
if (injector === undefined) return null;
|
|
144
138
|
return injector.getStatus();
|
|
145
139
|
}
|
|
146
140
|
|
|
@@ -249,8 +243,8 @@ export class CliToolService {
|
|
|
249
243
|
}
|
|
250
244
|
|
|
251
245
|
async downloadConfig(toolId: string, input: ApplyInput): Promise<DownloadResult | null> {
|
|
252
|
-
const injector =
|
|
253
|
-
if (injector ===
|
|
246
|
+
const injector = INJECTORS[toolId as ToolId];
|
|
247
|
+
if (injector === undefined) return null;
|
|
254
248
|
if (!input.endpoint || !input.apiKey) return null;
|
|
255
249
|
try {
|
|
256
250
|
return await injector.download(input);
|
|
@@ -281,8 +275,8 @@ export class CliToolService {
|
|
|
281
275
|
apiKeyId: string,
|
|
282
276
|
input: ApplyInput & { mode?: "file" | "remote" | "both" },
|
|
283
277
|
): Promise<ApplyConfigResult | null> {
|
|
284
|
-
const injector =
|
|
285
|
-
if (injector ===
|
|
278
|
+
const injector = INJECTORS[toolId as ToolId];
|
|
279
|
+
if (injector === undefined) return null;
|
|
286
280
|
const mode = input.mode ?? "both";
|
|
287
281
|
const wantsFile = mode === "file" || mode === "both";
|
|
288
282
|
const wantsRemote = mode === "remote" || mode === "both";
|
|
@@ -11,6 +11,27 @@ import { GatewayError } from "../protocol/gateway-error";
|
|
|
11
11
|
import { fastPathname } from "../transport/pathname";
|
|
12
12
|
import { parseCookieValue, SESSION_COOKIE_NAME, isCsrfValid } from "../security/csrf";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Console auth routes are excluded from CSRF and mutation-rate limiting: they
|
|
16
|
+
* run before a session exists or are the very requests that mint the session,
|
|
17
|
+
* so they must not require a CSRF token or count against the session's budget.
|
|
18
|
+
*/
|
|
19
|
+
const AUTH_ROUTE_SUFFIXES = ["/auth/login", "/auth/setup", "/auth/setup/restore", "/auth/session"] as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether `request` is a protected console mutation: a `/console/api/*` route
|
|
23
|
+
* in an unsafe method, excluding the auth-route suffixes. Shared by the CSRF
|
|
24
|
+
* and mutation-limiter guards so a change to the protected set lands in both.
|
|
25
|
+
*/
|
|
26
|
+
function isProtectedConsoleMutation(request: Request): boolean {
|
|
27
|
+
const path = fastPathname(request.url);
|
|
28
|
+
return (
|
|
29
|
+
path.startsWith("/console/api/") &&
|
|
30
|
+
["POST", "PUT", "PATCH", "DELETE"].includes(request.method) &&
|
|
31
|
+
!AUTH_ROUTE_SUFFIXES.some((suffix) => path.endsWith(suffix))
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
/**
|
|
15
36
|
* Stateless double-submit CSRF guard for unsafe `/console/api/*` mutations.
|
|
16
37
|
* Compares the `x-csrf-token` header against the readable `csrf_token`
|
|
@@ -19,16 +40,9 @@ import { parseCookieValue, SESSION_COOKIE_NAME, isCsrfValid } from "../security/
|
|
|
19
40
|
* store whose outage could masquerade as an auth failure.
|
|
20
41
|
*/
|
|
21
42
|
export function createConsoleCsrfMiddleware(): Elysia {
|
|
22
|
-
const excluded = ["/auth/login", "/auth/setup", "/auth/setup/restore", "/auth/session"];
|
|
23
43
|
return new Elysia()
|
|
24
44
|
.beforeHandle(async ({ request }) => {
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
!path.startsWith("/console/api/") ||
|
|
28
|
-
!["POST", "PUT", "PATCH", "DELETE"].includes(request.method) ||
|
|
29
|
-
excluded.some((suffix) => path.endsWith(suffix))
|
|
30
|
-
)
|
|
31
|
-
return;
|
|
45
|
+
if (!isProtectedConsoleMutation(request)) return;
|
|
32
46
|
const token = parseCookieValue(request, SESSION_COOKIE_NAME);
|
|
33
47
|
// No session cookie means no ambient authority to forge: logout and
|
|
34
48
|
// other mutations without a session fail authentication downstream.
|
|
@@ -75,16 +89,9 @@ function pruneConsoleMutationHits(now: number): void {
|
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
export function createConsoleMutationLimiterMiddleware(): Elysia {
|
|
78
|
-
const excluded = ["/auth/login", "/auth/setup", "/auth/setup/restore", "/auth/session"];
|
|
79
92
|
return new Elysia()
|
|
80
93
|
.beforeHandle(async ({ request, set }) => {
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
!path.startsWith("/console/api/") ||
|
|
84
|
-
!["POST", "PUT", "PATCH", "DELETE"].includes(request.method) ||
|
|
85
|
-
excluded.some((suffix) => path.endsWith(suffix))
|
|
86
|
-
)
|
|
87
|
-
return;
|
|
94
|
+
if (!isProtectedConsoleMutation(request)) return;
|
|
88
95
|
const key = consoleMutationKey(request);
|
|
89
96
|
// No session cookie means no ambient authority to abuse: the request
|
|
90
97
|
// fails authentication downstream. Only tracked sessions are throttled.
|
|
@@ -33,7 +33,7 @@ import { DrizzleCliToolSecretSource } from "./cli-tools/secret-source";
|
|
|
33
33
|
import type { ShareLinkStore } from "../persistence/stores/share-store";
|
|
34
34
|
import type { ProviderRegistry } from "../providers/provider-registry";
|
|
35
35
|
import type { BundledProviderCatalog } from "../providers/operations/provider-catalog-service";
|
|
36
|
-
import type { ValidatedNetworkBindingFactory } from "../network/
|
|
36
|
+
import type { ValidatedNetworkBindingFactory } from "../network/egress/factory";
|
|
37
37
|
import type { TrustedProxyBoundary } from "../config/network";
|
|
38
38
|
import type { IpAbuseStore } from "../security/abuse";
|
|
39
39
|
import { gzipResponse } from "./response-compression";
|
|
@@ -38,7 +38,6 @@ import { createAuditRoutes } from "./audit/routes";
|
|
|
38
38
|
import { DrizzleAuditReadStore } from "./audit/store";
|
|
39
39
|
import { createIpBanRoutes } from "./ip-bans/routes";
|
|
40
40
|
import type { IpAbuseStore } from "../security/abuse";
|
|
41
|
-
import { createPerformanceRoutes } from "./observability/performance";
|
|
42
41
|
import { createUpdateRoutes } from "./updates/routes";
|
|
43
42
|
import { createRemoteSupportRoutes } from "./remote-support/routes";
|
|
44
43
|
import { createRemoteSupportService } from "./remote-support/service";
|
|
@@ -66,7 +65,7 @@ import {
|
|
|
66
65
|
} from "../persistence/stores/share-store";
|
|
67
66
|
import { createShareUsagePort } from "./share/share-usage";
|
|
68
67
|
import { resolveSsrfPolicy } from "../config/network";
|
|
69
|
-
import { createValidatedFetch } from "../network/
|
|
68
|
+
import { createValidatedFetch } from "../network/egress/fetch";
|
|
70
69
|
import type { AuditRecorder } from "./auth/audit-recorder";
|
|
71
70
|
import type { CliToolService } from "./cli-tools/service";
|
|
72
71
|
import type { NetworkPoolSelector } from "../network/pool/selector";
|
|
@@ -78,7 +77,7 @@ import { resolveRedisBackend } from "../persistence/redis";
|
|
|
78
77
|
import type { RedisClient } from "../persistence/redis";
|
|
79
78
|
import type { ProviderRegistry } from "../providers/provider-registry";
|
|
80
79
|
import type { BundledProviderCatalog } from "../providers/operations/provider-catalog-service";
|
|
81
|
-
import type { ValidatedNetworkBindingFactory } from "../network/
|
|
80
|
+
import type { ValidatedNetworkBindingFactory } from "../network/egress/factory";
|
|
82
81
|
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
83
82
|
import type { ConsoleAccessResolver } from "./auth/access";
|
|
84
83
|
|
|
@@ -278,7 +277,6 @@ export function registerConsoleDomains(
|
|
|
278
277
|
...(ctx.stateStore ? { stateStore: ctx.stateStore } : {}),
|
|
279
278
|
}));
|
|
280
279
|
console.use(createLogRoutes({ accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder }));
|
|
281
|
-
console.use(createPerformanceRoutes({ accessResolver: ctx.accessResolver }));
|
|
282
280
|
console.use(createUpdateRoutes({
|
|
283
281
|
accessResolver: ctx.accessResolver,
|
|
284
282
|
observabilityStore,
|
|
@@ -436,6 +434,11 @@ export function registerConsoleDomains(
|
|
|
436
434
|
accessResolver: ctx.accessResolver,
|
|
437
435
|
snapshotInvalidator: ctx.routeSnapshotService,
|
|
438
436
|
callbackListener: oauthCallbackListener,
|
|
437
|
+
deviceEgress: {
|
|
438
|
+
db: ctx.db,
|
|
439
|
+
poolSelector: ctx.poolSelector,
|
|
440
|
+
networkBindingFactory: ctx.networkBindingFactory,
|
|
441
|
+
},
|
|
439
442
|
};
|
|
440
443
|
console.use(createOAuthLoginRoutes(oauthConfig));
|
|
441
444
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* and a failure returns `undefined` — never a half-parsed object that a route
|
|
14
14
|
* would render as if it were good.
|
|
15
15
|
*/
|
|
16
|
-
import { createValidatedFetch, type ValidatedFetch } from "../../network/
|
|
16
|
+
import { createValidatedFetch, type ValidatedFetch } from "../../network/egress/fetch";
|
|
17
17
|
|
|
18
18
|
export const INFERHUB_API_BASE = "https://inferhub.dev/api";
|
|
19
19
|
|
|
@@ -35,19 +35,12 @@ const FAMILY_STATES: readonly FamilyState[] = [
|
|
|
35
35
|
"low_traffic",
|
|
36
36
|
];
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const parsed = Number(value);
|
|
45
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Reads a finite number, or null. Never coerces a missing metric to zero. */
|
|
38
|
+
/**
|
|
39
|
+
* Reads a finite number, or null. Never coerces a missing metric to zero.
|
|
40
|
+
*
|
|
41
|
+
* Catalog and budgets carry decimal strings; market carries numbers. Both
|
|
42
|
+
* are read here so one helper covers the wire's own inconsistency.
|
|
43
|
+
*/
|
|
51
44
|
export function numOrNull(value: unknown): number | null {
|
|
52
45
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
53
46
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
@@ -122,8 +115,8 @@ function pricePointsToTiers(
|
|
|
122
115
|
const seen = new Map<number, Tier>();
|
|
123
116
|
for (const point of points) {
|
|
124
117
|
if (!Array.isArray(point) || point.length < 2) continue;
|
|
125
|
-
const price =
|
|
126
|
-
const avail =
|
|
118
|
+
const price = numOrNull(point[0]);
|
|
119
|
+
const avail = numOrNull(point[1]);
|
|
127
120
|
if (price === null || avail === null) continue;
|
|
128
121
|
const count = Math.max(0, Math.trunc(avail));
|
|
129
122
|
const key = Math.round(price * 10 ** PRICE_PRECISION) / 10 ** PRICE_PRECISION;
|
|
@@ -208,8 +201,8 @@ export function normalizeCatalogModel(
|
|
|
208
201
|
const prefix = str(upstream.prefix) ?? "";
|
|
209
202
|
if (modelSlug === null || prefix.length === 0) return null;
|
|
210
203
|
const id = `${prefix}/${modelSlug}`;
|
|
211
|
-
const officialIn =
|
|
212
|
-
const officialOut =
|
|
204
|
+
const officialIn = numOrNull(raw.officialIn);
|
|
205
|
+
const officialOut = numOrNull(raw.officialOut);
|
|
213
206
|
const tiersIn = pricePointsToTiers(raw.pricePointsIn, officialIn);
|
|
214
207
|
const tiersOut = pricePointsToTiers(raw.pricePointsOut, officialOut);
|
|
215
208
|
// Live providers is the capacity behind the cheapest tier, not a row count:
|
|
@@ -234,10 +227,13 @@ export function normalizeCatalogModel(
|
|
|
234
227
|
minAskOut: tiersOut[0]?.price ?? null,
|
|
235
228
|
maxAskIn: tiersIn.at(-1)?.price ?? null,
|
|
236
229
|
maxAskOut: tiersOut.at(-1)?.price ?? null,
|
|
230
|
+
// Computed by the route layer before sending; starts null in internal models.
|
|
231
|
+
capacityPriceIn: null,
|
|
232
|
+
capacityPriceOut: null,
|
|
237
233
|
providersLive,
|
|
238
234
|
// Not carried by the catalog; merged from `/api/market` by the store.
|
|
239
235
|
lastRate: null,
|
|
240
|
-
cacheHitRate:
|
|
236
|
+
cacheHitRate: numOrNull(raw.cacheHitRate),
|
|
241
237
|
health: {
|
|
242
238
|
successRatePct: null,
|
|
243
239
|
avgTtftMs: null,
|
|
@@ -245,8 +241,8 @@ export function normalizeCatalogModel(
|
|
|
245
241
|
familyState: null,
|
|
246
242
|
},
|
|
247
243
|
capabilities: {
|
|
248
|
-
contextWindow:
|
|
249
|
-
maxOutputTokens:
|
|
244
|
+
contextWindow: numOrNull(raw.contextWindow),
|
|
245
|
+
maxOutputTokens: numOrNull(raw.maxOutputTokens),
|
|
250
246
|
modalities: stringArray(raw.modalities),
|
|
251
247
|
outputModality:
|
|
252
248
|
outputModality === "image" || outputModality === "video" ? outputModality : "text",
|
|
@@ -274,13 +270,13 @@ export function mergeMarketPrices(
|
|
|
274
270
|
if (entry === undefined) return model;
|
|
275
271
|
return {
|
|
276
272
|
...model,
|
|
277
|
-
lastRate:
|
|
273
|
+
lastRate: numOrNull(entry.lastRate),
|
|
278
274
|
// Market is the cheaper source for the extremes, but the catalog's tier
|
|
279
275
|
// grouping is what gives them capacity, so only the nulls are filled.
|
|
280
|
-
minAskIn: model.minAskIn ??
|
|
281
|
-
minAskOut: model.minAskOut ??
|
|
282
|
-
maxAskIn: model.maxAskIn ??
|
|
283
|
-
maxAskOut: model.maxAskOut ??
|
|
276
|
+
minAskIn: model.minAskIn ?? numOrNull(entry.minAskIn),
|
|
277
|
+
minAskOut: model.minAskOut ?? numOrNull(entry.minAskOut),
|
|
278
|
+
maxAskIn: model.maxAskIn ?? numOrNull(entry.maxAskIn),
|
|
279
|
+
maxAskOut: model.maxAskOut ?? numOrNull(entry.maxAskOut),
|
|
284
280
|
};
|
|
285
281
|
});
|
|
286
282
|
}
|
|
@@ -306,9 +302,9 @@ export function mergeModelHealth(
|
|
|
306
302
|
...model,
|
|
307
303
|
health: {
|
|
308
304
|
...model.health,
|
|
309
|
-
successRatePct:
|
|
310
|
-
avgTtftMs:
|
|
311
|
-
avgTps:
|
|
305
|
+
successRatePct: numOrNull(entry.successRatePct),
|
|
306
|
+
avgTtftMs: numOrNull(entry.avgTtftMs),
|
|
307
|
+
avgTps: numOrNull(entry.avgTps),
|
|
312
308
|
},
|
|
313
309
|
};
|
|
314
310
|
});
|
|
@@ -61,6 +61,9 @@ export interface ModelMarket {
|
|
|
61
61
|
readonly minAskOut: number | null;
|
|
62
62
|
readonly maxAskIn: number | null;
|
|
63
63
|
readonly maxAskOut: number | null;
|
|
64
|
+
/** Capacity-weighted price computed by the route layer; null when no tiers. */
|
|
65
|
+
readonly capacityPriceIn: number | null;
|
|
66
|
+
readonly capacityPriceOut: number | null;
|
|
64
67
|
/** Zero means no live provider — listed, but never routed to. */
|
|
65
68
|
readonly providersLive: number;
|
|
66
69
|
readonly lastRate: number | null;
|