cartethyia 2.7.3 → 2.7.4
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/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-ChG5svFb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-BIcWQun7.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-Bf2VelDe.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-JEbYJFxw.js → CombosPage-DCgm5g6g.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-U6AKaJcF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-BMS2t8BA.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-f0DE1h0-.js} +1 -1
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-BAL5npBC.js → SortableList-ClzpwyET.js} +1 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-Cq5tW7hW.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +2 -0
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-D9CS6dZk.js} +1 -1
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-DympBNq4.js} +1 -1
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/{card-Be5Uzamx.js → card-ibRWU_uT.js} +1 -1
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-DJm-9pz7.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-8fiE6tec.js} +1 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-CqGYeOrd.js} +1 -1
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-BSAsYsmV.js} +1 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-D-MTJxMr.js} +1 -1
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-CPK47xQx.js} +1 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/{index-cPrX5vn6.js → index-DBNPOfhV.js} +2 -2
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-BT0zntEr.js} +1 -1
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-Da2ZViWg.js} +1 -1
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-BXNeLw6M.js} +1 -1
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-BycOrrIV.js} +1 -1
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BNDZnsUE.js → numbers-CEagj6ZZ.js} +2 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
- package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/{select-B3iTetHN.js → select-BIYIBkMU.js} +2 -2
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-DfgnFDWY.js} +1 -1
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/{state-DLS7IDMy.js → state-CkfdYWel.js} +1 -1
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-SYm237HG.js} +1 -1
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-BQqCRNIi.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQqCRNIi.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 +55 -54
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
- package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
- package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
- package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
- package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
- package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
- package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
- package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
- package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
- package/package.json +2 -1
- package/src/boot/app.ts +9 -8
- package/src/config/network.ts +7 -3
- package/src/console/api-keys/contracts.ts +52 -0
- package/src/console/api-keys/routes.ts +93 -4
- package/src/console/audit/routes.ts +3 -3
- package/src/console/auth/account-reset.ts +4 -4
- package/src/console/auth/session.ts +3 -3
- package/src/console/backup/contracts.ts +2 -0
- package/src/console/backup/export-endpoint.ts +6 -6
- package/src/console/backup/routes.ts +3 -3
- package/src/console/cli-tools/routes.ts +3 -3
- package/src/console/console-router-guards.ts +2 -2
- package/src/console/console-router.ts +4 -4
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +2 -2
- package/src/console/observability/contracts.ts +4 -0
- package/src/console/observability/live.ts +3 -3
- package/src/console/observability/logs.ts +3 -3
- package/src/console/observability/performance.ts +3 -3
- package/src/console/observability/routes.ts +3 -3
- package/src/console/observability/store.ts +16 -16
- package/src/console/providers/catalog/contracts.ts +43 -22
- package/src/console/providers/catalog/provider-operations.ts +17 -2
- package/src/console/providers/catalog/routes.ts +3 -3
- package/src/console/providers/catalog/store-helpers.ts +349 -326
- package/src/console/providers/catalog/store.ts +1523 -1292
- package/src/console/providers/detail/routes.ts +3 -3
- package/src/console/providers/oauth/routes.ts +3 -3
- package/src/console/quota/account-quota-tenant.ts +26 -1
- package/src/console/quota/account-test-egress.ts +4 -4
- package/src/console/routing/model/routes.ts +3 -3
- package/src/console/routing/pools/contracts.ts +11 -8
- package/src/console/routing/pools/operations.ts +14 -7
- package/src/console/routing/pools/probe-verdict.ts +8 -3
- package/src/console/routing/pools/relay-deploy.ts +9 -9
- package/src/console/routing/pools/routes.ts +3 -3
- package/src/console/routing/pools/store.ts +21 -1
- package/src/console/settings/routes.ts +3 -3
- package/src/console/share/share-router.ts +12 -12
- package/src/console/share/share-stats.ts +105 -35
- package/src/console/shared/read-through-cache.ts +38 -2
- package/src/console/updates/routes.ts +7 -4
- package/src/network/outbound-fetch.ts +75 -4
- package/src/network/pool/resolver.ts +39 -39
- package/src/network/pool-cooldown-store.ts +71 -16
- package/src/network/pool-health-machine.ts +21 -1
- package/src/network/rate-limit-verdict.ts +68 -0
- package/src/network/ssrf.ts +4 -4
- package/src/observability/event-query.ts +320 -16
- package/src/observability/telemetry-status.ts +1 -1
- package/src/persistence/db-sqlite.ts +7 -2
- package/src/persistence/readiness.ts +18 -6
- package/src/persistence/schema.ts +66 -7
- package/src/persistence/stores/api-key-store.ts +99 -3
- package/src/persistence/stores/telemetry-store.ts +1 -1
- package/src/protocol/codecs.ts +6 -6
- package/src/protocol/gateway-error.ts +6 -3
- package/src/protocol/image-generation.ts +2 -2
- package/src/protocol/response/chat.ts +2 -2
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/gemini.ts +1 -1
- package/src/protocol/response/messages.ts +5 -5
- package/src/protocol/response/responses.ts +2 -2
- package/src/protocol/sse-decoder.ts +14 -3
- package/src/protocol/translation/capabilities.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +3 -3
- package/src/providers/adapters/base-adapter.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +2 -2
- package/src/providers/authentication/oauth-refresh-service.ts +2 -2
- package/src/providers/configured-provider.ts +1 -1
- package/src/providers/discovery/probe-phases.ts +2 -2
- package/src/providers/discovery/probing-service.ts +12 -12
- package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
- package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
- package/src/providers/integrations/anthropic.ts +4 -11
- package/src/providers/integrations/antigravity/antigravity.ts +13 -24
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
- package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
- package/src/providers/integrations/buddy/buddy-media.ts +16 -16
- package/src/providers/integrations/buddy/codebuddy/provider.ts +1 -1
- package/src/providers/integrations/buddy/workbuddy/provider.ts +1 -1
- package/src/providers/integrations/claude/claude-betas.ts +1 -1
- package/src/providers/integrations/claude/claude-credentials.ts +1 -1
- package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
- package/src/providers/integrations/claude/claude.ts +3 -10
- package/src/providers/integrations/cline/cline.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +14 -15
- package/src/providers/integrations/commandcode/commandcode.ts +4 -4
- package/src/providers/integrations/commandcode/quota.ts +5 -1
- package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
- package/src/providers/integrations/cursor/cursor.ts +15 -15
- package/src/providers/integrations/factory/factory.ts +4 -4
- package/src/providers/integrations/gemini.ts +11 -11
- package/src/providers/integrations/github/github.ts +6 -6
- package/src/providers/integrations/grok/grok.ts +2 -6
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kimi/kimi.ts +4 -11
- package/src/providers/integrations/kiro/kiro.ts +10 -10
- package/src/providers/integrations/muse/muse.ts +6 -9
- package/src/providers/integrations/perplexity.ts +2 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
- package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
- package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +12 -13
- package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
- package/src/providers/integrations/qoder/qoder.ts +188 -22
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae.ts +6 -9
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/provider-catalog-service.ts +4 -4
- package/src/providers/operations/provider-credential-service.ts +16 -4
- package/src/providers/operations/upstream-deadline.ts +3 -3
- package/src/providers/provider-contracts.ts +47 -3
- package/src/providers/provider-credential-resolver.ts +1 -1
- package/src/providers/provider-registry.ts +4 -4
- package/src/providers/quota/quota-contracts.ts +24 -10
- package/src/providers/quota/refresh.ts +2 -2
- package/src/providers/search/search-provider.ts +5 -5
- package/src/security/abuse.ts +4 -4
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/api-key-auth.ts +4 -4
- package/src/shared/coerce.ts +19 -0
- package/src/transport/dispatch/attempt-finalize.ts +4 -3
- package/src/transport/dispatch/attempt-loop.ts +10 -7
- package/src/transport/dispatch/empty-stream.ts +6 -6
- package/src/transport/dispatch/generation.ts +9 -9
- package/src/transport/dispatch/leases.ts +8 -8
- package/src/transport/dispatch/proxy-request.ts +7 -7
- package/src/transport/dispatch/responses-compact.ts +11 -11
- package/src/transport/dispatch/retry-policy.ts +4 -67
- package/src/transport/dispatch/stream-bridge.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +9 -8
- package/src/transport/dispatch/systemone.ts +8 -8
- package/src/transport/dispatch/token-count.ts +9 -9
- package/src/transport/dispatch/websearch.ts +6 -6
- package/src/transport/drain-endpoint.ts +2 -2
- package/src/transport/failure-policy.ts +13 -6
- package/src/transport/guards/auth-guard.ts +6 -5
- package/src/transport/guards/body-policy.ts +4 -4
- package/src/transport/guards/error-lifecycle.ts +18 -3
- package/src/transport/guards/guard-errors.ts +7 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +6 -6
- package/src/transport/guards/readiness-guard.ts +16 -2
- package/src/transport/guards/request-context.ts +3 -3
- package/src/transport/request/preparer.ts +20 -20
- package/src/transport/request/state.ts +4 -7
- package/src/transport/routing/route-catalog.ts +97 -2
- package/src/transport/routing/route-model.ts +7 -7
- package/src/transport/routing/router.ts +2 -2
- package/src/transport/shutdown-notice.ts +19 -7
- package/src/transport/surface/completion.ts +1 -1
- package/src/transport/surface/messages/encode.ts +1 -1
- package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
- package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
- package/dist/dashboard/assets/assert-C0YYuZJ4.js +0 -1
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-muFRphDu.js +0 -1
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
- package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/routing-BEH32QOC.js +0 -1
- package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
- package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
- /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-BayTTy01.js} +0 -0
- /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-BayTTy01.js.gz} +0 -0
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { and, desc, eq, gte, inArray, isNotNull, or, sql, type SQL } from "drizzle-orm";
|
|
21
21
|
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
22
|
-
import { apiKeys, telemetryEvents } from "../persistence/schema";
|
|
22
|
+
import { apiKeys, telemetryEvents, telemetryUsageTotals } from "../persistence/schema";
|
|
23
23
|
import type { TelemetryEventRecord } from "./event-log";
|
|
24
24
|
import { gatewayErrorSql } from "./telemetry-status";
|
|
25
25
|
|
|
@@ -31,7 +31,7 @@ import { gatewayErrorSql } from "./telemetry-status";
|
|
|
31
31
|
* totals, and the durable usage rollup cannot drift apart — an unknown status
|
|
32
32
|
* must be classified identically in all three.
|
|
33
33
|
*/
|
|
34
|
-
const IS_GATEWAY_ERROR_SQL = gatewayErrorSql(telemetryEvents.status, telemetryEvents.httpStatus);
|
|
34
|
+
export const IS_GATEWAY_ERROR_SQL = gatewayErrorSql(telemetryEvents.status, telemetryEvents.httpStatus);
|
|
35
35
|
|
|
36
36
|
/** One bounded page of events, newest first. */
|
|
37
37
|
export interface EventPage {
|
|
@@ -516,23 +516,39 @@ async function latencyPercentiles(
|
|
|
516
516
|
}
|
|
517
517
|
|
|
518
518
|
// SQLite: nearest-rank percentile = the value at ceil(p * n) rows into the
|
|
519
|
-
// ordered latency column
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
519
|
+
// ordered latency column. One query computes all three ranks via a window
|
|
520
|
+
// function, instead of three separate OFFSET/LIMIT passes over the same
|
|
521
|
+
// ordered column (one per percentile).
|
|
522
|
+
const n = Math.max(1, sampled);
|
|
523
|
+
const ranks = [
|
|
524
|
+
Math.ceil(0.5 * n),
|
|
525
|
+
Math.ceil(0.95 * n),
|
|
526
|
+
Math.ceil(0.99 * n),
|
|
527
|
+
];
|
|
528
|
+
const [row] = await db
|
|
529
|
+
.select({
|
|
530
|
+
p50: sql<number | null>`max(case when rn = ${ranks[0]} then value end)`,
|
|
531
|
+
p95: sql<number | null>`max(case when rn = ${ranks[1]} then value end)`,
|
|
532
|
+
p99: sql<number | null>`max(case when rn = ${ranks[2]} then value end)`,
|
|
533
|
+
})
|
|
534
|
+
.from(
|
|
535
|
+
sql`(
|
|
536
|
+
select ${telemetryEvents.latencyMs} as value,
|
|
537
|
+
row_number() over (order by ${telemetryEvents.latencyMs}) as rn
|
|
538
|
+
from ${telemetryEvents}
|
|
539
|
+
where ${and(...base)}
|
|
540
|
+
)`,
|
|
541
|
+
);
|
|
542
|
+
return {
|
|
543
|
+
p50: numericNullable(row?.p50 ?? null),
|
|
544
|
+
p95: numericNullable(row?.p95 ?? null),
|
|
545
|
+
p99: numericNullable(row?.p99 ?? null),
|
|
530
546
|
};
|
|
531
|
-
const [p50, p95, p99] = await Promise.all([rankAt(0.5), rankAt(0.95), rankAt(0.99)]);
|
|
532
|
-
return { p50, p95, p99 };
|
|
533
547
|
}
|
|
534
548
|
|
|
535
|
-
/**
|
|
549
|
+
/**
|
|
550
|
+
* The SQL column one breakdown dimension groups by.
|
|
551
|
+
*/
|
|
536
552
|
function dimensionColumn(dimension: string) {
|
|
537
553
|
if (dimension === "model") return telemetryEvents.requestedModel;
|
|
538
554
|
if (dimension === "provider") return telemetryEvents.providerId;
|
|
@@ -767,6 +783,196 @@ export async function sumTelemetryTokens(
|
|
|
767
783
|
return numeric(row?.total ?? 0);
|
|
768
784
|
}
|
|
769
785
|
|
|
786
|
+
/** One hour, in milliseconds — the share page's hourly bucket width. */
|
|
787
|
+
const HOUR_MS = 3_600_000;
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* All-time family counts from the durable rollup, not from a scan of the
|
|
791
|
+
* retained telemetry window.
|
|
792
|
+
*
|
|
793
|
+
* The share KPI cards need lifetime numbers. `telemetry_usage_totals` is
|
|
794
|
+
* charged at write time and accumulates per identity, so a family's lifetime
|
|
795
|
+
* requests/errors/tokens are a SUM over its rows (keyed by
|
|
796
|
+
* `family_template_key_id`) — sub-millisecond on a small table, versus a
|
|
797
|
+
* full-window aggregate over `telemetry_events` that reads every retained
|
|
798
|
+
* event on every tick. The rollup is the same definition as the read-side
|
|
799
|
+
* counts, so the two cannot drift.
|
|
800
|
+
*/
|
|
801
|
+
export interface FamilyKpiTotals {
|
|
802
|
+
readonly requests: number;
|
|
803
|
+
readonly errors: number;
|
|
804
|
+
readonly tokens: number;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Sums one share family's durable lifetime totals. */
|
|
808
|
+
export async function familyKpiTotals(
|
|
809
|
+
db: CartethyiaDatabase,
|
|
810
|
+
tenantId: string,
|
|
811
|
+
familyKeyId: string,
|
|
812
|
+
): Promise<FamilyKpiTotals> {
|
|
813
|
+
const [row] = await db
|
|
814
|
+
.select({
|
|
815
|
+
requests: sql<number>`coalesce(sum(${telemetryUsageTotals.requests}), 0)`,
|
|
816
|
+
errors: sql<number>`coalesce(sum(${telemetryUsageTotals.errors}), 0)`,
|
|
817
|
+
tokens: sql<number>`coalesce(sum(coalesce(${telemetryUsageTotals.inputTokens}, 0) + coalesce(${telemetryUsageTotals.outputTokens}, 0)), 0)`,
|
|
818
|
+
})
|
|
819
|
+
.from(telemetryUsageTotals)
|
|
820
|
+
.where(
|
|
821
|
+
and(
|
|
822
|
+
eq(telemetryUsageTotals.tenantId, tenantId),
|
|
823
|
+
eq(telemetryUsageTotals.familyTemplateKeyId, familyKeyId),
|
|
824
|
+
),
|
|
825
|
+
);
|
|
826
|
+
return {
|
|
827
|
+
requests: numeric(row?.requests ?? 0),
|
|
828
|
+
errors: numeric(row?.errors ?? 0),
|
|
829
|
+
tokens: numeric(row?.tokens ?? 0),
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* All-time family counts: the numbers a share page's KPI card shows.
|
|
835
|
+
*
|
|
836
|
+
* These must be computed in SQL, not derived from the bounded page read — the
|
|
837
|
+
* page caps at {@link MAX_LIMIT}, so a busy family would freeze at 500
|
|
838
|
+
* requests/errors/last-hour regardless of real traffic.
|
|
839
|
+
*/
|
|
840
|
+
export interface FamilyUsageTotals {
|
|
841
|
+
readonly requests: number;
|
|
842
|
+
readonly errors: number;
|
|
843
|
+
readonly lastHourRequests: number;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** Counts one share family's requests, errors, and last-hour traffic in SQL. */
|
|
847
|
+
export async function familyUsageTotals(
|
|
848
|
+
db: CartethyiaDatabase,
|
|
849
|
+
tenantId: string,
|
|
850
|
+
familyKeyId: string,
|
|
851
|
+
now: Date,
|
|
852
|
+
): Promise<FamilyUsageTotals> {
|
|
853
|
+
const lastHourStart = new Date(now.getTime() - HOUR_MS);
|
|
854
|
+
const conditions = [eq(telemetryEvents.tenantId, tenantId), familyKeyCondition(familyKeyId)];
|
|
855
|
+
const [row] = await db
|
|
856
|
+
.select({
|
|
857
|
+
requests: sql<number>`count(*)`,
|
|
858
|
+
errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
|
|
859
|
+
lastHourRequests: sql<number>`sum(case when ${telemetryEvents.createdAt} >= ${lastHourStart} then 1 else 0 end)`,
|
|
860
|
+
})
|
|
861
|
+
.from(telemetryEvents)
|
|
862
|
+
.where(and(...conditions));
|
|
863
|
+
return {
|
|
864
|
+
requests: numeric(row?.requests ?? 0),
|
|
865
|
+
errors: numeric(row?.errors ?? 0),
|
|
866
|
+
lastHourRequests: numeric(row?.lastHourRequests ?? 0),
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Counts one share family's requests in a bounded window — a range scan, not a full-window aggregate. */
|
|
871
|
+
export async function familyWindowRequests(
|
|
872
|
+
db: CartethyiaDatabase,
|
|
873
|
+
tenantId: string,
|
|
874
|
+
familyKeyId: string,
|
|
875
|
+
since: Date,
|
|
876
|
+
): Promise<number> {
|
|
877
|
+
const [row] = await db
|
|
878
|
+
.select({
|
|
879
|
+
requests: sql<number>`count(*)`,
|
|
880
|
+
})
|
|
881
|
+
.from(telemetryEvents)
|
|
882
|
+
.where(
|
|
883
|
+
and(
|
|
884
|
+
eq(telemetryEvents.tenantId, tenantId),
|
|
885
|
+
familyKeyCondition(familyKeyId),
|
|
886
|
+
gte(telemetryEvents.createdAt, since),
|
|
887
|
+
),
|
|
888
|
+
);
|
|
889
|
+
return numeric(row?.requests ?? 0);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* One share family's client-IP breakdown, computed in SQL.
|
|
894
|
+
*
|
|
895
|
+
* Unlike the 24-hour page read, this covers a bounded history so an address
|
|
896
|
+
* that stopped before the last day still appears — marked inactive. The
|
|
897
|
+
* `active` flag is decided in the caller from `lastSeenAt` vs a cutoff; here
|
|
898
|
+
* we only aggregate. The window is bounded (`since`, default the whole
|
|
899
|
+
* retained window) so an unbounded scan is never the default for callers
|
|
900
|
+
* that only need recent addresses.
|
|
901
|
+
*/
|
|
902
|
+
export async function familyIpBreakdown(
|
|
903
|
+
db: CartethyiaDatabase,
|
|
904
|
+
tenantId: string,
|
|
905
|
+
familyKeyId: string,
|
|
906
|
+
limit = 50,
|
|
907
|
+
since?: Date,
|
|
908
|
+
): Promise<{ ip: string; requests: number; tokens: number; lastSeenAt: Date | null }[]> {
|
|
909
|
+
const conditions = [
|
|
910
|
+
eq(telemetryEvents.tenantId, tenantId),
|
|
911
|
+
familyKeyCondition(familyKeyId),
|
|
912
|
+
...(since === undefined ? [] : [gte(telemetryEvents.createdAt, since)]),
|
|
913
|
+
];
|
|
914
|
+
const rows = await db
|
|
915
|
+
.select({
|
|
916
|
+
ip: telemetryEvents.clientIp,
|
|
917
|
+
requests: sql<number>`count(*)`,
|
|
918
|
+
tokens: sql<number>`coalesce(sum(coalesce(${telemetryEvents.inputTokens}, 0) + coalesce(${telemetryEvents.outputTokens}, 0)), 0)`,
|
|
919
|
+
lastSeenAt: sql<Date>`max(${telemetryEvents.createdAt})`,
|
|
920
|
+
})
|
|
921
|
+
.from(telemetryEvents)
|
|
922
|
+
.where(and(...conditions))
|
|
923
|
+
.groupBy(telemetryEvents.clientIp)
|
|
924
|
+
// Highest volume first: the table answers "who has used the link most",
|
|
925
|
+
// not just who is active right now.
|
|
926
|
+
.orderBy(desc(sql`count(*)`))
|
|
927
|
+
.limit(limit);
|
|
928
|
+
return rows.flatMap((row) =>
|
|
929
|
+
row.ip === null ? [] : [{ ip: row.ip, requests: numeric(row.requests), tokens: numeric(row.tokens), lastSeenAt: row.lastSeenAt }],
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* One share family's time-series buckets, computed in SQL with the family
|
|
935
|
+
* membership filter.
|
|
936
|
+
*
|
|
937
|
+
* Mirrors {@link chartTelemetry} but scoped to a share family. The share page's
|
|
938
|
+
* 24-hour strip must not be truncated to the bounded page (500 events), or a
|
|
939
|
+
* busy family's chart undercounts every hour bucket.
|
|
940
|
+
*/
|
|
941
|
+
export async function familyHourlyTelemetry(
|
|
942
|
+
db: CartethyiaDatabase,
|
|
943
|
+
tenantId: string,
|
|
944
|
+
familyKeyId: string,
|
|
945
|
+
since: Date,
|
|
946
|
+
bucketSeconds: number,
|
|
947
|
+
): Promise<ChartBucket[]> {
|
|
948
|
+
const width = Math.max(1, Math.trunc(bucketSeconds));
|
|
949
|
+
// Inlined (not parameterised) so Postgres sees the SELECT and GROUP BY
|
|
950
|
+
// expressions as identical — see chartTelemetry for the failure this avoids.
|
|
951
|
+
const w = sql.raw(String(width));
|
|
952
|
+
const bucketExpression = isPostgres(db)
|
|
953
|
+
? sql<number>`(cast(extract(epoch from ${telemetryEvents.createdAt}) as bigint) / ${w}) * ${w}`
|
|
954
|
+
: sql<number>`(cast(strftime('%s', ${telemetryEvents.createdAt}) as integer) / ${w}) * ${w}`;
|
|
955
|
+
const conditions = [scoped(tenantId, since), familyKeyCondition(familyKeyId)];
|
|
956
|
+
const rows = await db
|
|
957
|
+
.select({
|
|
958
|
+
at: bucketExpression,
|
|
959
|
+
requests: sql<number>`count(*)`,
|
|
960
|
+
errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
|
|
961
|
+
inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
|
|
962
|
+
outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
|
|
963
|
+
})
|
|
964
|
+
.from(telemetryEvents)
|
|
965
|
+
.where(and(...conditions))
|
|
966
|
+
.groupBy(bucketExpression);
|
|
967
|
+
return rows.map((row) => ({
|
|
968
|
+
at: numeric(row.at) * 1000,
|
|
969
|
+
requests: numeric(row.requests),
|
|
970
|
+
errors: numeric(row.errors),
|
|
971
|
+
inputTokens: numeric(row.inputTokens),
|
|
972
|
+
outputTokens: numeric(row.outputTokens),
|
|
973
|
+
}));
|
|
974
|
+
}
|
|
975
|
+
|
|
770
976
|
/**
|
|
771
977
|
* The health card's latency and throughput figures for one window.
|
|
772
978
|
*
|
|
@@ -861,3 +1067,101 @@ export async function summarizeUsagePerformanceSql(
|
|
|
861
1067
|
avgTokensPerSec: numeric(row?.avgTokensPerSec ?? 0),
|
|
862
1068
|
};
|
|
863
1069
|
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Combined usage totals + performance in ONE window scan.
|
|
1073
|
+
*
|
|
1074
|
+
* `summarizeTelemetry` and `summarizeUsagePerformanceSql` both scan the same
|
|
1075
|
+
* tenant window and each additionally run the percentile path, so the Usage
|
|
1076
|
+
* summary endpoint paid two scans plus two percentile passes for one response.
|
|
1077
|
+
* This runs the combined SELECT once and one percentile pass, returning both
|
|
1078
|
+
* shapes so the caller needs no second query.
|
|
1079
|
+
*/
|
|
1080
|
+
export async function summarizeUsageWithPerformance(
|
|
1081
|
+
db: CartethyiaDatabase,
|
|
1082
|
+
tenantId: string,
|
|
1083
|
+
since: Date,
|
|
1084
|
+
): Promise<{ summary: UsageSummary; performance: UsagePerformanceTotals }> {
|
|
1085
|
+
const condition = scoped(tenantId, since);
|
|
1086
|
+
const [row] = await db
|
|
1087
|
+
.select({
|
|
1088
|
+
requests: sql<number>`count(*)`,
|
|
1089
|
+
errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
|
|
1090
|
+
cancelled: sql<number>`sum(case when ${telemetryEvents.status} = 'cancelled' then 1 else 0 end)`,
|
|
1091
|
+
truncated: sql<number>`sum(case when ${telemetryEvents.status} = 'truncated' then 1 else 0 end)`,
|
|
1092
|
+
inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
|
|
1093
|
+
outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
|
|
1094
|
+
cachedInputTokens: sql<number>`coalesce(sum(${telemetryEvents.cachedInputTokens}), 0)`,
|
|
1095
|
+
reasoningTokens: sql<number>`coalesce(sum(${telemetryEvents.reasoningTokens}), 0)`,
|
|
1096
|
+
estimatedCostUsd: sql<number>`coalesce(sum(${telemetryEvents.estimatedCostUsd}), 0)`,
|
|
1097
|
+
sampled: sql<number>`count(${telemetryEvents.latencyMs})`,
|
|
1098
|
+
latencyAvg: sql<number>`avg(${telemetryEvents.latencyMs})`,
|
|
1099
|
+
unpriced: sql<number>`sum(case when ${telemetryEvents.estimatedCostUsd} is null then 1 else 0 end)`,
|
|
1100
|
+
resolveSum: sql<number>`coalesce(sum(${telemetryEvents.resolveMs}), 0)`,
|
|
1101
|
+
resolveCount: sql<number>`count(${telemetryEvents.resolveMs})`,
|
|
1102
|
+
avgTokensPerSec: sql<number>`coalesce(avg(${telemetryEvents.tokensPerSec}), 0)`,
|
|
1103
|
+
})
|
|
1104
|
+
.from(telemetryEvents)
|
|
1105
|
+
.where(condition);
|
|
1106
|
+
|
|
1107
|
+
if (!row) {
|
|
1108
|
+
const empty: UsageSummary = {
|
|
1109
|
+
requests: 0,
|
|
1110
|
+
errors: 0,
|
|
1111
|
+
cancelled: 0,
|
|
1112
|
+
truncated: 0,
|
|
1113
|
+
inputTokens: 0,
|
|
1114
|
+
outputTokens: 0,
|
|
1115
|
+
cachedInputTokens: 0,
|
|
1116
|
+
reasoningTokens: 0,
|
|
1117
|
+
estimatedCostUsd: 0,
|
|
1118
|
+
sampled: 0,
|
|
1119
|
+
latencyMs: { avg: null, p50: null, p95: null, p99: null },
|
|
1120
|
+
unpriced: 0,
|
|
1121
|
+
};
|
|
1122
|
+
const emptyPerformance: UsagePerformanceTotals = {
|
|
1123
|
+
avgResolveMs: 0,
|
|
1124
|
+
avgDurationMs: 0,
|
|
1125
|
+
latencyP50Ms: 0,
|
|
1126
|
+
latencyP95Ms: 0,
|
|
1127
|
+
latencyP99Ms: 0,
|
|
1128
|
+
latencySampleCount: 0,
|
|
1129
|
+
resolveSampleCount: 0,
|
|
1130
|
+
avgTokensPerSec: 0,
|
|
1131
|
+
};
|
|
1132
|
+
return { summary: empty, performance: emptyPerformance };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const sampled = numeric(row.sampled);
|
|
1136
|
+
const percentiles = await latencyPercentiles(db, tenantId, since, sampled);
|
|
1137
|
+
const resolveCount = numeric(row.resolveCount);
|
|
1138
|
+
return {
|
|
1139
|
+
summary: {
|
|
1140
|
+
requests: numeric(row.requests),
|
|
1141
|
+
errors: numeric(row.errors),
|
|
1142
|
+
cancelled: numeric(row.cancelled),
|
|
1143
|
+
truncated: numeric(row.truncated),
|
|
1144
|
+
inputTokens: numeric(row.inputTokens),
|
|
1145
|
+
outputTokens: numeric(row.outputTokens),
|
|
1146
|
+
cachedInputTokens: numeric(row.cachedInputTokens),
|
|
1147
|
+
reasoningTokens: numeric(row.reasoningTokens),
|
|
1148
|
+
estimatedCostUsd: numeric(row.estimatedCostUsd),
|
|
1149
|
+
sampled,
|
|
1150
|
+
latencyMs: {
|
|
1151
|
+
avg: numericNullable(row.latencyAvg),
|
|
1152
|
+
...percentiles,
|
|
1153
|
+
},
|
|
1154
|
+
unpriced: numeric(row.unpriced),
|
|
1155
|
+
},
|
|
1156
|
+
performance: {
|
|
1157
|
+
avgResolveMs: resolveCount > 0 ? numeric(row.resolveSum) / resolveCount : 0,
|
|
1158
|
+
avgDurationMs: numericNullable(row.latencyAvg) ?? 0,
|
|
1159
|
+
latencyP50Ms: percentiles.p50 ?? 0,
|
|
1160
|
+
latencyP95Ms: percentiles.p95 ?? 0,
|
|
1161
|
+
latencyP99Ms: percentiles.p99 ?? 0,
|
|
1162
|
+
latencySampleCount: sampled,
|
|
1163
|
+
resolveSampleCount: resolveCount,
|
|
1164
|
+
avgTokensPerSec: numeric(row.avgTokensPerSec),
|
|
1165
|
+
},
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* rule as well, so it needs no special case.
|
|
31
31
|
* - **`503`** is capacity or availability: the gateway *correctly* refusing work
|
|
32
32
|
* it cannot do right now (`admission_unavailable`, `accounts_unavailable`,
|
|
33
|
-
* `shutting_down
|
|
33
|
+
* `shutting_down` (incl. update restart), `platform_unavailable`). Retryable,
|
|
34
34
|
* self-inflicted, and an operator-capacity signal rather than a defect.
|
|
35
35
|
* - **`504`** is a timeout, not a malfunction. It represents a deadline that
|
|
36
36
|
* expired: the request deadline, the stream-stall
|
|
@@ -327,7 +327,8 @@ const AUTH_KEY_COLUMNS = [
|
|
|
327
327
|
"issued_client_ip_key", "label", "scopes", "requests_per_minute", "daily_token_limit",
|
|
328
328
|
"monthly_token_limit", "lifetime_token_budget", "lifetime_tokens_consumed",
|
|
329
329
|
"max_concurrent_requests", "model_access_mode", "model_list", "client_router_denylist",
|
|
330
|
-
"model_prefix", "created_at", "enabled", "revoked_at", "
|
|
330
|
+
"model_prefix", "created_at", "enabled", "revoked_at", "banned_at", "ban_reason", "banned_until",
|
|
331
|
+
"key_prefix", "key_encrypted",
|
|
331
332
|
"notes_title", "notes_subtitle", "notes_body", "is_default", "sort_index",
|
|
332
333
|
].join(", ");
|
|
333
334
|
|
|
@@ -341,7 +342,11 @@ export interface SqliteAuthStatements {
|
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
export function prepareAuthStatements(sqlite: Database): SqliteAuthStatements {
|
|
344
|
-
|
|
345
|
+
// A key is active while not revoked and not under an active ban: a ban is
|
|
346
|
+
// active when `banned_at` is set and `banned_until` is null (permanent) or
|
|
347
|
+
// still in the future. SQLite stores timestamps as ISO text, so the
|
|
348
|
+
// comparison is lexical (ISO sorts chronologically).
|
|
349
|
+
const active = `enabled = 1 AND revoked_at IS NULL AND (banned_at IS NULL OR (banned_until IS NOT NULL AND banned_until <= datetime('now')))`;
|
|
345
350
|
// `query`, not `prepare`: bun's `prepare` compiles a statement per call and
|
|
346
351
|
// never finalizes one, while `query` caches the compiled statement behind an
|
|
347
352
|
// LRU of its own. All three are constant text run once per authenticated
|
|
@@ -89,12 +89,15 @@ async function runReadinessChecks(
|
|
|
89
89
|
|
|
90
90
|
// 1. Check database connectivity via simple query
|
|
91
91
|
let dbOk = false;
|
|
92
|
+
let dbTimedOut = false;
|
|
92
93
|
try {
|
|
93
94
|
const result = await withTimeoutCheck(
|
|
94
95
|
() => db.execute(sql`SELECT 1`),
|
|
95
96
|
"Database connectivity check",
|
|
96
97
|
);
|
|
97
|
-
if (result
|
|
98
|
+
if (result === "timeout") {
|
|
99
|
+
dbTimedOut = true;
|
|
100
|
+
} else {
|
|
98
101
|
dbOk = true;
|
|
99
102
|
}
|
|
100
103
|
} catch {
|
|
@@ -107,7 +110,10 @@ async function runReadinessChecks(
|
|
|
107
110
|
db: "disconnected",
|
|
108
111
|
migrations: "pending",
|
|
109
112
|
redis: "disconnected",
|
|
110
|
-
|
|
113
|
+
// A timed-out probe under load is not evidence the database is down —
|
|
114
|
+
// it is the probe itself failing to get a backend in time. The guard
|
|
115
|
+
// reads this to call it overload rather than a dependency failure.
|
|
116
|
+
reason: dbTimedOut ? "database probe timed out" : "database not connected",
|
|
111
117
|
};
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -115,13 +121,16 @@ async function runReadinessChecks(
|
|
|
115
121
|
// Readiness requires every discovered migration to have a ledger row; a
|
|
116
122
|
// reachable ledger alone is not proof the upgrade finished.
|
|
117
123
|
let migrationsApplied = false;
|
|
124
|
+
let migrationsTimedOut = false;
|
|
118
125
|
try {
|
|
119
126
|
const result = await withTimeoutCheck(
|
|
120
127
|
() => readMigrationLedgerStatus(db),
|
|
121
128
|
"Migrations check",
|
|
122
129
|
);
|
|
123
130
|
|
|
124
|
-
if (result
|
|
131
|
+
if (result === "timeout") {
|
|
132
|
+
migrationsTimedOut = true;
|
|
133
|
+
} else {
|
|
125
134
|
migrationsApplied = result.applied;
|
|
126
135
|
}
|
|
127
136
|
} catch {
|
|
@@ -134,18 +143,21 @@ async function runReadinessChecks(
|
|
|
134
143
|
db: "connected",
|
|
135
144
|
migrations: "pending",
|
|
136
145
|
redis: "disconnected",
|
|
137
|
-
reason: "migrations not yet applied",
|
|
146
|
+
reason: migrationsTimedOut ? "migrations probe timed out" : "migrations not yet applied",
|
|
138
147
|
};
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
// 3. Check Redis. The memory backend is healthy by construction — there is
|
|
142
151
|
// no connection to probe — so it passes without a round trip.
|
|
143
152
|
let redisOk = redisBackend === "memory";
|
|
153
|
+
let redisTimedOut = false;
|
|
144
154
|
|
|
145
155
|
if (redisBackend === "redis" && redis) {
|
|
146
156
|
try {
|
|
147
157
|
const result = await withTimeoutCheck(() => redis.ping(), "Redis connectivity check");
|
|
148
|
-
if (result
|
|
158
|
+
if (result === "timeout") {
|
|
159
|
+
redisTimedOut = true;
|
|
160
|
+
} else if (result === "PONG") {
|
|
149
161
|
redisOk = true;
|
|
150
162
|
}
|
|
151
163
|
} catch {
|
|
@@ -159,7 +171,7 @@ async function runReadinessChecks(
|
|
|
159
171
|
db: "connected",
|
|
160
172
|
migrations: "applied",
|
|
161
173
|
redis: "disconnected",
|
|
162
|
-
reason: "redis not connected",
|
|
174
|
+
reason: redisTimedOut ? "redis probe timed out" : "redis not connected",
|
|
163
175
|
};
|
|
164
176
|
}
|
|
165
177
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { desc, sql } from "drizzle-orm";
|
|
3
3
|
import {
|
|
4
4
|
bigint, boolean, check, customType, index, integer, jsonb, numeric, pgEnum, pgTable,
|
|
5
|
-
primaryKey, text, timestamp, uniqueIndex, uuid, type AnyPgColumn,
|
|
5
|
+
primaryKey, text, timestamp, unique, uniqueIndex, uuid, type AnyPgColumn,
|
|
6
6
|
} from "drizzle-orm/pg-core";
|
|
7
7
|
|
|
8
8
|
export const TENANT_STATUSES = ["active", "suspended", "deleted"] as const;
|
|
@@ -188,15 +188,16 @@ export const providers = pgTable(
|
|
|
188
188
|
*
|
|
189
189
|
* The device identity is a deterministic function of a seed file
|
|
190
190
|
* (`~/.config/.locale_cfg`): the `runtime-info` bridge reads that seed and
|
|
191
|
-
* derives `machineToken`/`machineType`/`machineCode`. The
|
|
192
|
-
*
|
|
193
|
-
*
|
|
191
|
+
* derives `machineToken`/`machineType`/`machineCode`. The daily 100-credit
|
|
192
|
+
* campaign does not gate grants on this identity. A persisted seed allows the
|
|
193
|
+
* vendor bridge to replay the same native identity where another Qoder surface
|
|
194
|
+
* requires it.
|
|
194
195
|
*
|
|
195
196
|
* `seed` is the exact seed-file content the identity was minted from, stored
|
|
196
197
|
* so the same device identity can be replayed (restore seed → bridge → same
|
|
197
|
-
* token). It is not a credential, but it is
|
|
198
|
-
*
|
|
199
|
-
*
|
|
198
|
+
* token). It is not a credential, but it is per-account device material and
|
|
199
|
+
* should not be shared between accounts. Campaign grant eligibility is
|
|
200
|
+
* independent of `machineToken`.
|
|
200
201
|
*/
|
|
201
202
|
export interface NativeIdentityRecord {
|
|
202
203
|
readonly machineToken: string;
|
|
@@ -569,6 +570,49 @@ export const networkPools = pgTable("network_pools", {
|
|
|
569
570
|
],
|
|
570
571
|
);
|
|
571
572
|
|
|
573
|
+
/**
|
|
574
|
+
* A pool's account-equivalent routing entity for ip-scoped, credential-less
|
|
575
|
+
* providers (OpenCode Free today).
|
|
576
|
+
*
|
|
577
|
+
* These providers own no `provider_accounts` rows — the rate limit belongs to
|
|
578
|
+
* the egress address, not to a credential. Materializing one row per (pool,
|
|
579
|
+
* ip-scoped provider) lets the account health machine, routing eligibility, and
|
|
580
|
+
* the provider-detail accounts UI treat each egress like an account: the row
|
|
581
|
+
* carries the same health state machine and cooldown columns, so a spent egress
|
|
582
|
+
* is excluded from the plan the same way a cooling account is, and the operator
|
|
583
|
+
* sees it in the provider's Accounts card with a "Pool" badge.
|
|
584
|
+
*
|
|
585
|
+
* The row is an *account-equivalent*, not an account: it resolves no credential
|
|
586
|
+
* (`credential_kind` is fixed to `none`-style egress), is skipped by the quota
|
|
587
|
+
* refresh and identity sweeps (they filter on real credential kinds), and is
|
|
588
|
+
* owned by its pool — deleting the pool cascades the row.
|
|
589
|
+
*/
|
|
590
|
+
export const providerPoolAccounts = pgTable(
|
|
591
|
+
"provider_pool_accounts",
|
|
592
|
+
{
|
|
593
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
594
|
+
tenantId: tenantRefRequired(),
|
|
595
|
+
networkPoolId: uuid("network_pool_id")
|
|
596
|
+
.notNull()
|
|
597
|
+
.references(() => networkPools.id, { onDelete: "cascade" }),
|
|
598
|
+
providerId: text("provider_id").notNull(),
|
|
599
|
+
label: text("label").notNull(),
|
|
600
|
+
...healthColumns(),
|
|
601
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
602
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
603
|
+
},
|
|
604
|
+
(table) => [
|
|
605
|
+
unique("provider_pool_accounts_pool_provider_uidx").on(
|
|
606
|
+
table.networkPoolId,
|
|
607
|
+
table.providerId,
|
|
608
|
+
table.tenantId,
|
|
609
|
+
),
|
|
610
|
+
index("provider_pool_accounts_tenant_status_idx").on(table.tenantId, table.status),
|
|
611
|
+
index("provider_pool_accounts_provider_status_idx").on(table.providerId, table.status),
|
|
612
|
+
],
|
|
613
|
+
);
|
|
614
|
+
export type ProviderPoolAccount = typeof providerPoolAccounts.$inferSelect;
|
|
615
|
+
|
|
572
616
|
|
|
573
617
|
// Account and network-pool health events share one table. The entity kind
|
|
574
618
|
// selects exactly one owner FK, and the CHECK constraint below preserves
|
|
@@ -845,6 +889,21 @@ export const apiKeys = pgTable(
|
|
|
845
889
|
createdAt: createdAtColumn(),
|
|
846
890
|
enabled: boolean("enabled").notNull().default(true),
|
|
847
891
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
892
|
+
/**
|
|
893
|
+
* Ban state. A ban is distinct from a revoke: it records the operator's
|
|
894
|
+
* reason (`banReason`) so the console can show it, and it is fully
|
|
895
|
+
* reversible — unban clears both columns, leaving no metadata behind.
|
|
896
|
+
* Banned keys are rejected by the auth path exactly like revoked keys.
|
|
897
|
+
*/
|
|
898
|
+
bannedAt: timestamp("banned_at", { withTimezone: true }),
|
|
899
|
+
banReason: text("ban_reason"),
|
|
900
|
+
/**
|
|
901
|
+
* When the ban lifts on its own, if it is not permanent. `null` means the
|
|
902
|
+
* ban does not lapse (until an operator unbans it). The auth path treats a
|
|
903
|
+
* key as banned only while `banned_at` is set and `banned_until` is either
|
|
904
|
+
* null or in the future — so a timed ban expires without SQL.
|
|
905
|
+
*/
|
|
906
|
+
bannedUntil: timestamp("banned_until", { withTimezone: true }),
|
|
848
907
|
/**
|
|
849
908
|
* The tenant's mandatory gateway key. Seeded at first boot whether or not
|
|
850
909
|
* `CARTETHYIA_API_KEY` was configured, and the one key the console refuses
|