cartethyia 2.7.9 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -2
- package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-BJ87Pp0U.js → ProxyPage-BwNC_-Cx.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
- package/dist/dashboard/assets/assert-CqTLw5e7.js +1 -0
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-CEL5Sg_J.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-HvBW0AjI.js} +1 -1
- package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
- package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-BFQX2VBc.js} +1 -1
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
- package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/package.json +1 -1
- package/src/boot/app.ts +10 -13
- package/src/boot/dependencies.ts +1 -1
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +20 -0
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +40 -28
- package/src/console/auth/account-reset.ts +7 -9
- package/src/console/backup/contracts.ts +4 -0
- package/src/console/backup/export-endpoint.ts +7 -5
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/domain-registration.ts +0 -2
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +7 -13
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/contracts.ts +2 -2
- package/src/console/providers/catalog/discover-constants.ts +6 -0
- package/src/console/providers/catalog/routes.ts +1 -0
- package/src/console/providers/catalog/store.ts +21 -16
- package/src/console/quota/account-quota-global.ts +14 -24
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +6 -18
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/operations.ts +31 -46
- package/src/console/shared/errors.ts +33 -6
- package/src/main.ts +2 -3
- package/src/network/pool/byte-accounting.ts +3 -2
- package/src/network/pool/selector.ts +182 -39
- package/src/network/pool-cooldown-store.ts +89 -14
- package/src/network/pool-health.ts +49 -61
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/ssrf.ts +6 -6
- package/src/observability/event-log.ts +7 -361
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +15 -25
- package/src/observability/telemetry-reconciliation.ts +9 -37
- package/src/observability/telemetry-row.ts +125 -0
- package/src/persistence/db-mode.ts +4 -1
- package/src/persistence/dialect.ts +18 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +8 -0
- package/src/persistence/ttl-cache.ts +20 -2
- package/src/protocol/codecs.ts +1 -1
- package/src/protocol/dsml.ts +34 -86
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/request/chat.ts +10 -10
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +3 -4
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/providers/adapters/compatible-adapter.ts +71 -12
- package/src/providers/authentication/oauth-client.ts +1 -1
- package/src/providers/authentication/oauth-device-flow.ts +98 -8
- package/src/providers/authentication/oauth-refresh-service.ts +30 -49
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +61 -36
- package/src/providers/discovery/probe-phases.ts +14 -4
- package/src/providers/discovery/probing-service.ts +15 -2
- package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
- package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +5 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
- package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
- package/src/providers/integrations/buddy/capabilities.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline.ts +2 -2
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
- package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
- package/src/providers/integrations/cursor/cursor-request.ts +26 -12
- package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
- package/src/providers/integrations/cursor/cursor.ts +89 -40
- package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
- package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
- package/src/providers/integrations/factory/factory-oauth.ts +17 -44
- package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
- package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
- package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
- package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
- package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
- package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
- package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
- package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
- package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
- package/src/providers/integrations/freebuff/freebuff.ts +682 -0
- package/src/providers/integrations/grok/grok-oauth.ts +48 -45
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
- package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +9 -9
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
- package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/ollamacloud/index.ts +2 -0
- package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
- package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
- package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-protocol.ts +0 -3
- package/src/providers/integrations/perch/perch-quota.ts +2 -16
- package/src/providers/integrations/perch/perch.ts +3 -13
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
- package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
- package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
- package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
- package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
- package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
- package/src/providers/integrations/qoder/qoder.ts +0 -1
- package/src/providers/integrations/trae/trae-protocol.ts +9 -5
- package/src/providers/integrations/trae/trae-quota.ts +6 -28
- package/src/providers/integrations/trae/trae-realm.ts +38 -0
- package/src/providers/integrations/trae/trae.ts +7 -24
- package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
- package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
- package/src/providers/integrations/zro/zro.ts +204 -0
- package/src/providers/operations/account-error-classification.ts +199 -307
- package/src/providers/operations/account-health-service.ts +82 -1
- package/src/providers/operations/account-refresh-status.ts +22 -37
- package/src/providers/operations/account-tier.ts +15 -16
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +5 -51
- package/src/providers/operations/cooldown-rules.ts +11 -16
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +102 -32
- package/src/providers/operations/provider-rules/github.ts +48 -0
- package/src/providers/operations/provider-rules/grok.ts +33 -0
- package/src/providers/operations/provider-rules/inception.ts +49 -0
- package/src/providers/operations/provider-rules/index.ts +32 -0
- package/src/providers/operations/provider-rules/perch.ts +37 -0
- package/src/providers/operations/provider-rules/qoder.ts +70 -0
- package/src/providers/operations/provider-rules/trae.ts +81 -0
- package/src/providers/provider-contracts.ts +4 -2
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +40 -10
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-limit-windows.ts +16 -5
- package/src/providers/quota/quota-support.ts +1 -1
- package/src/providers/quota/quota-window-parser.ts +90 -18
- package/src/providers/quota/refresh.ts +75 -18
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/transport/dispatch/attempt-finalize.ts +14 -13
- package/src/transport/dispatch/attempt-loop.ts +71 -17
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +8 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/streaming/finalize.ts +9 -6
- package/src/transport/dispatch/streaming/stream.ts +8 -36
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +63 -0
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +16 -30
- package/src/transport/guards/body-policy.ts +7 -1
- package/src/transport/guards/error-lifecycle.ts +13 -1
- package/src/transport/guards/request-context.ts +22 -14
- package/src/transport/request/preparer.ts +81 -71
- package/src/transport/routing/bounded-lru.ts +14 -0
- package/src/transport/routing/drain-reserve.ts +129 -0
- package/src/transport/routing/plan-gate.ts +7 -1
- package/src/transport/routing/round-robin.ts +60 -0
- package/src/transport/routing/route-catalog.ts +123 -111
- package/src/transport/routing/route-model.ts +51 -9
- package/src/transport/routing/router.ts +393 -32
- package/src/transport/surface/adapters.ts +11 -2
- package/src/transport/surface/chat/adapter.ts +2 -2
- package/src/transport/surface/chat/encode.ts +47 -4
- package/src/transport/surface/chat/parse.ts +2 -12
- package/src/transport/surface/completion.ts +44 -33
- package/src/transport/surface/content-parts.ts +21 -0
- package/src/transport/surface/messages/adapter.ts +7 -1
- package/src/transport/surface/messages/encode.ts +11 -3
- package/src/transport/surface/messages/parse.ts +2 -1
- package/src/transport/surface/messages/stream.ts +33 -17
- package/src/transport/surface/responses/encode.ts +22 -5
- package/src/transport/surface/responses/parse.ts +3 -11
- package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
- package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
- package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
- package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
- package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
- package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
- package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
- package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
- package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
- package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
- package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
- package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
- package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
- package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
- package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
- package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
- package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
- package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
- package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
- package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
- package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
- package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
- package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/operations/model-discovery-cache.ts +0 -34
- package/src/providers/operations/probe-fetch.ts +0 -15
- package/src/providers/operations/provider-version-cache.ts +0 -30
- package/src/providers/provider-capabilities.ts +0 -33
- package/src/providers/provider-credential-envelope.ts +0 -43
- /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-DIXLjojg.js} +0 -0
- /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
|
@@ -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
|
|
@@ -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.
|
|
@@ -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";
|
|
@@ -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,
|
|
@@ -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;
|
|
@@ -10,6 +10,7 @@ import { Elysia, t, type AnyElysia } from "elysia";
|
|
|
10
10
|
import { errorResponse, requireGlobalAdmin } from "../shared/errors";
|
|
11
11
|
import type { IpAbuseStore } from "../../security/abuse";
|
|
12
12
|
import type { ConsoleAccessResolver } from "../auth/access";
|
|
13
|
+
import { BAN_DURATION_CHOICES } from "../api-keys/ban-constants";
|
|
13
14
|
|
|
14
15
|
/** One active IP ban, as the console returns it. */
|
|
15
16
|
export interface IpBanResponse {
|
|
@@ -26,17 +27,8 @@ export interface IpBanRoutesConfig {
|
|
|
26
27
|
readonly store: IpAbuseStore;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{ minutes: 6 * 60, label: "6 hours" },
|
|
32
|
-
{ minutes: 12 * 60, label: "12 hours" },
|
|
33
|
-
{ minutes: 24 * 60, label: "1 day" },
|
|
34
|
-
{ minutes: 7 * 24 * 60, label: "7 days" },
|
|
35
|
-
{ minutes: null, label: "Permanent" },
|
|
36
|
-
] as const;
|
|
37
|
-
|
|
38
|
-
/** Parses a duration choice into minutes; null = permanent. */
|
|
39
|
-
export function parseBanDuration(value: unknown): number | null | undefined {
|
|
30
|
+
/** Parses a duration choice into minutes; null = permanent; undefined = invalid. */
|
|
31
|
+
function parseBanDuration(value: unknown): number | null | undefined {
|
|
40
32
|
if (value === null || value === undefined) return value === null ? null : undefined;
|
|
41
33
|
const minutes = Number(value);
|
|
42
34
|
if (!Number.isFinite(minutes) || minutes <= 0) return undefined;
|
|
@@ -68,8 +60,10 @@ export function createIpBanRoutes(config: IpBanRoutesConfig): AnyElysia {
|
|
|
68
60
|
const ip = body.ip.trim();
|
|
69
61
|
if (ip.length === 0)
|
|
70
62
|
return errorResponse(new Error("ip is required"), set, "Invalid IP ban request");
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
// Absent is permanent (the default), exactly like an explicit null;
|
|
64
|
+
// parseBanDuration maps those to null and rejects anything else.
|
|
65
|
+
const minutes = body.minutes === undefined ? null : parseBanDuration(body.minutes);
|
|
66
|
+
if (minutes === undefined)
|
|
73
67
|
return errorResponse(new Error("minutes must be a positive integer or null"), set, "Invalid IP ban request");
|
|
74
68
|
await store.ban(ip, minutes === null ? 0 : minutes * 60_000);
|
|
75
69
|
return {
|
|
@@ -196,59 +196,6 @@ const TOP_N = 50;
|
|
|
196
196
|
*/
|
|
197
197
|
const USAGE_READ_THROUGH_TTL_SECONDS = 30;
|
|
198
198
|
|
|
199
|
-
/**
|
|
200
|
-
* Nearest-rank percentile over a sorted array — kept for the JS-side
|
|
201
|
-
* `summarizeUsagePerformance` (still used by callers that already hold a
|
|
202
|
-
* bounded event list). The usage endpoints read percentiles from SQL via
|
|
203
|
-
* `summarizeUsagePerformanceSql`, which is what the dashboard polls.
|
|
204
|
-
*/
|
|
205
|
-
function percentile(sorted: readonly number[], fraction: number): number {
|
|
206
|
-
if (sorted.length === 0) return 0;
|
|
207
|
-
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));
|
|
208
|
-
return Math.round(sorted[index] ?? 0);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
interface UsagePerformanceSummary {
|
|
212
|
-
avgResolveMs: number;
|
|
213
|
-
avgDurationMs: number;
|
|
214
|
-
latencyP50Ms: number;
|
|
215
|
-
latencyP95Ms: number;
|
|
216
|
-
latencyP99Ms: number;
|
|
217
|
-
/** Events each series was computed from; see `UsageSummaryTotals`. */
|
|
218
|
-
latencySampleCount: number;
|
|
219
|
-
resolveSampleCount: number;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Summarizes routing delay and request latency for an already-filtered period. */
|
|
223
|
-
export function summarizeUsagePerformance(
|
|
224
|
-
events: readonly Pick<TelemetryEventRecord, "latencyMs" | "resolveMs">[],
|
|
225
|
-
): UsagePerformanceSummary {
|
|
226
|
-
const latencies: number[] = [];
|
|
227
|
-
let latencySum = 0;
|
|
228
|
-
let resolveSum = 0;
|
|
229
|
-
let resolveCount = 0;
|
|
230
|
-
for (const event of events) {
|
|
231
|
-
if (event.latencyMs !== null) {
|
|
232
|
-
latencies.push(event.latencyMs);
|
|
233
|
-
latencySum += event.latencyMs;
|
|
234
|
-
}
|
|
235
|
-
if (event.resolveMs !== null) {
|
|
236
|
-
resolveSum += event.resolveMs;
|
|
237
|
-
resolveCount += 1;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
latencies.sort((a, b) => a - b);
|
|
241
|
-
return {
|
|
242
|
-
avgResolveMs: resolveCount > 0 ? resolveSum / resolveCount : 0,
|
|
243
|
-
avgDurationMs: latencies.length > 0 ? latencySum / latencies.length : 0,
|
|
244
|
-
latencyP50Ms: percentile(latencies, 0.5),
|
|
245
|
-
latencyP95Ms: percentile(latencies, 0.95),
|
|
246
|
-
latencyP99Ms: percentile(latencies, 0.99),
|
|
247
|
-
latencySampleCount: latencies.length,
|
|
248
|
-
resolveSampleCount: resolveCount,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
199
|
/**
|
|
253
200
|
* The console dimension as the SQL reader's grouping key.
|
|
254
201
|
*
|
|
@@ -83,8 +83,8 @@ export interface ProviderAccountBulkActionResult {
|
|
|
83
83
|
readonly globalSkipped: number;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** Cap on one discovery-commit request (D29). */
|
|
87
|
-
export
|
|
86
|
+
/** Cap on one discovery-commit request (D29). Kept pure (no `node:*`) so the dashboard can import it. */
|
|
87
|
+
export { DISCOVER_MODEL_COMMIT_MAX } from "./discover-constants";
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* One model the provider's own listing advertised, projected for the Add-Model
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure discovery constants, kept free of any `node:*` imports so the
|
|
3
|
+
* dashboard can import them without dragging Node crypto into the browser
|
|
4
|
+
* bundle — the same reason `ban-constants.ts` and `usage-dimensions.ts` exist.
|
|
5
|
+
*/
|
|
6
|
+
export const DISCOVER_MODEL_COMMIT_MAX = 100;
|
|
@@ -44,6 +44,7 @@ const compatibilityProfileSchema = t.Object({
|
|
|
44
44
|
}),
|
|
45
45
|
),
|
|
46
46
|
),
|
|
47
|
+
service_kinds: t.Optional(t.Array(literalUnion(SERVICE_KINDS))),
|
|
47
48
|
streaming_usage_mode: t.Optional(t.Union([t.Literal("include_usage"), t.Literal("none")])),
|
|
48
49
|
structured_output: t.Optional(
|
|
49
50
|
t.Object({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Drizzle-backed provider, account, model, routing, and API-key repositories.
|
|
2
|
-
import { and, asc, eq, gte, inArray, isNull, or, sql, type SQL } from "drizzle-orm";
|
|
3
|
-
import {
|
|
2
|
+
import { and, asc, eq, gte, inArray, isNull, ne, or, sql, type SQL } from "drizzle-orm";
|
|
3
|
+
import { liveModelCooldownExistsSql } from "../../../persistence/dialect";
|
|
4
4
|
import { ConsoleDomainError } from "../../shared/errors";
|
|
5
5
|
import { parseCustomProviderId, isBundledProviderId } from "../../../providers/provider-contracts";
|
|
6
6
|
import { providerRateLimitIsIpScoped } from "../../../providers/provider-metadata";
|
|
@@ -1697,7 +1697,9 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
1697
1697
|
* is evaluated in SQL, so the whole provider is handled in one statement
|
|
1698
1698
|
* without reading rows into the process. `cooldown_until` and
|
|
1699
1699
|
* `model_cooldowns` are left untouched: they are the evidence the operator
|
|
1700
|
-
* sees on a disabled row until an explicit Recover clears them.
|
|
1700
|
+
* sees on a disabled row until an explicit Recover clears them. A row that
|
|
1701
|
+
* is already `disabled` is not a target — those columns surviving the
|
|
1702
|
+
* status change would otherwise keep matching the predicate forever.
|
|
1701
1703
|
*
|
|
1702
1704
|
* Pool-as-account rows live in `provider_pool_accounts`, not this table, so
|
|
1703
1705
|
* they are excluded by construction. Shared (`tenant_id IS NULL`) rows are
|
|
@@ -1790,21 +1792,24 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
1790
1792
|
}
|
|
1791
1793
|
|
|
1792
1794
|
/**
|
|
1793
|
-
* `status='cooldown'
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1795
|
+
* Not already disabled, and either `status='cooldown'`, `cooldown_until` is
|
|
1796
|
+
* still in the future, or `model_cooldowns` carries a live entry. Scoped to
|
|
1797
|
+
* the caller's own rows (the tenant path never touches `tenant_id IS NULL`
|
|
1798
|
+
* rows). The shared JSON subquery lives in the dialect module; only the
|
|
1799
|
+
* surrounding scoping is the store's.
|
|
1800
|
+
*
|
|
1801
|
+
* The disabled exclusion is load-bearing. Disable preserves the deadline
|
|
1802
|
+
* columns as evidence, so without it a row this action just wrote matches
|
|
1803
|
+
* the predicate again and the operator's count never drops to zero.
|
|
1799
1804
|
*/
|
|
1800
1805
|
private liveCooldownPredicate(nowIso: string): SQL {
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1806
|
+
return and(
|
|
1807
|
+
ne(providerAccounts.status, "disabled"),
|
|
1808
|
+
or(
|
|
1809
|
+
eq(providerAccounts.status, "cooldown"),
|
|
1810
|
+
sql`${providerAccounts.cooldownUntil} > ${nowIso}`,
|
|
1811
|
+
liveModelCooldownExistsSql(this.db, nowIso),
|
|
1812
|
+
),
|
|
1808
1813
|
) as SQL;
|
|
1809
1814
|
}
|
|
1810
1815
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t } from "elysia";
|
|
2
2
|
import type { Elysia } from "elysia";
|
|
3
3
|
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
4
|
-
import {
|
|
4
|
+
import { liveModelCooldownExistsSql } from "../../persistence/dialect";
|
|
5
5
|
import { adminAuditLog, providerAccounts } from "../../persistence/schema";
|
|
6
6
|
import { requireGlobalAdmin } from "../shared/errors";
|
|
7
7
|
import {
|
|
@@ -23,9 +23,8 @@ import {
|
|
|
23
23
|
} from "../../providers/quota/refresh";
|
|
24
24
|
import { sanitizeGlobalAccount } from "./quota-view";
|
|
25
25
|
import {
|
|
26
|
-
|
|
26
|
+
resolveNextAccountStatus,
|
|
27
27
|
type AccountQuotaRoutesDeps,
|
|
28
|
-
type AccountStatus,
|
|
29
28
|
} from "./account-quota-shared";
|
|
30
29
|
import type { ProviderAccountBulkActionResult } from "../providers/catalog/contracts";
|
|
31
30
|
|
|
@@ -33,16 +32,18 @@ import type { ProviderAccountBulkActionResult } from "../providers/catalog/contr
|
|
|
33
32
|
* The same live-cooldown predicate as the tenant-scoped store, evaluated in
|
|
34
33
|
* SQL so one statement covers the whole shared-account set. `status='cooldown'`
|
|
35
34
|
* counts even when its deadline elapsed; a live `model_cooldowns` entry counts
|
|
36
|
-
* even while `status` is still `active` (D07).
|
|
35
|
+
* even while `status` is still `active` (D07). A row already `disabled` does
|
|
36
|
+
* not: disable keeps the deadline columns as evidence, and counting them would
|
|
37
|
+
* make a second pass look like it still has work. Scoped strictly to shared
|
|
38
|
+
* rows (`tenant_id IS NULL`); the shared JSON subquery lives in the dialect
|
|
39
|
+
* module.
|
|
37
40
|
*/
|
|
38
41
|
function globalLiveCooldownPredicate(database: unknown): ReturnType<typeof and> {
|
|
39
42
|
const nowIso = new Date().toISOString();
|
|
40
|
-
const liveModelCooldown = isSqliteDatabase(database)
|
|
41
|
-
? sql`EXISTS (SELECT 1 FROM json_each(${providerAccounts.modelCooldowns}) AS entry WHERE json_valid(${providerAccounts.modelCooldowns}) AND json_type(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`
|
|
42
|
-
: sql`EXISTS (SELECT 1 FROM jsonb_each_text(${providerAccounts.modelCooldowns}) AS entry WHERE jsonb_typeof(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`;
|
|
43
43
|
return and(
|
|
44
44
|
isNull(providerAccounts.tenantId),
|
|
45
|
-
sql`${providerAccounts.status}
|
|
45
|
+
sql`${providerAccounts.status} <> 'disabled'`,
|
|
46
|
+
sql`${providerAccounts.status} = 'cooldown' OR ${providerAccounts.cooldownUntil} > ${nowIso} OR ${liveModelCooldownExistsSql(database, nowIso)}`,
|
|
46
47
|
)!;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -117,23 +118,12 @@ export function registerAccountQuotaGlobalRoutes(
|
|
|
117
118
|
},
|
|
118
119
|
async ({ request, params, body, set }) => {
|
|
119
120
|
const access = requireGlobalAdmin(accessResolver(request));
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
code: "invalid_status",
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
const nextStatus: AccountStatus | undefined =
|
|
128
|
-
body.active !== undefined
|
|
129
|
-
? body.active
|
|
130
|
-
? "active"
|
|
131
|
-
: "disabled"
|
|
132
|
-
: (body.status as AccountStatus | undefined);
|
|
133
|
-
if (!nextStatus) {
|
|
134
|
-
set.status = 400;
|
|
135
|
-
return { error: "active or status is required", code: "invalid_request" };
|
|
121
|
+
const resolved = resolveNextAccountStatus(body);
|
|
122
|
+
if (!resolved.ok) {
|
|
123
|
+
set.status = resolved.status;
|
|
124
|
+
return resolved.body;
|
|
136
125
|
}
|
|
126
|
+
const nextStatus = resolved.nextStatus;
|
|
137
127
|
const updated = await db
|
|
138
128
|
.update(providerAccounts)
|
|
139
129
|
.set({ status: nextStatus })
|
|
@@ -20,6 +20,55 @@ export interface AccountQuotaRoutesDeps extends QuotaRefreshDeps {
|
|
|
20
20
|
export const ACCOUNT_STATUSES = ["active", "cooldown", "disabled"] as const;
|
|
21
21
|
export type AccountStatus = (typeof ACCOUNT_STATUSES)[number];
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The merged result of interpreting a PATCH account-status body. On success it
|
|
25
|
+
* carries the resolved `AccountStatus`; on failure it carries the exact 400
|
|
26
|
+
* response the tenant and global routes both return.
|
|
27
|
+
*/
|
|
28
|
+
export type ResolveNextAccountStatusResult =
|
|
29
|
+
| { readonly ok: true; readonly nextStatus: AccountStatus }
|
|
30
|
+
| {
|
|
31
|
+
readonly ok: false;
|
|
32
|
+
readonly status: 400;
|
|
33
|
+
readonly body: { readonly error: string; readonly code: string };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves the next account status from a PATCH body: an explicit `active`
|
|
38
|
+
* boolean wins over `status`, and a missing `status` (or an invalid one) is
|
|
39
|
+
* rejected with the shared 400 shape. Both account PATCH handlers interpret
|
|
40
|
+
* the same input contract, so it lives here once.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveNextAccountStatus(body: {
|
|
43
|
+
readonly active?: boolean;
|
|
44
|
+
readonly status?: string;
|
|
45
|
+
}): ResolveNextAccountStatusResult {
|
|
46
|
+
if (body.status !== undefined && !ACCOUNT_STATUSES.includes(body.status as AccountStatus)) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
status: 400,
|
|
50
|
+
body: {
|
|
51
|
+
error: `status must be one of: ${ACCOUNT_STATUSES.join(", ")}`,
|
|
52
|
+
code: "invalid_status",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const nextStatus: AccountStatus | undefined =
|
|
57
|
+
body.active !== undefined
|
|
58
|
+
? body.active
|
|
59
|
+
? "active"
|
|
60
|
+
: "disabled"
|
|
61
|
+
: (body.status as AccountStatus | undefined);
|
|
62
|
+
if (nextStatus === undefined) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
status: 400,
|
|
66
|
+
body: { error: "active or status is required", code: "invalid_request" },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, nextStatus };
|
|
70
|
+
}
|
|
71
|
+
|
|
23
72
|
/**
|
|
24
73
|
* Wraps a stored api_key credential in the envelope its collector expects.
|
|
25
74
|
*
|
|
@@ -72,9 +72,8 @@ import {
|
|
|
72
72
|
toQuotaView,
|
|
73
73
|
} from "./quota-view";
|
|
74
74
|
import {
|
|
75
|
-
|
|
75
|
+
resolveNextAccountStatus,
|
|
76
76
|
type AccountQuotaRoutesDeps,
|
|
77
|
-
type AccountStatus,
|
|
78
77
|
} from "./account-quota-shared";
|
|
79
78
|
import {
|
|
80
79
|
DAILY_GROWTH_LEG_TIMEOUT_MS,
|
|
@@ -745,23 +744,12 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
745
744
|
},
|
|
746
745
|
async ({ request, params, body, set }) => {
|
|
747
746
|
const access = requireTenantScope(accessResolver(request), "dashboard:write");
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
code: "invalid_status",
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
const nextStatus: AccountStatus | undefined =
|
|
756
|
-
body.active !== undefined
|
|
757
|
-
? body.active
|
|
758
|
-
? "active"
|
|
759
|
-
: "disabled"
|
|
760
|
-
: (body.status as AccountStatus | undefined);
|
|
761
|
-
if (!nextStatus) {
|
|
762
|
-
set.status = 400;
|
|
763
|
-
return { error: "active or status is required", code: "invalid_request" };
|
|
747
|
+
const resolved = resolveNextAccountStatus(body);
|
|
748
|
+
if (!resolved.ok) {
|
|
749
|
+
set.status = resolved.status;
|
|
750
|
+
return resolved.body;
|
|
764
751
|
}
|
|
752
|
+
const nextStatus = resolved.nextStatus;
|
|
765
753
|
const updated = await db
|
|
766
754
|
.update(providerAccounts)
|
|
767
755
|
.set({ status: nextStatus })
|