cartethyia 2.7.2 → 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/.env.example +2 -0
- package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-ChG5svFb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.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-BKtAPYVF.js → HealthEventsModal-DzSWzE_j.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-U6AKaJcF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.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-f0DE1h0-.js +1 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-ClzpwyET.js} +1 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Byj2lAEm.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/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.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-DC2L43Mj.js → badge-D9CS6dZk.js} +1 -1
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DympBNq4.js} +1 -1
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.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-DUzEbynq.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-DFvWhwTa.js → dialog-8fiE6tec.js} +1 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js +1 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.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/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-D-MTJxMr.js} +1 -1
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-CPK47xQx.js} +1 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/{index-DFVFrnWW.js → index-DBNPOfhV.js} +2 -2
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-BT0zntEr.js} +1 -1
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-Da2ZViWg.js} +1 -1
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-BXNeLw6M.js} +1 -1
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.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-sMD965oO.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-BO9dKaFr.js → numbers-CEagj6ZZ.js} +2 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.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-DBNVLdUu.js → select-BIYIBkMU.js} +2 -2
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.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-EMrOMt4I.js → stack-DfgnFDWY.js} +1 -1
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-CkfdYWel.js} +1 -1
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-SYm237HG.js} +1 -1
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-BQqCRNIi.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +118 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- 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/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- 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/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +475 -410
- package/src/console/api-keys/routes.ts +211 -23
- 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 +14 -17
- package/src/console/backup/routes.ts +43 -11
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- 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/dashboard-assets.ts +16 -1
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +4 -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 +34 -2
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/account-test-egress.ts +4 -4
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- 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 +51 -18
- package/src/console/share/share-stats.ts +158 -49
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/shared/read-through-cache.ts +38 -2
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/console/updates/routes.ts +7 -4
- package/src/main.ts +11 -0
- 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 +437 -22
- package/src/observability/telemetry-status.ts +1 -1
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/db-sqlite.ts +7 -2
- package/src/persistence/readiness.ts +18 -6
- package/src/persistence/schema.ts +162 -1
- package/src/persistence/stores/api-key-store.ts +849 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +58 -2
- package/src/persistence/system-prompt-store.ts +104 -0
- 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/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- 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 +33 -47
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- 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 +21 -17
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -2
- 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 +8 -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 +26 -7
- package/src/providers/integrations/grok/grok.ts +2 -6
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- 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/openrouter/provider.ts +4 -0
- 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 +242 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +157 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
- package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +287 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
- package/src/providers/integrations/qoder/qoder.ts +192 -24
- 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/account-error-classification.ts +6 -8
- package/src/providers/operations/account-health-service.ts +14 -16
- package/src/providers/operations/client-versions.ts +37 -22
- 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 +55 -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/access-control.ts +19 -0
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/api-key-auth.ts +18 -5
- 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 +38 -9
- 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/streaming/stream.ts +16 -1
- package/src/transport/dispatch/systemone.ts +8 -8
- package/src/transport/dispatch/token-count.ts +9 -9
- package/src/transport/dispatch/upstream.ts +10 -0
- 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 +37 -21
- package/src/transport/request/state.ts +4 -7
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +104 -2
- package/src/transport/routing/route-model.ts +60 -7
- package/src/transport/routing/router.ts +306 -256
- 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/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js +0 -1
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +0 -56
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js +0 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js +0 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js +0 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-ClCMCWWQ.js +0 -4
- package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js +0 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js +0 -1
- package/dist/dashboard/assets/live-D0mm--ME.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-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.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-BnIpi1IJ.js +0 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js +0 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-BayTTy01.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-BayTTy01.js.gz} +0 -0
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* latency arrays are never returned to JavaScript;
|
|
18
18
|
* - `isGatewayError()` semantics live in one shared SQL predicate.
|
|
19
19
|
*/
|
|
20
|
-
import { and, desc, eq, gte, inArray, isNotNull, sql } from "drizzle-orm";
|
|
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 { 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 {
|
|
@@ -52,6 +52,17 @@ export interface EventListQuery {
|
|
|
52
52
|
readonly until?: Date;
|
|
53
53
|
readonly apiKeyIds?: readonly string[];
|
|
54
54
|
readonly accountIds?: readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* The share-template key whose family (the template itself plus every child
|
|
57
|
+
* key it issued) scopes the read. Rendered as an `EXISTS` subquery against
|
|
58
|
+
* `api_keys` rather than an id list: a family is minted by the tenant and
|
|
59
|
+
* can grow to whatever size the tenant drives it to, so enumerating its ids
|
|
60
|
+
* in JavaScript to feed a bind list would scale the query's wire size with
|
|
61
|
+
* the family instead of with the result. The subquery keeps the same
|
|
62
|
+
* membership semantics — template id or any child of it — for any family
|
|
63
|
+
* size.
|
|
64
|
+
*/
|
|
65
|
+
readonly familyApiKeyId?: string;
|
|
55
66
|
readonly requestId?: string;
|
|
56
67
|
/** Several request ids at once — the backup restore's "already present" check. */
|
|
57
68
|
readonly requestIds?: readonly string[];
|
|
@@ -231,6 +242,94 @@ function scoped(tenantId: string, since?: Date) {
|
|
|
231
242
|
return since === undefined ? tenant : and(tenant, gte(telemetryEvents.createdAt, since));
|
|
232
243
|
}
|
|
233
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Thrown when a membership filter outruns the guard below. This is a handled
|
|
247
|
+
* query error, not a protocol fault: callers surface it as a request failure
|
|
248
|
+
* instead of crashing the process with a PostgreSQL wire error that systemd
|
|
249
|
+
* would only restart into.
|
|
250
|
+
*/
|
|
251
|
+
export class TelemetryQueryInputError extends Error {
|
|
252
|
+
constructor(message: string) {
|
|
253
|
+
super(message);
|
|
254
|
+
this.name = "TelemetryQueryInputError";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The deduplicated id count a list membership filter accepts before refusing.
|
|
260
|
+
*
|
|
261
|
+
* `idMembershipFilter` binds the whole set as one array parameter, so even a
|
|
262
|
+
* six-figure set is wire-safe; this guard is not a protocol necessity but a
|
|
263
|
+
* runaway-collector tripwire — a bug elsewhere (an unbounded accumulator, a
|
|
264
|
+
* vanished scope) must fail as a query error instead of materializing an
|
|
265
|
+
* unbounded parameter. Families larger than this must use the `familyApiKeyId`
|
|
266
|
+
* filter, which never materializes ids.
|
|
267
|
+
*/
|
|
268
|
+
const MAX_MEMBERSHIP_IDS = 200_000;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The chunk size for the SQLite rendering of a list membership filter.
|
|
272
|
+
*
|
|
273
|
+
* SQLite has no array parameters, so the set has to come back as individual
|
|
274
|
+
* placeholders; 500 per chunk sits far below every build's
|
|
275
|
+
* SQLITE_MAX_VARIABLE_NUMBER while keeping the OR of chunks — semantically
|
|
276
|
+
* the identical membership test — to a handful of clauses for realistic sets.
|
|
277
|
+
*/
|
|
278
|
+
const SQLITE_IDS_PER_CHUNK = 500;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Membership of `column` in a caller-supplied id set, safe at any set size.
|
|
282
|
+
*
|
|
283
|
+
* Expanding the members into one bind placeholder each breaks the wire twice
|
|
284
|
+
* over: PostgreSQL caps a Bind message at 65,535 parameters, and node-postgres
|
|
285
|
+
* miscounts long placeholder lists anyway (`08P01 bind message has N parameter
|
|
286
|
+
* formats but 0 parameters` — the shape that crash-looped the dev deployment,
|
|
287
|
+
* whose share family carried ~90k minted child keys). Instead the set is
|
|
288
|
+
* deduplicated and handed to the server as a single array parameter
|
|
289
|
+
* (`= ANY(…::uuid[])`), or, on SQLite, to chunks of individual placeholders;
|
|
290
|
+
* membership semantics are unchanged in both renderings.
|
|
291
|
+
*/
|
|
292
|
+
function idMembershipFilter(
|
|
293
|
+
db: CartethyiaDatabase,
|
|
294
|
+
column: typeof telemetryEvents.apiKeyId | typeof telemetryEvents.accountId | typeof telemetryEvents.requestId,
|
|
295
|
+
values: readonly string[],
|
|
296
|
+
cast: "uuid[]" | "text[]",
|
|
297
|
+
): SQL {
|
|
298
|
+
const unique = [...new Set(values)];
|
|
299
|
+
if (unique.length > MAX_MEMBERSHIP_IDS) {
|
|
300
|
+
throw new TelemetryQueryInputError(
|
|
301
|
+
`telemetry filter exceeds ${MAX_MEMBERSHIP_IDS} distinct ids (got ${unique.length}); scope the read by family or tenant instead`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (unique.length === 0) return sql`false`;
|
|
305
|
+
if (isPostgres(db)) {
|
|
306
|
+
return sql`${column} = ANY(${unique}::${sql.raw(cast)})`;
|
|
307
|
+
}
|
|
308
|
+
if (unique.length <= SQLITE_IDS_PER_CHUNK) return inArray(column, unique);
|
|
309
|
+
const chunks: SQL[] = [];
|
|
310
|
+
for (let start = 0; start < unique.length; start += SQLITE_IDS_PER_CHUNK) {
|
|
311
|
+
chunks.push(inArray(column, unique.slice(start, start + SQLITE_IDS_PER_CHUNK)));
|
|
312
|
+
}
|
|
313
|
+
return or(...chunks) as SQL;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Membership of `api_key_id` in the family of one share template — the
|
|
318
|
+
* template itself plus every key it issued.
|
|
319
|
+
*
|
|
320
|
+
* The primary predicate is the denormalized `family_template_key_id` column:
|
|
321
|
+
* an event is in the family when it was stamped with the template at write
|
|
322
|
+
* time. That makes the read independent of the key row surviving — a revoked
|
|
323
|
+
* or removed key keeps its history. Events written before the column existed
|
|
324
|
+
* (or whose key was hard-deleted before the backfill) fall back to an
|
|
325
|
+
* `EXISTS` against `api_keys`, so the migration window is still counted when
|
|
326
|
+
* the key row is present. The tenant predicate is carried by every read
|
|
327
|
+
* already, so a child in another tenant still cannot match.
|
|
328
|
+
*/
|
|
329
|
+
function familyKeyCondition(templateKeyId: string): SQL {
|
|
330
|
+
return sql`(${telemetryEvents.familyTemplateKeyId} = ${templateKeyId} or (${telemetryEvents.familyTemplateKeyId} is null and exists (select 1 from ${apiKeys} where ${apiKeys.id} = ${telemetryEvents.apiKeyId} and (${apiKeys.id} = ${templateKeyId} or ${apiKeys.parentKeyId} = ${templateKeyId}))))`;
|
|
331
|
+
}
|
|
332
|
+
|
|
234
333
|
/**
|
|
235
334
|
* Bounded detail/list read — the only place rows are materialized.
|
|
236
335
|
*
|
|
@@ -249,16 +348,19 @@ export async function queryTelemetryEvents(
|
|
|
249
348
|
conditions.push(sql`${telemetryEvents.createdAt} < ${query.until}`);
|
|
250
349
|
}
|
|
251
350
|
if (query.apiKeyIds !== undefined) {
|
|
252
|
-
conditions.push(
|
|
351
|
+
conditions.push(idMembershipFilter(db, telemetryEvents.apiKeyId, query.apiKeyIds, "uuid[]"));
|
|
352
|
+
}
|
|
353
|
+
if (query.familyApiKeyId !== undefined) {
|
|
354
|
+
conditions.push(familyKeyCondition(query.familyApiKeyId));
|
|
253
355
|
}
|
|
254
356
|
if (query.accountIds !== undefined) {
|
|
255
|
-
conditions.push(
|
|
357
|
+
conditions.push(idMembershipFilter(db, telemetryEvents.accountId, query.accountIds, "uuid[]"));
|
|
256
358
|
}
|
|
257
359
|
if (query.requestId !== undefined) {
|
|
258
360
|
conditions.push(eq(telemetryEvents.requestId, query.requestId));
|
|
259
361
|
}
|
|
260
362
|
if (query.requestIds !== undefined) {
|
|
261
|
-
conditions.push(
|
|
363
|
+
conditions.push(idMembershipFilter(db, telemetryEvents.requestId, query.requestIds, "text[]"));
|
|
262
364
|
}
|
|
263
365
|
if (query.id !== undefined) {
|
|
264
366
|
conditions.push(eq(telemetryEvents.id, query.id));
|
|
@@ -414,23 +516,39 @@ async function latencyPercentiles(
|
|
|
414
516
|
}
|
|
415
517
|
|
|
416
518
|
// SQLite: nearest-rank percentile = the value at ceil(p * n) rows into the
|
|
417
|
-
// ordered latency column
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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),
|
|
428
546
|
};
|
|
429
|
-
const [p50, p95, p99] = await Promise.all([rankAt(0.5), rankAt(0.95), rankAt(0.99)]);
|
|
430
|
-
return { p50, p95, p99 };
|
|
431
547
|
}
|
|
432
548
|
|
|
433
|
-
/**
|
|
549
|
+
/**
|
|
550
|
+
* The SQL column one breakdown dimension groups by.
|
|
551
|
+
*/
|
|
434
552
|
function dimensionColumn(dimension: string) {
|
|
435
553
|
if (dimension === "model") return telemetryEvents.requestedModel;
|
|
436
554
|
if (dimension === "provider") return telemetryEvents.providerId;
|
|
@@ -638,14 +756,23 @@ export async function healthTelemetry(
|
|
|
638
756
|
export async function sumTelemetryTokens(
|
|
639
757
|
db: CartethyiaDatabase,
|
|
640
758
|
tenantId: string,
|
|
641
|
-
filters: {
|
|
759
|
+
filters: {
|
|
760
|
+
readonly apiKeyIds?: readonly string[];
|
|
761
|
+
/** The share-template family filter; see {@link EventListQuery.familyApiKeyId}. */
|
|
762
|
+
readonly familyKeyId?: string;
|
|
763
|
+
readonly since: Date;
|
|
764
|
+
readonly until?: Date;
|
|
765
|
+
},
|
|
642
766
|
): Promise<number> {
|
|
643
767
|
const conditions = [scoped(tenantId, filters.since)];
|
|
644
768
|
if (filters.until !== undefined) {
|
|
645
769
|
conditions.push(sql`${telemetryEvents.createdAt} < ${filters.until}`);
|
|
646
770
|
}
|
|
647
771
|
if (filters.apiKeyIds !== undefined && filters.apiKeyIds.length > 0) {
|
|
648
|
-
conditions.push(
|
|
772
|
+
conditions.push(idMembershipFilter(db, telemetryEvents.apiKeyId, filters.apiKeyIds, "uuid[]"));
|
|
773
|
+
}
|
|
774
|
+
if (filters.familyKeyId !== undefined) {
|
|
775
|
+
conditions.push(familyKeyCondition(filters.familyKeyId));
|
|
649
776
|
}
|
|
650
777
|
const [row] = await db
|
|
651
778
|
.select({
|
|
@@ -656,6 +783,196 @@ export async function sumTelemetryTokens(
|
|
|
656
783
|
return numeric(row?.total ?? 0);
|
|
657
784
|
}
|
|
658
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
|
+
|
|
659
976
|
/**
|
|
660
977
|
* The health card's latency and throughput figures for one window.
|
|
661
978
|
*
|
|
@@ -750,3 +1067,101 @@ export async function summarizeUsagePerformanceSql(
|
|
|
750
1067
|
avgTokensPerSec: numeric(row?.avgTokensPerSec ?? 0),
|
|
751
1068
|
};
|
|
752
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
|
package/src/persistence/db-pg.ts
CHANGED
|
@@ -108,6 +108,44 @@ export function poolMaxFromEnv(): number {
|
|
|
108
108
|
return value;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Target number of idle connections the pool shrinks to (default 2). */
|
|
112
|
+
export function poolIdleFloorFromEnv(): number {
|
|
113
|
+
const raw = process.env.DATABASE_POOL_IDLE_FLOOR ?? "2";
|
|
114
|
+
const value = Number(raw);
|
|
115
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
116
|
+
throw new Error(`DATABASE_POOL_IDLE_FLOOR must be a non-negative integer (got "${raw}")`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Periodically closes idle clients above the floor so a low-traffic process
|
|
123
|
+
* does not hold `DATABASE_POOL_MAX` backends open for nothing. node-postgres
|
|
124
|
+
* only closes an idle client after `idleTimeoutMillis` (60s here); this gives
|
|
125
|
+
* the pool a faster, explicit shrink path while still keeping the floor warm.
|
|
126
|
+
*/
|
|
127
|
+
export function startPoolShrinker(pool: Pool, floor: number): () => void {
|
|
128
|
+
const interval = setInterval(() => {
|
|
129
|
+
try {
|
|
130
|
+
const idle = pool.idleCount;
|
|
131
|
+
const excess = idle - floor;
|
|
132
|
+
if (excess <= 0) return;
|
|
133
|
+
// Close the excess idle clients in order; `release(true)` destroys the
|
|
134
|
+
// client instead of returning it to the pool.
|
|
135
|
+
for (let index = 0; index < excess; index += 1) {
|
|
136
|
+
pool
|
|
137
|
+
.connect()
|
|
138
|
+
.then((client) => client.release(true))
|
|
139
|
+
.catch(() => undefined);
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// The pool is best-effort; a failed shrink must never crash a request.
|
|
143
|
+
}
|
|
144
|
+
}, 30_000);
|
|
145
|
+
interval.unref();
|
|
146
|
+
return () => clearInterval(interval);
|
|
147
|
+
}
|
|
148
|
+
|
|
111
149
|
declare global {
|
|
112
150
|
// eslint-disable-next-line no-var -- globalThis augmentation requires `var`
|
|
113
151
|
var __cartethyiaPool: Pool | undefined;
|
|
@@ -147,6 +185,10 @@ export function getPool(): Pool {
|
|
|
147
185
|
pool.on("error", (err) => {
|
|
148
186
|
log.error("[postgres] pool error (idle client)", err);
|
|
149
187
|
});
|
|
188
|
+
// Idle-floor shrinker: keeps the pool warm at the floor instead of parking
|
|
189
|
+
// a full `max` worth of backends. Tied to the pool's lifetime, so a hot
|
|
190
|
+
// reload that replaces the pool re-registers it.
|
|
191
|
+
startPoolShrinker(pool, poolIdleFloorFromEnv());
|
|
150
192
|
globalThis.__cartethyiaPool = pool;
|
|
151
193
|
return pool;
|
|
152
194
|
}
|
|
@@ -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
|