cartethyia 2.5.9 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -42
- package/README.md +11 -10
- package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
- package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
- package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
- package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
- package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -51
- package/migrations/README.md +52 -52
- package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
- package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
- package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/package.json +96 -97
- package/scripts/cli/admin.ts +126 -125
- package/scripts/cli/autostart.ts +237 -237
- package/scripts/cli/background.ts +120 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +167 -167
- package/scripts/cli/console-menu.ts +308 -275
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/doctor.ts +1 -1
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -261
- package/scripts/{health.ts → cli/health.ts} +4 -4
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -164
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +275 -107
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -82
- package/scripts/cli/startup-signal.ts +23 -23
- package/scripts/cli/telemetry.ts +70 -0
- package/scripts/cli/tray/index.ts +263 -263
- package/scripts/cli/tray/windows.ps1 +107 -107
- package/scripts/cli/tray.ts +192 -208
- package/scripts/cli/update.ts +39 -39
- package/scripts/cli/version.ts +67 -67
- package/scripts/cli/wordmark.ts +58 -58
- package/src/boot/app.ts +16 -1
- package/src/boot/dependencies.ts +128 -25
- package/src/config/package-paths.ts +24 -0
- package/src/config/resources.ts +137 -0
- package/src/config/runtime.ts +133 -76
- package/src/config/spec.ts +30 -29
- package/src/config/telemetry.ts +0 -25
- package/src/console/api-keys/bucket-spend.ts +14 -17
- package/src/console/api-keys/contracts.ts +410 -410
- package/src/console/api-keys/routes.ts +12 -0
- package/src/console/audit/store.ts +2 -3
- package/src/console/auth/setup-service.ts +8 -1
- package/src/console/backup/routes.ts +7 -0
- package/src/console/backup/service.ts +463 -463
- package/src/console/backup/store.ts +16 -11
- package/src/console/backup/validate.ts +1 -4
- package/src/console/cli-tools/fs-ops.ts +18 -2
- package/src/console/data-management/contracts.ts +1 -8
- package/src/console/data-management/operations.ts +61 -48
- package/src/console/data-management/routes.ts +4 -2
- package/src/console/domain-registration.ts +25 -3
- package/src/console/inferhub/client.ts +139 -0
- package/src/console/inferhub/key-store.ts +129 -0
- package/src/console/inferhub/normalize.ts +347 -0
- package/src/console/inferhub/routes.ts +411 -0
- package/src/console/inferhub/store.ts +109 -0
- package/src/console/inferhub/types.ts +183 -0
- package/src/console/observability/contracts.ts +2 -41
- package/src/console/observability/store.ts +176 -371
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/store.ts +23 -36
- package/src/console/quota/account-quota-shared.ts +52 -52
- package/src/console/quota/account-quota-tenant.ts +16 -24
- package/src/console/quota/refresh.ts +441 -441
- package/src/console/remote-support/service.ts +270 -114
- package/src/console/routing/pools/import-parse.ts +206 -206
- package/src/console/routing/pools/operations.ts +543 -543
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -181
- package/src/console/routing/pools/store.ts +1 -1
- package/src/console/settings/contracts.ts +42 -40
- package/src/console/settings/store.ts +25 -25
- package/src/console/share/share-stats.ts +22 -4
- package/src/console/share/share-usage.ts +30 -4
- package/src/console/shared/cursor.ts +15 -0
- package/src/console/updates/repository-updates.ts +40 -39
- package/src/console/updates/routes.ts +12 -0
- package/src/main.ts +13 -5
- package/src/network/pool/resolver.ts +1104 -1104
- package/src/observability/client-name.ts +22 -0
- package/src/observability/event-log.ts +5 -29
- package/src/observability/event-query.ts +690 -0
- package/src/observability/log-ring.ts +26 -2
- package/src/observability/log-sanitize.ts +118 -0
- package/src/observability/logger.ts +17 -13
- package/src/observability/metrics.ts +35 -0
- package/src/observability/payload-capture.ts +114 -41
- package/src/observability/payload-store.ts +249 -24
- package/src/observability/raw-event-writer.ts +118 -0
- package/src/observability/runtime-metrics.ts +71 -1
- package/src/observability/telemetry-buffer.ts +100 -17
- package/src/observability/telemetry-reconciliation.ts +417 -0
- package/src/persistence/boot.ts +20 -9
- package/src/persistence/db-pg.ts +13 -0
- package/src/persistence/db-sqlite.ts +569 -497
- package/src/persistence/dialect.ts +3 -2
- package/src/persistence/migrate.ts +181 -181
- package/src/persistence/schema.ts +1496 -1406
- package/src/persistence/shared-cache.ts +5 -1
- package/src/persistence/stores/api-key-store.ts +581 -581
- package/src/persistence/stores/telemetry-store.ts +124 -63
- package/src/protocol/reasoning-text.ts +30 -0
- package/src/protocol/registry.ts +2 -0
- package/src/protocol/response/chat.ts +33 -7
- package/src/protocol/response/codex.ts +14 -2
- package/src/protocol/response/messages.ts +5 -2
- package/src/providers/authentication/oauth-flow-store.ts +3 -10
- package/src/providers/bundled-discovery-providers.ts +122 -122
- package/src/providers/default-registry.ts +14 -0
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +43 -7
- package/src/providers/discovery/probing-service.ts +70 -32
- package/src/providers/integrations/antigravity/antigravity.ts +9 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
- package/src/providers/integrations/buddy/capabilities.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
- package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
- package/src/providers/integrations/claude/claude-quota.ts +2 -2
- package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
- package/src/providers/integrations/claude/claude.ts +3 -4
- package/src/providers/integrations/cline/cline.ts +94 -8
- package/src/providers/integrations/codex/codex.ts +61 -37
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
- package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
- package/src/providers/integrations/cursor/cursor-request.ts +11 -0
- package/src/providers/integrations/cursor/cursor.ts +29 -17
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
- package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
- package/src/providers/integrations/github/github.ts +3 -1
- package/src/providers/integrations/grok/grok-quota.ts +279 -279
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
- package/src/providers/integrations/hermes.ts +7 -1
- package/src/providers/integrations/inferhub.ts +201 -73
- package/src/providers/integrations/kilo/kilo.ts +152 -152
- package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
- package/src/providers/integrations/opencode/provider.ts +2 -0
- package/src/providers/integrations/openrouter/provider.ts +129 -129
- package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
- package/src/providers/integrations/trae/trae-headers.ts +17 -48
- package/src/providers/integrations/trae/trae-oauth.ts +4 -26
- package/src/providers/integrations/trae/trae-quota.ts +389 -384
- package/src/providers/integrations/trae/trae-realm.ts +15 -2
- package/src/providers/integrations/trae/trae.ts +22 -24
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-health-service.ts +605 -279
- package/src/providers/provider-metadata.ts +12 -1
- package/src/providers/provider-model-definition.ts +373 -373
- package/src/providers/provider-probe.ts +3 -0
- package/src/providers/quota/quota-contracts.ts +3 -7
- package/src/providers/search/search-provider.ts +2 -6
- package/src/security/admission/contracts.ts +10 -0
- package/src/security/admission/in-memory-store.ts +89 -14
- package/src/security/admission/lua.ts +191 -182
- package/src/security/admission/redis-store.ts +30 -0
- package/src/security/admission/service.ts +18 -0
- package/src/security/api-key-auth-cache.ts +96 -0
- package/src/security/api-key-auth.ts +11 -100
- package/src/security/client-router-fingerprint.ts +3 -7
- package/src/security/ip-boundary.ts +7 -3
- package/src/shared/coerce.ts +67 -0
- package/src/shared/hash.ts +6 -0
- package/src/shared/time.ts +7 -0
- package/src/transport/dispatch/attempt-finalize.ts +26 -138
- package/src/transport/dispatch/attempt-loop.ts +103 -81
- package/src/transport/dispatch/capture-policy.ts +78 -0
- package/src/transport/dispatch/proxy-request.ts +4 -2
- package/src/transport/dispatch/streaming/encoder.ts +111 -5
- package/src/transport/dispatch/streaming/stream.ts +19 -2
- package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
- package/src/transport/failure-policy.ts +27 -0
- package/src/transport/guards/abuse-guard.ts +2 -2
- package/src/transport/guards/auth-guard.ts +17 -0
- package/src/transport/guards/body-policy.ts +3 -2
- package/src/transport/guards/concurrency-guard.ts +89 -13
- package/src/transport/guards/early-rejection-capture.ts +43 -3
- package/src/transport/guards/error-lifecycle.ts +10 -2
- package/src/transport/guards/guard-errors.ts +23 -0
- package/src/transport/guards/headroom-guard.ts +41 -7
- package/src/transport/guards/pipeline.ts +24 -0
- package/src/transport/guards/request-context.ts +2 -1
- package/src/transport/routing/admission.ts +51 -9
- package/src/transport/routing/bounded-lru.ts +49 -0
- package/src/transport/routing/candidate-index.ts +88 -0
- package/src/transport/routing/reservations.ts +121 -7
- package/src/transport/routing/round-robin.ts +10 -22
- package/src/transport/routing/route-catalog.ts +26 -13
- package/src/transport/routing/route-model.ts +22 -2
- package/src/transport/routing/router.ts +53 -31
- package/src/transport/surface/chat/encode.ts +2 -3
- package/src/transport/surface/chat/parse.ts +2 -1
- package/src/transport/surface/dialects.ts +11 -3
- package/src/transport/surface/messages/encode.ts +2 -1
- package/src/transport/surface/messages/parse.ts +3 -1
- package/src/transport/surface/messages/stream.ts +2 -6
- package/src/workers/maintenance-lease.ts +200 -0
- package/src/workers/scheduler.ts +101 -1
- package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
- package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/scripts/build/aot.ts +0 -143
- package/scripts/build/build.ts +0 -24
- package/scripts/build/compile.ts +0 -88
- package/scripts/build/restart.ts +0 -120
- package/scripts/build/start.ts +0 -25
- package/src/observability/event-compaction.ts +0 -317
- package/src/observability/event-day-index.ts +0 -440
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical telemetry reader.
|
|
3
|
+
*
|
|
4
|
+
* `telemetry_events` is already written by the production path (the
|
|
5
|
+
* transactional `insertEvents`), is indexed for keyset pagination and
|
|
6
|
+
* tenant-scoped breakdowns, and carries exactly the bounded metadata fields
|
|
7
|
+
* the console aggregates over — bodies live only in payload frame files. It
|
|
8
|
+
* is therefore the analytics source: every function here runs its aggregate
|
|
9
|
+
* IN SQL and returns only totals, groups, or buckets, so no read materializes
|
|
10
|
+
* the retained window in JavaScript.
|
|
11
|
+
*
|
|
12
|
+
* Rules enforced here:
|
|
13
|
+
* - every statement is tenant-scoped and `created_at >= since`;
|
|
14
|
+
* - list/detail use `(created_at, id)` keyset pagination, newest first;
|
|
15
|
+
* - latency percentiles are computed by the database (Postgres
|
|
16
|
+
* `percentile_cont`, SQLite nearest-rank via ordered OFFSET/LIMIT) —
|
|
17
|
+
* latency arrays are never returned to JavaScript;
|
|
18
|
+
* - `isGatewayError()` semantics live in one shared SQL predicate.
|
|
19
|
+
*/
|
|
20
|
+
import { and, desc, eq, gte, inArray, isNotNull, sql } from "drizzle-orm";
|
|
21
|
+
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
22
|
+
import { telemetryEvents } from "../persistence/schema";
|
|
23
|
+
import type { TelemetryEventRecord } from "./event-log";
|
|
24
|
+
import { gatewayErrorSql } from "./telemetry-status";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The SQL predicate for "this request counts as a gateway error".
|
|
28
|
+
*
|
|
29
|
+
* `gatewayErrorSql` (telemetry-status.ts) is derived from the same constants
|
|
30
|
+
* `isGatewayError` reads, so the row-level predicate, the console's error
|
|
31
|
+
* totals, and the durable usage rollup cannot drift apart — an unknown status
|
|
32
|
+
* must be classified identically in all three.
|
|
33
|
+
*/
|
|
34
|
+
const IS_GATEWAY_ERROR_SQL = gatewayErrorSql(telemetryEvents.status, telemetryEvents.httpStatus);
|
|
35
|
+
|
|
36
|
+
/** One bounded page of events, newest first. */
|
|
37
|
+
export interface EventPage {
|
|
38
|
+
readonly events: TelemetryEventRecord[];
|
|
39
|
+
/** Cursor for the next (older) page; undefined when the window is exhausted. */
|
|
40
|
+
readonly nextCursor?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface EventListQuery {
|
|
44
|
+
readonly tenantId: string;
|
|
45
|
+
/**
|
|
46
|
+
* Lower bound. Omitting it means "the whole retained window" — the SQL table
|
|
47
|
+
* is pruned by the same retention rule as the raw files, so an unbounded
|
|
48
|
+
* query is bounded by retention rather than unbounded in effect. (Backup
|
|
49
|
+
* export is the one caller that needs the full window.)
|
|
50
|
+
*/
|
|
51
|
+
readonly since?: Date;
|
|
52
|
+
readonly until?: Date;
|
|
53
|
+
readonly apiKeyIds?: readonly string[];
|
|
54
|
+
readonly accountIds?: readonly string[];
|
|
55
|
+
readonly requestId?: string;
|
|
56
|
+
/** Several request ids at once — the backup restore's "already present" check. */
|
|
57
|
+
readonly requestIds?: readonly string[];
|
|
58
|
+
readonly id?: string;
|
|
59
|
+
readonly httpStatus?: number;
|
|
60
|
+
readonly limit?: number;
|
|
61
|
+
/** Keyset cursor: `{createdAt}|{id}` of the last row of the previous page. */
|
|
62
|
+
readonly cursor?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface UsageSummary {
|
|
66
|
+
readonly requests: number;
|
|
67
|
+
readonly errors: number;
|
|
68
|
+
/** Lifecycle breakdown: cancelled and truncated counts, for the summary card. */
|
|
69
|
+
readonly cancelled: number;
|
|
70
|
+
readonly truncated: number;
|
|
71
|
+
readonly inputTokens: number;
|
|
72
|
+
readonly outputTokens: number;
|
|
73
|
+
readonly cachedInputTokens: number;
|
|
74
|
+
readonly reasoningTokens: number;
|
|
75
|
+
readonly estimatedCostUsd: number;
|
|
76
|
+
/** Requests whose latency was recorded — the denominator for `latencyMs`. */
|
|
77
|
+
readonly sampled: number;
|
|
78
|
+
readonly latencyMs: {
|
|
79
|
+
readonly avg: number | null;
|
|
80
|
+
readonly p50: number | null;
|
|
81
|
+
readonly p95: number | null;
|
|
82
|
+
readonly p99: number | null;
|
|
83
|
+
};
|
|
84
|
+
/** Requests whose usage carried no price; the analytics `partial` flag. */
|
|
85
|
+
readonly unpriced: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface BreakdownGroup {
|
|
89
|
+
readonly key: string;
|
|
90
|
+
readonly requests: number;
|
|
91
|
+
readonly errors: number;
|
|
92
|
+
readonly inputTokens: number;
|
|
93
|
+
readonly outputTokens: number;
|
|
94
|
+
/** Summed estimated cost; null when no row in the group carried a price. */
|
|
95
|
+
readonly costUsd: number | null;
|
|
96
|
+
/** Mean tokens/sec across rows that reported one; 0 when none did. */
|
|
97
|
+
readonly avgTokensPerSec: number;
|
|
98
|
+
/** Cached-input share of the prompt, summed for the group. */
|
|
99
|
+
readonly cachedInputTokens: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ChartBucket {
|
|
103
|
+
readonly at: number;
|
|
104
|
+
readonly requests: number;
|
|
105
|
+
readonly errors: number;
|
|
106
|
+
readonly inputTokens: number;
|
|
107
|
+
readonly outputTokens: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface HealthTotals {
|
|
111
|
+
readonly requests: number;
|
|
112
|
+
readonly errors: number;
|
|
113
|
+
readonly inputTokens: number;
|
|
114
|
+
readonly outputTokens: number;
|
|
115
|
+
readonly cachedInputTokens: number;
|
|
116
|
+
readonly latencyAvgMs: number | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const DEFAULT_LIMIT = 50;
|
|
120
|
+
const MAX_LIMIT = 500;
|
|
121
|
+
|
|
122
|
+
function numeric(value: unknown): number {
|
|
123
|
+
const parsed = Number(value ?? 0);
|
|
124
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function numericNullable(value: unknown): number | null {
|
|
128
|
+
const parsed = Number(value ?? 0);
|
|
129
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Maps one `telemetry_events` row to the console's record shape. */
|
|
133
|
+
export function eventRecordFromRow(row: {
|
|
134
|
+
readonly id: string | null;
|
|
135
|
+
readonly createdAt: Date;
|
|
136
|
+
readonly tenantId: string;
|
|
137
|
+
readonly requestId: string;
|
|
138
|
+
readonly sourceSurface: string | null;
|
|
139
|
+
readonly requestedModel: string | null;
|
|
140
|
+
readonly reportedModel: string | null;
|
|
141
|
+
readonly requestedEffort: string | null;
|
|
142
|
+
readonly providerId: string | null;
|
|
143
|
+
readonly accountId: string | null;
|
|
144
|
+
readonly networkPoolId: string | null;
|
|
145
|
+
readonly endpoint: string | null;
|
|
146
|
+
readonly apiKeyId: string | null;
|
|
147
|
+
readonly userAgent: string | null;
|
|
148
|
+
readonly clientName: string | null;
|
|
149
|
+
readonly clientIp: string | null;
|
|
150
|
+
readonly latencyMs: number | null;
|
|
151
|
+
readonly ttfbMs: number | null;
|
|
152
|
+
readonly stream: boolean | null;
|
|
153
|
+
readonly status: string | null;
|
|
154
|
+
readonly httpStatus: number | null;
|
|
155
|
+
readonly errorOrigin: string | null;
|
|
156
|
+
readonly errorCategory: string | null;
|
|
157
|
+
readonly inputTokens: number | null;
|
|
158
|
+
readonly cachedInputTokens: number | null;
|
|
159
|
+
readonly outputTokens: number | null;
|
|
160
|
+
readonly reasoningTokens: number | null;
|
|
161
|
+
readonly estimatedCostUsd: string | number | null;
|
|
162
|
+
readonly creditUsed: string | number | null;
|
|
163
|
+
readonly resolveMs: number | null;
|
|
164
|
+
readonly tokensPerSec: string | number | null;
|
|
165
|
+
readonly firstContentDeltaAtMs: number | null;
|
|
166
|
+
readonly lastEventAtMs: number | null;
|
|
167
|
+
readonly payloadCaptureState: string | null;
|
|
168
|
+
}): TelemetryEventRecord {
|
|
169
|
+
return {
|
|
170
|
+
id: row.id ?? row.requestId,
|
|
171
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
172
|
+
tenantId: row.tenantId,
|
|
173
|
+
requestId: row.requestId,
|
|
174
|
+
sourceSurface: row.sourceSurface,
|
|
175
|
+
requestedModel: row.requestedModel,
|
|
176
|
+
reportedModel: row.reportedModel,
|
|
177
|
+
requestedEffort: row.requestedEffort,
|
|
178
|
+
providerId: row.providerId,
|
|
179
|
+
accountId: row.accountId,
|
|
180
|
+
networkPoolId: row.networkPoolId,
|
|
181
|
+
endpoint: row.endpoint,
|
|
182
|
+
apiKeyId: row.apiKeyId,
|
|
183
|
+
userAgent: row.userAgent,
|
|
184
|
+
clientIp: row.clientIp,
|
|
185
|
+
latencyMs: row.latencyMs,
|
|
186
|
+
ttfbMs: row.ttfbMs,
|
|
187
|
+
stream: row.stream,
|
|
188
|
+
status: row.status,
|
|
189
|
+
httpStatus: row.httpStatus,
|
|
190
|
+
errorOrigin: row.errorOrigin,
|
|
191
|
+
errorCategory: row.errorCategory,
|
|
192
|
+
inputTokens: row.inputTokens,
|
|
193
|
+
cachedInputTokens: row.cachedInputTokens,
|
|
194
|
+
outputTokens: row.outputTokens,
|
|
195
|
+
reasoningTokens: row.reasoningTokens,
|
|
196
|
+
estimatedCostUsd: numericNullable(row.estimatedCostUsd),
|
|
197
|
+
creditUsed: numericNullable(row.creditUsed),
|
|
198
|
+
resolveMs: row.resolveMs,
|
|
199
|
+
tokensPerSec: numericNullable(row.tokensPerSec),
|
|
200
|
+
firstContentDeltaAtMs: row.firstContentDeltaAtMs,
|
|
201
|
+
lastEventAtMs: row.lastEventAtMs,
|
|
202
|
+
payloadCaptureState: row.payloadCaptureState,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* True when this handle speaks PostgreSQL.
|
|
208
|
+
*
|
|
209
|
+
* Read off the live dialect rather than `resolveDbMode()`/`handle.kind`:
|
|
210
|
+
* `CartethyiaDatabase` is the pg-typed builder on both backends (the SQLite
|
|
211
|
+
* adapter shares the type), and a test can build a SQLite handle while the
|
|
212
|
+
* process' mode is `full` — a mode-based guess then sends `percentile_cont`,
|
|
213
|
+
* which SQLite lacks, to SQLite. The dialect is the thing that actually
|
|
214
|
+
* renders the SQL, so it is the only answer that cannot disagree with it.
|
|
215
|
+
*/
|
|
216
|
+
function isPostgres(db: CartethyiaDatabase): boolean {
|
|
217
|
+
const dialect = (db as unknown as { dialect?: { constructor?: { name?: string } } }).dialect;
|
|
218
|
+
return dialect?.constructor?.name === "PostgresDialect";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The tenant predicate — the scope every read shares — plus the window when
|
|
223
|
+
* one is given.
|
|
224
|
+
*
|
|
225
|
+
* Tenant is NOT optional: every statement here is tenant-scoped, and a query
|
|
226
|
+
* that forgot it would read another tenant's traffic.
|
|
227
|
+
*/
|
|
228
|
+
function scoped(tenantId: string, since?: Date) {
|
|
229
|
+
const tenant = eq(telemetryEvents.tenantId, tenantId);
|
|
230
|
+
return since === undefined ? tenant : and(tenant, gte(telemetryEvents.createdAt, since));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Bounded detail/list read — the only place rows are materialized.
|
|
235
|
+
*
|
|
236
|
+
* Keyset pagination on `(created_at DESC, id DESC)`: `created_at` has
|
|
237
|
+
* second resolution, so a cursor on it alone could skip or repeat rows at a
|
|
238
|
+
* boundary. The cursor is the last row of the page, encoded as
|
|
239
|
+
* `{createdAtMs}|{id}`.
|
|
240
|
+
*/
|
|
241
|
+
export async function queryTelemetryEvents(
|
|
242
|
+
db: CartethyiaDatabase,
|
|
243
|
+
query: EventListQuery,
|
|
244
|
+
): Promise<EventPage> {
|
|
245
|
+
const limit = Math.max(1, Math.min(MAX_LIMIT, Math.trunc(query.limit ?? DEFAULT_LIMIT)));
|
|
246
|
+
const conditions = [scoped(query.tenantId, query.since)];
|
|
247
|
+
if (query.until !== undefined) {
|
|
248
|
+
conditions.push(sql`${telemetryEvents.createdAt} < ${query.until}`);
|
|
249
|
+
}
|
|
250
|
+
if (query.apiKeyIds !== undefined) {
|
|
251
|
+
conditions.push(inArray(telemetryEvents.apiKeyId, [...query.apiKeyIds]));
|
|
252
|
+
}
|
|
253
|
+
if (query.accountIds !== undefined) {
|
|
254
|
+
conditions.push(inArray(telemetryEvents.accountId, [...query.accountIds]));
|
|
255
|
+
}
|
|
256
|
+
if (query.requestId !== undefined) {
|
|
257
|
+
conditions.push(eq(telemetryEvents.requestId, query.requestId));
|
|
258
|
+
}
|
|
259
|
+
if (query.requestIds !== undefined) {
|
|
260
|
+
conditions.push(inArray(telemetryEvents.requestId, [...query.requestIds]));
|
|
261
|
+
}
|
|
262
|
+
if (query.id !== undefined) {
|
|
263
|
+
conditions.push(eq(telemetryEvents.id, query.id));
|
|
264
|
+
}
|
|
265
|
+
if (query.httpStatus !== undefined) {
|
|
266
|
+
conditions.push(eq(telemetryEvents.httpStatus, query.httpStatus));
|
|
267
|
+
}
|
|
268
|
+
if (query.cursor !== undefined) {
|
|
269
|
+
const separator = query.cursor.indexOf("|");
|
|
270
|
+
const at = Number(query.cursor.slice(0, separator));
|
|
271
|
+
const id = query.cursor.slice(separator + 1);
|
|
272
|
+
if (Number.isFinite(at) && id.length > 0) {
|
|
273
|
+
conditions.push(
|
|
274
|
+
sql`(${telemetryEvents.createdAt} < ${new Date(at)} OR (${telemetryEvents.createdAt} = ${new Date(at)} AND ${telemetryEvents.id} < ${id}))`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const rows = await db
|
|
280
|
+
.select()
|
|
281
|
+
.from(telemetryEvents)
|
|
282
|
+
.where(and(...conditions))
|
|
283
|
+
.orderBy(desc(telemetryEvents.createdAt), desc(telemetryEvents.id))
|
|
284
|
+
.limit(limit + 1);
|
|
285
|
+
|
|
286
|
+
const page = rows.slice(0, limit);
|
|
287
|
+
const hasMore = rows.length > limit;
|
|
288
|
+
const last = page[page.length - 1];
|
|
289
|
+
const events = page.map((row) => eventRecordFromRow(row as Parameters<typeof eventRecordFromRow>[0]));
|
|
290
|
+
return {
|
|
291
|
+
events,
|
|
292
|
+
...(hasMore && last
|
|
293
|
+
? {
|
|
294
|
+
nextCursor: `${new Date(last.createdAt as unknown as Date).getTime()}|${last.id ?? last.requestId}`,
|
|
295
|
+
}
|
|
296
|
+
: {}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** One event by request id — a single indexed row, never a window scan. */
|
|
301
|
+
export async function eventByRequestId(
|
|
302
|
+
db: CartethyiaDatabase,
|
|
303
|
+
tenantId: string,
|
|
304
|
+
requestId: string,
|
|
305
|
+
): Promise<TelemetryEventRecord | undefined> {
|
|
306
|
+
const [row] = await db
|
|
307
|
+
.select()
|
|
308
|
+
.from(telemetryEvents)
|
|
309
|
+
.where(and(eq(telemetryEvents.tenantId, tenantId), eq(telemetryEvents.requestId, requestId)))
|
|
310
|
+
.limit(1);
|
|
311
|
+
if (!row) return undefined;
|
|
312
|
+
return eventRecordFromRow(row as Parameters<typeof eventRecordFromRow>[0]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Totals plus latency percentiles for one tenant window.
|
|
317
|
+
*
|
|
318
|
+
* Percentiles are computed by the database. Postgres uses `percentile_cont`;
|
|
319
|
+
* SQLite (no ordered-set aggregates) computes nearest-rank percentiles from
|
|
320
|
+
* the ordered latency column via OFFSET/LIMIT. Either way, latency values are
|
|
321
|
+
* never returned to JavaScript — the array that the old raw reader built to
|
|
322
|
+
* sort in JS was the memory cost this replaces.
|
|
323
|
+
*/
|
|
324
|
+
export async function summarizeTelemetry(
|
|
325
|
+
db: CartethyiaDatabase,
|
|
326
|
+
tenantId: string,
|
|
327
|
+
since: Date,
|
|
328
|
+
): Promise<UsageSummary> {
|
|
329
|
+
const condition = scoped(tenantId, since);
|
|
330
|
+
const [row] = await db
|
|
331
|
+
.select({
|
|
332
|
+
requests: sql<number>`count(*)`,
|
|
333
|
+
errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
|
|
334
|
+
cancelled: sql<number>`sum(case when ${telemetryEvents.status} = 'cancelled' then 1 else 0 end)`,
|
|
335
|
+
truncated: sql<number>`sum(case when ${telemetryEvents.status} = 'truncated' then 1 else 0 end)`,
|
|
336
|
+
inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
|
|
337
|
+
outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
|
|
338
|
+
cachedInputTokens: sql<number>`coalesce(sum(${telemetryEvents.cachedInputTokens}), 0)`,
|
|
339
|
+
reasoningTokens: sql<number>`coalesce(sum(${telemetryEvents.reasoningTokens}), 0)`,
|
|
340
|
+
estimatedCostUsd: sql<number>`coalesce(sum(${telemetryEvents.estimatedCostUsd}), 0)`,
|
|
341
|
+
sampled: sql<number>`count(${telemetryEvents.latencyMs})`,
|
|
342
|
+
latencyAvg: sql<number>`avg(${telemetryEvents.latencyMs})`,
|
|
343
|
+
unpriced: sql<number>`sum(case when ${telemetryEvents.estimatedCostUsd} is null then 1 else 0 end)`,
|
|
344
|
+
})
|
|
345
|
+
.from(telemetryEvents)
|
|
346
|
+
.where(condition);
|
|
347
|
+
if (!row) {
|
|
348
|
+
return {
|
|
349
|
+
requests: 0,
|
|
350
|
+
errors: 0,
|
|
351
|
+
cancelled: 0,
|
|
352
|
+
truncated: 0,
|
|
353
|
+
inputTokens: 0,
|
|
354
|
+
outputTokens: 0,
|
|
355
|
+
cachedInputTokens: 0,
|
|
356
|
+
reasoningTokens: 0,
|
|
357
|
+
estimatedCostUsd: 0,
|
|
358
|
+
sampled: 0,
|
|
359
|
+
latencyMs: { avg: null, p50: null, p95: null, p99: null },
|
|
360
|
+
unpriced: 0,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const sampled = numeric(row.sampled);
|
|
365
|
+
const percentiles = await latencyPercentiles(db, tenantId, since, sampled);
|
|
366
|
+
return {
|
|
367
|
+
requests: numeric(row.requests),
|
|
368
|
+
errors: numeric(row.errors),
|
|
369
|
+
cancelled: numeric(row.cancelled),
|
|
370
|
+
truncated: numeric(row.truncated),
|
|
371
|
+
inputTokens: numeric(row.inputTokens),
|
|
372
|
+
outputTokens: numeric(row.outputTokens),
|
|
373
|
+
cachedInputTokens: numeric(row.cachedInputTokens),
|
|
374
|
+
reasoningTokens: numeric(row.reasoningTokens),
|
|
375
|
+
estimatedCostUsd: numeric(row.estimatedCostUsd),
|
|
376
|
+
sampled,
|
|
377
|
+
latencyMs: {
|
|
378
|
+
avg: numericNullable(row.latencyAvg),
|
|
379
|
+
...percentiles,
|
|
380
|
+
},
|
|
381
|
+
unpriced: numeric(row.unpriced),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
type Percentiles = { p50: number | null; p95: number | null; p99: number | null };
|
|
386
|
+
|
|
387
|
+
async function latencyPercentiles(
|
|
388
|
+
db: CartethyiaDatabase,
|
|
389
|
+
tenantId: string,
|
|
390
|
+
since: Date,
|
|
391
|
+
sampled: number,
|
|
392
|
+
): Promise<Percentiles> {
|
|
393
|
+
const empty: Percentiles = { p50: null, p95: null, p99: null };
|
|
394
|
+
if (sampled <= 0) return empty;
|
|
395
|
+
const base = [scoped(tenantId, since), isNotNull(telemetryEvents.latencyMs)];
|
|
396
|
+
|
|
397
|
+
// Postgres: one statement computing all three continuous percentiles.
|
|
398
|
+
if (isPostgres(db)) {
|
|
399
|
+
const [row] = await db
|
|
400
|
+
.select({
|
|
401
|
+
p50: sql<number>`percentile_cont(0.50) within group (order by ${telemetryEvents.latencyMs})`,
|
|
402
|
+
p95: sql<number>`percentile_cont(0.95) within group (order by ${telemetryEvents.latencyMs})`,
|
|
403
|
+
p99: sql<number>`percentile_cont(0.99) within group (order by ${telemetryEvents.latencyMs})`,
|
|
404
|
+
})
|
|
405
|
+
.from(telemetryEvents)
|
|
406
|
+
.where(and(...base));
|
|
407
|
+
if (!row) return empty;
|
|
408
|
+
return {
|
|
409
|
+
p50: numericNullable(row.p50),
|
|
410
|
+
p95: numericNullable(row.p95),
|
|
411
|
+
p99: numericNullable(row.p99),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// SQLite: nearest-rank percentile = the value at ceil(p * n) rows into the
|
|
416
|
+
// ordered latency column, via ORDER BY + OFFSET/LIMIT in SQL.
|
|
417
|
+
const rankAt = async (fraction: number): Promise<number | null> => {
|
|
418
|
+
const offset = Math.max(0, Math.ceil(fraction * sampled) - 1);
|
|
419
|
+
const [row] = await db
|
|
420
|
+
.select({ value: telemetryEvents.latencyMs })
|
|
421
|
+
.from(telemetryEvents)
|
|
422
|
+
.where(and(...base))
|
|
423
|
+
.orderBy(telemetryEvents.latencyMs)
|
|
424
|
+
.limit(1)
|
|
425
|
+
.offset(offset);
|
|
426
|
+
return row?.value ?? null;
|
|
427
|
+
};
|
|
428
|
+
const [p50, p95, p99] = await Promise.all([rankAt(0.5), rankAt(0.95), rankAt(0.99)]);
|
|
429
|
+
return { p50, p95, p99 };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The SQL column one breakdown dimension groups by. */
|
|
433
|
+
function dimensionColumn(dimension: string) {
|
|
434
|
+
if (dimension === "model") return telemetryEvents.requestedModel;
|
|
435
|
+
if (dimension === "provider") return telemetryEvents.providerId;
|
|
436
|
+
if (dimension === "account") return telemetryEvents.accountId;
|
|
437
|
+
if (dimension === "api_key" || dimension === "key") return telemetryEvents.apiKeyId;
|
|
438
|
+
if (dimension === "client") return telemetryEvents.clientName;
|
|
439
|
+
// `client_ip` groups on the raw address; masking stays a post-grouping
|
|
440
|
+
// presentation step in the console, unchanged.
|
|
441
|
+
if (dimension === "client_ip" || dimension === "ip") return telemetryEvents.clientIp;
|
|
442
|
+
if (dimension === "status") return telemetryEvents.httpStatus;
|
|
443
|
+
if (dimension === "endpoint") return telemetryEvents.endpoint;
|
|
444
|
+
return telemetryEvents.requestedModel;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** The dimension's raw value as a string, for the console's grouping step. */
|
|
448
|
+
function dimensionValue(raw: unknown): string {
|
|
449
|
+
const value = raw === null || raw === undefined ? "" : String(raw);
|
|
450
|
+
return value.length === 0 ? "unknown" : value;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* One dimension's groups, at most 100, ordered by request count.
|
|
455
|
+
*
|
|
456
|
+
* Grouping happens in SQL; only the groups come back. `client` groups on the
|
|
457
|
+
* ingestion-stamped `client_name` rather than re-deriving a name per row in
|
|
458
|
+
* JavaScript, and `client_ip` masking stays a post-grouping presentation step.
|
|
459
|
+
*/
|
|
460
|
+
export async function breakdownTelemetry(
|
|
461
|
+
db: CartethyiaDatabase,
|
|
462
|
+
tenantId: string,
|
|
463
|
+
dimension: string,
|
|
464
|
+
since: Date,
|
|
465
|
+
): Promise<BreakdownGroup[]> {
|
|
466
|
+
const column = dimensionColumn(dimension);
|
|
467
|
+
const rows = await db
|
|
468
|
+
.select({
|
|
469
|
+
key: column,
|
|
470
|
+
requests: sql<number>`count(*)`,
|
|
471
|
+
errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
|
|
472
|
+
inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
|
|
473
|
+
outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
|
|
474
|
+
cachedInputTokens: sql<number>`coalesce(sum(${telemetryEvents.cachedInputTokens}), 0)`,
|
|
475
|
+
costUsd: sql<number>`coalesce(sum(${telemetryEvents.estimatedCostUsd}), 0)`,
|
|
476
|
+
avgTokensPerSec: sql<number>`coalesce(avg(${telemetryEvents.tokensPerSec}), 0)`,
|
|
477
|
+
})
|
|
478
|
+
.from(telemetryEvents)
|
|
479
|
+
.where(scoped(tenantId, since))
|
|
480
|
+
.groupBy(column)
|
|
481
|
+
.orderBy(desc(sql`count(*)`))
|
|
482
|
+
.limit(100);
|
|
483
|
+
return rows.map((row) => ({
|
|
484
|
+
key: dimensionValue(row.key),
|
|
485
|
+
requests: numeric(row.requests),
|
|
486
|
+
errors: numeric(row.errors),
|
|
487
|
+
inputTokens: numeric(row.inputTokens),
|
|
488
|
+
outputTokens: numeric(row.outputTokens),
|
|
489
|
+
cachedInputTokens: numeric(row.cachedInputTokens),
|
|
490
|
+
// A group whose rows carried no price reports null, matching the console's
|
|
491
|
+
// "cost is unknown" reading rather than a false zero.
|
|
492
|
+
costUsd: numeric(row.costUsd) > 0 ? numeric(row.costUsd) : null,
|
|
493
|
+
avgTokensPerSec: numeric(row.avgTokensPerSec),
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Time-series buckets, computed in SQL.
|
|
499
|
+
*
|
|
500
|
+
* Bucket width comes from the caller (the console's existing period rule); the
|
|
501
|
+
* epoch bucketing is portable — `to_timestamp`/`unixepoch` differences are
|
|
502
|
+
* handled per backend.
|
|
503
|
+
*/
|
|
504
|
+
export async function chartTelemetry(
|
|
505
|
+
db: CartethyiaDatabase,
|
|
506
|
+
tenantId: string,
|
|
507
|
+
since: Date,
|
|
508
|
+
bucketSeconds: number,
|
|
509
|
+
): Promise<ChartBucket[]> {
|
|
510
|
+
const width = Math.max(1, Math.trunc(bucketSeconds));
|
|
511
|
+
const bucketExpression = isPostgres(db)
|
|
512
|
+
? sql<number>`(cast(extract(epoch from ${telemetryEvents.createdAt}) as bigint) / ${width}) * ${width}`
|
|
513
|
+
: sql<number>`(cast(strftime('%s', ${telemetryEvents.createdAt}) as integer) / ${width}) * ${width}`;
|
|
514
|
+
const rows = await db
|
|
515
|
+
.select({
|
|
516
|
+
at: bucketExpression,
|
|
517
|
+
requests: sql<number>`count(*)`,
|
|
518
|
+
errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
|
|
519
|
+
inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
|
|
520
|
+
outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
|
|
521
|
+
})
|
|
522
|
+
.from(telemetryEvents)
|
|
523
|
+
.where(scoped(tenantId, since))
|
|
524
|
+
.groupBy(bucketExpression)
|
|
525
|
+
.orderBy(bucketExpression);
|
|
526
|
+
return rows.map((row) => ({
|
|
527
|
+
at: numeric(row.at) * 1000,
|
|
528
|
+
requests: numeric(row.requests),
|
|
529
|
+
errors: numeric(row.errors),
|
|
530
|
+
inputTokens: numeric(row.inputTokens),
|
|
531
|
+
outputTokens: numeric(row.outputTokens),
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Overview/health totals for one tenant window. */
|
|
536
|
+
export async function healthTelemetry(
|
|
537
|
+
db: CartethyiaDatabase,
|
|
538
|
+
tenantId: string,
|
|
539
|
+
since: Date,
|
|
540
|
+
): Promise<HealthTotals> {
|
|
541
|
+
const [row] = await db
|
|
542
|
+
.select({
|
|
543
|
+
requests: sql<number>`count(*)`,
|
|
544
|
+
errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
|
|
545
|
+
inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
|
|
546
|
+
outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
|
|
547
|
+
cachedInputTokens: sql<number>`coalesce(sum(${telemetryEvents.cachedInputTokens}), 0)`,
|
|
548
|
+
latencyAvg: sql<number>`avg(${telemetryEvents.latencyMs})`,
|
|
549
|
+
})
|
|
550
|
+
.from(telemetryEvents)
|
|
551
|
+
.where(scoped(tenantId, since));
|
|
552
|
+
if (!row) {
|
|
553
|
+
return {
|
|
554
|
+
requests: 0,
|
|
555
|
+
errors: 0,
|
|
556
|
+
inputTokens: 0,
|
|
557
|
+
outputTokens: 0,
|
|
558
|
+
cachedInputTokens: 0,
|
|
559
|
+
latencyAvgMs: null,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
requests: numeric(row.requests),
|
|
564
|
+
errors: numeric(row.errors),
|
|
565
|
+
inputTokens: numeric(row.inputTokens),
|
|
566
|
+
outputTokens: numeric(row.outputTokens),
|
|
567
|
+
cachedInputTokens: numeric(row.cachedInputTokens),
|
|
568
|
+
latencyAvgMs: numericNullable(row.latencyAvg),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Summed input+output tokens for a set of API keys over one window — the
|
|
574
|
+
* family bucket spend query.
|
|
575
|
+
*/
|
|
576
|
+
export async function sumTelemetryTokens(
|
|
577
|
+
db: CartethyiaDatabase,
|
|
578
|
+
tenantId: string,
|
|
579
|
+
filters: { readonly apiKeyIds?: readonly string[]; readonly since: Date; readonly until?: Date },
|
|
580
|
+
): Promise<number> {
|
|
581
|
+
const conditions = [scoped(tenantId, filters.since)];
|
|
582
|
+
if (filters.until !== undefined) {
|
|
583
|
+
conditions.push(sql`${telemetryEvents.createdAt} < ${filters.until}`);
|
|
584
|
+
}
|
|
585
|
+
if (filters.apiKeyIds !== undefined && filters.apiKeyIds.length > 0) {
|
|
586
|
+
conditions.push(inArray(telemetryEvents.apiKeyId, [...filters.apiKeyIds]));
|
|
587
|
+
}
|
|
588
|
+
const [row] = await db
|
|
589
|
+
.select({
|
|
590
|
+
total: sql<number>`coalesce(sum(coalesce(${telemetryEvents.inputTokens}, 0) + coalesce(${telemetryEvents.outputTokens}, 0)), 0)`,
|
|
591
|
+
})
|
|
592
|
+
.from(telemetryEvents)
|
|
593
|
+
.where(and(...conditions));
|
|
594
|
+
return numeric(row?.total ?? 0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The health card's latency and throughput figures for one window.
|
|
599
|
+
*
|
|
600
|
+
* Same principle as {@link summarizeTelemetry}: `avg(tokens_per_sec)` and the
|
|
601
|
+
* latency percentiles are computed by the database. The console's health card
|
|
602
|
+
* used to pull every retained event into an array to average `tokensPerSec`
|
|
603
|
+
* and sort latencies in JavaScript.
|
|
604
|
+
*/
|
|
605
|
+
export interface HealthPerformance {
|
|
606
|
+
readonly latencyAvgMs: number | null;
|
|
607
|
+
readonly latencyP50Ms: number | null;
|
|
608
|
+
readonly latencyP95Ms: number | null;
|
|
609
|
+
readonly latencyP99Ms: number | null;
|
|
610
|
+
readonly avgTokensPerSec: number;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export async function summarizeHealthPerformance(
|
|
614
|
+
db: CartethyiaDatabase,
|
|
615
|
+
tenantId: string,
|
|
616
|
+
since: Date,
|
|
617
|
+
): Promise<HealthPerformance> {
|
|
618
|
+
const [row] = await db
|
|
619
|
+
.select({
|
|
620
|
+
latencyAvg: sql<number>`avg(${telemetryEvents.latencyMs})`,
|
|
621
|
+
sampled: sql<number>`count(${telemetryEvents.latencyMs})`,
|
|
622
|
+
avgTokensPerSec: sql<number>`coalesce(avg(${telemetryEvents.tokensPerSec}), 0)`,
|
|
623
|
+
})
|
|
624
|
+
.from(telemetryEvents)
|
|
625
|
+
.where(scoped(tenantId, since));
|
|
626
|
+
const sampled = numeric(row?.sampled ?? 0);
|
|
627
|
+
const percentiles = await latencyPercentiles(db, tenantId, since, sampled);
|
|
628
|
+
return {
|
|
629
|
+
latencyAvgMs: numericNullable(row?.latencyAvg ?? null),
|
|
630
|
+
latencyP50Ms: percentiles.p50,
|
|
631
|
+
latencyP95Ms: percentiles.p95,
|
|
632
|
+
latencyP99Ms: percentiles.p99,
|
|
633
|
+
avgTokensPerSec: numeric(row?.avgTokensPerSec ?? 0),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* The Usage summary's performance block, computed in SQL.
|
|
639
|
+
*
|
|
640
|
+
* Replaces `summarizeUsagePerformance` (which sorted a latency array in
|
|
641
|
+
* JavaScript): `avg(resolve_ms)` and `avg(tokens_per_sec)` are SQL averages,
|
|
642
|
+
* and the latency percentiles come from the same database-side percentile
|
|
643
|
+
* path {@link summarizeTelemetry} uses.
|
|
644
|
+
*/
|
|
645
|
+
export interface UsagePerformanceTotals {
|
|
646
|
+
readonly avgResolveMs: number;
|
|
647
|
+
readonly avgDurationMs: number;
|
|
648
|
+
/**
|
|
649
|
+
* Percentiles as plain numbers: an empty window reports 0, which is what the
|
|
650
|
+
* JS-side `summarizeUsagePerformance` reported and what the summary card's
|
|
651
|
+
* contract expects. (The health reader keeps null so "no samples" stays
|
|
652
|
+
* distinguishable from a genuine near-zero latency.)
|
|
653
|
+
*/
|
|
654
|
+
readonly latencyP50Ms: number;
|
|
655
|
+
readonly latencyP95Ms: number;
|
|
656
|
+
readonly latencyP99Ms: number;
|
|
657
|
+
readonly latencySampleCount: number;
|
|
658
|
+
readonly resolveSampleCount: number;
|
|
659
|
+
readonly avgTokensPerSec: number;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function summarizeUsagePerformanceSql(
|
|
663
|
+
db: CartethyiaDatabase,
|
|
664
|
+
tenantId: string,
|
|
665
|
+
since: Date,
|
|
666
|
+
): Promise<UsagePerformanceTotals> {
|
|
667
|
+
const [row] = await db
|
|
668
|
+
.select({
|
|
669
|
+
sampled: sql<number>`count(${telemetryEvents.latencyMs})`,
|
|
670
|
+
latencyAvg: sql<number>`avg(${telemetryEvents.latencyMs})`,
|
|
671
|
+
resolveSum: sql<number>`coalesce(sum(${telemetryEvents.resolveMs}), 0)`,
|
|
672
|
+
resolveCount: sql<number>`count(${telemetryEvents.resolveMs})`,
|
|
673
|
+
avgTokensPerSec: sql<number>`coalesce(avg(${telemetryEvents.tokensPerSec}), 0)`,
|
|
674
|
+
})
|
|
675
|
+
.from(telemetryEvents)
|
|
676
|
+
.where(scoped(tenantId, since));
|
|
677
|
+
const sampled = numeric(row?.sampled ?? 0);
|
|
678
|
+
const resolveCount = numeric(row?.resolveCount ?? 0);
|
|
679
|
+
const percentiles = await latencyPercentiles(db, tenantId, since, sampled);
|
|
680
|
+
return {
|
|
681
|
+
avgResolveMs: resolveCount > 0 ? numeric(row?.resolveSum ?? 0) / resolveCount : 0,
|
|
682
|
+
avgDurationMs: numericNullable(row?.latencyAvg ?? null) ?? 0,
|
|
683
|
+
latencyP50Ms: percentiles.p50 ?? 0,
|
|
684
|
+
latencyP95Ms: percentiles.p95 ?? 0,
|
|
685
|
+
latencyP99Ms: percentiles.p99 ?? 0,
|
|
686
|
+
latencySampleCount: sampled,
|
|
687
|
+
resolveSampleCount: resolveCount,
|
|
688
|
+
avgTokensPerSec: numeric(row?.avgTokensPerSec ?? 0),
|
|
689
|
+
};
|
|
690
|
+
}
|