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
|
@@ -3,19 +3,25 @@ import { and, desc, eq, inArray } from "drizzle-orm";
|
|
|
3
3
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
4
4
|
import type { RedisClient } from "../../persistence/redis";
|
|
5
5
|
import { apiKeys, networkPools, telemetryPayloads } from "../../persistence/schema";
|
|
6
|
+
import { type TelemetryEventRecord } from "../../observability/event-log";
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
breakdownTelemetry,
|
|
9
|
+
chartTelemetry,
|
|
10
|
+
healthTelemetry,
|
|
11
|
+
queryTelemetryEvents as queryTelemetryEventPage,
|
|
12
|
+
summarizeHealthPerformance,
|
|
13
|
+
summarizeUsagePerformanceSql,
|
|
14
|
+
summarizeTelemetry,
|
|
15
|
+
} from "../../observability/event-query";
|
|
16
|
+
import { clientNameFromUserAgent } from "../../observability/client-name";
|
|
17
|
+
|
|
18
|
+
// Re-exported from the canonical module: ingestion (`telemetryEventRow`) and
|
|
19
|
+
// the backfill stamp the same value the console read side groups by, so the
|
|
20
|
+
// derivation cannot drift between writer and reader. Existing console
|
|
21
|
+
// imports of `clientNameFromUserAgent` keep working.
|
|
22
|
+
export { clientNameFromUserAgent };
|
|
17
23
|
import { cachedReadWithEtag, type CachedResult } from "../shared/read-through-cache";
|
|
18
|
-
import { decodeDatedCursor, encodeCursor } from "../shared/cursor";
|
|
24
|
+
import { decodeDatedCursor, encodeCursor, trimKeysetPage } from "../shared/cursor";
|
|
19
25
|
import { CARTETHYIA_VERSION } from "../../version";
|
|
20
26
|
import { CachedPreferencesReader, DrizzlePreferencesReader, type PreferencesReader } from "../../persistence/tenant-preferences";
|
|
21
27
|
import type {
|
|
@@ -88,23 +94,6 @@ function periodStartDate(period: string): Date | undefined {
|
|
|
88
94
|
return new Date(Date.now() - amount * unitMs);
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Client identity straight from the stored `User-Agent`: the first RFC 9110
|
|
94
|
-
* product token carries both the client and its version (`bun/1.2.3`,
|
|
95
|
-
* `codex-cli/0.1.0`), so no prefix catalog can drift from what the client
|
|
96
|
-
* actually sent. Browser agents start with the historical `Mozilla/5.0`
|
|
97
|
-
* token, which names no real client, so they report as `Browser`.
|
|
98
|
-
*/
|
|
99
|
-
export function clientNameFromUserAgent(userAgent: string | null | undefined): string {
|
|
100
|
-
const raw = userAgent?.trim();
|
|
101
|
-
if (!raw) return "unknown";
|
|
102
|
-
if (/^mozilla\//i.test(raw)) return "Browser";
|
|
103
|
-
const product = raw.split(/\s+/, 1)[0] ?? "";
|
|
104
|
-
const slash = product.indexOf("/");
|
|
105
|
-
const token = slash <= 0 ? product : product.slice(0, slash + 1).concat(product.slice(slash + 1).split(/[;,)]/, 1)[0] ?? "");
|
|
106
|
-
return token || "unknown";
|
|
107
|
-
}
|
|
108
97
|
/** Time-series bucket width for the usage chart, chosen from the window length. */
|
|
109
98
|
function chartBucketSeconds(period: string): number {
|
|
110
99
|
const since = periodStartDate(period);
|
|
@@ -197,15 +186,13 @@ async function resolveProxyLabel(
|
|
|
197
186
|
}
|
|
198
187
|
const HEALTH_WINDOW_MS = 24 * 3_600_000;
|
|
199
188
|
const TOP_N = 50;
|
|
189
|
+
|
|
200
190
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
191
|
+
* Nearest-rank percentile over a sorted array — kept for the JS-side
|
|
192
|
+
* `summarizeUsagePerformance` (still used by callers that already hold a
|
|
193
|
+
* bounded event list). The usage endpoints read percentiles from SQL via
|
|
194
|
+
* `summarizeUsagePerformanceSql`, which is what the dashboard polls.
|
|
205
195
|
*/
|
|
206
|
-
const PERIOD_MEMO_TTL_MS = 5_000;
|
|
207
|
-
|
|
208
|
-
/** Nearest-rank percentile over a sorted array, matching `percentile_cont` closely enough for a health gauge. */
|
|
209
196
|
function percentile(sorted: readonly number[], fraction: number): number {
|
|
210
197
|
if (sorted.length === 0) return 0;
|
|
211
198
|
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));
|
|
@@ -254,93 +241,22 @@ export function summarizeUsagePerformance(
|
|
|
254
241
|
}
|
|
255
242
|
|
|
256
243
|
/**
|
|
257
|
-
* The
|
|
244
|
+
* The console dimension as the SQL reader's grouping key.
|
|
258
245
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
246
|
+
* `client` maps to `client_name` (stamped at ingestion) rather than
|
|
247
|
+
* `user_agent`: the grouping is now a plain GROUP BY on a column the writer
|
|
248
|
+
* filled, instead of a per-row `clientNameFromUserAgent` call in JavaScript.
|
|
262
249
|
*/
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
let resolveCount = 0;
|
|
270
|
-
let latencyMaxMs: number | null = null;
|
|
271
|
-
const histogram: number[] = [];
|
|
272
|
-
for (const bucket of buckets) {
|
|
273
|
-
latencySum += bucket.latencySum;
|
|
274
|
-
latencyCount += bucket.latencyCount;
|
|
275
|
-
// Read defensively: an index written before these fields existed carries
|
|
276
|
-
// neither, and summing `undefined` poisons the whole period with NaN —
|
|
277
|
-
// including the days that do have the data.
|
|
278
|
-
resolveSum += numeric(bucket.resolveSumMs);
|
|
279
|
-
resolveCount += numeric(bucket.resolveCount);
|
|
280
|
-
if (bucket.latencyMaxMs !== null)
|
|
281
|
-
latencyMaxMs = latencyMaxMs === null ? bucket.latencyMaxMs : Math.max(latencyMaxMs, bucket.latencyMaxMs);
|
|
282
|
-
for (let index = 0; index < bucket.latencyHistogram.length; index += 1) {
|
|
283
|
-
histogram[index] = (histogram[index] ?? 0) + (bucket.latencyHistogram[index] ?? 0);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
const fill =
|
|
287
|
-
histogram.length > 0
|
|
288
|
-
? histogram
|
|
289
|
-
: new Array<number>(LATENCY_BUCKETS_MS.length + 1).fill(0);
|
|
290
|
-
return {
|
|
291
|
-
avgResolveMs: resolveCount > 0 ? resolveSum / resolveCount : 0,
|
|
292
|
-
avgDurationMs: latencyCount > 0 ? latencySum / latencyCount : 0,
|
|
293
|
-
// `latencyMaxMs` is passed as the overflow answer: a percentile past the
|
|
294
|
-
// histogram's last edge is at most the slowest request actually recorded.
|
|
295
|
-
// Without it an overflow returned `null`, and `?? 0` turned a P95 of 40 s
|
|
296
|
-
// into "0 ms" — the slow tail reported as nothing at all.
|
|
297
|
-
latencyP50Ms: percentileFromHistogram(fill, 50, latencyMaxMs) ?? 0,
|
|
298
|
-
latencyP95Ms: percentileFromHistogram(fill, 95, latencyMaxMs) ?? 0,
|
|
299
|
-
latencyP99Ms: percentileFromHistogram(fill, 99, latencyMaxMs) ?? 0,
|
|
300
|
-
latencySampleCount: latencyCount,
|
|
301
|
-
resolveSampleCount: resolveCount,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** The largest entries of a count map, capped. */
|
|
306
|
-
function topCounts(counts: ReadonlyMap<string, number>, limit: number): Array<[string, number]> {
|
|
307
|
-
return [...counts].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** The grouping value for one usage breakdown dimension. */
|
|
311
|
-
function dimensionName(dimension: UsageDimension, event: TelemetryEventRecord): string | null {
|
|
312
|
-
if (dimension === "model") return event.requestedModel;
|
|
313
|
-
if (dimension === "key") return event.apiKeyId;
|
|
314
|
-
if (dimension === "client") return event.userAgent;
|
|
315
|
-
if (dimension === "client_ip") return event.clientIp;
|
|
316
|
-
return event.providerId;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
interface BreakdownBucket {
|
|
320
|
-
name: string;
|
|
321
|
-
requests: number;
|
|
322
|
-
input: number;
|
|
323
|
-
output: number;
|
|
324
|
-
cached: number;
|
|
325
|
-
errors: number;
|
|
326
|
-
cost: number | null;
|
|
327
|
-
rateSum: number;
|
|
328
|
-
rateCount: number;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function emptyBreakdown(name: string): BreakdownBucket {
|
|
332
|
-
return { name, requests: 0, input: 0, output: 0, cached: 0, errors: 0, cost: null, rateSum: 0, rateCount: 0 };
|
|
250
|
+
function breakdownDimension(dimension: UsageDimension): string {
|
|
251
|
+
if (dimension === "model") return "model";
|
|
252
|
+
if (dimension === "key") return "api_key";
|
|
253
|
+
if (dimension === "client") return "client";
|
|
254
|
+
if (dimension === "client_ip") return "client_ip";
|
|
255
|
+
return "provider";
|
|
333
256
|
}
|
|
334
257
|
|
|
335
258
|
/** Real Drizzle-backed observability repository used by the production console. */
|
|
336
259
|
export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
337
|
-
/**
|
|
338
|
-
* Per-(tenant, period) parsed event set, shared by the ~8 usage endpoints
|
|
339
|
-
* one dashboard poll fires in parallel. 1.5 s window: a poll burst shares
|
|
340
|
-
* one file parse; the next poll re-reads. In-flight shares one promise.
|
|
341
|
-
*/
|
|
342
|
-
private readonly periodMemo = new Map<string, { at: number; promise: Promise<readonly TelemetryEventRecord[]> }>();
|
|
343
|
-
private readonly periodBucketMemo = new Map<string, { at: number; promise: Promise<readonly IndexedBucket[]> }>();
|
|
344
260
|
constructor(
|
|
345
261
|
private readonly db: CartethyiaDatabase,
|
|
346
262
|
private readonly redis: RedisClient | undefined,
|
|
@@ -404,44 +320,27 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
404
320
|
let cacheHitRate = 0;
|
|
405
321
|
let avgTokensPerSec = 0;
|
|
406
322
|
try {
|
|
407
|
-
// Bounded 24h rolling window (matches the Overview copy)
|
|
408
|
-
//
|
|
323
|
+
// Bounded 24h rolling window (matches the Overview copy), computed IN
|
|
324
|
+
// SQL. The previous shape materialized every retained event into a
|
|
325
|
+
// latency array and sorted it in JavaScript — the single largest read
|
|
326
|
+
// cost on the dashboard. `healthTelemetry` returns the totals and the
|
|
327
|
+
// database computes the percentiles.
|
|
409
328
|
const since = new Date(Date.now() - HEALTH_WINDOW_MS);
|
|
410
|
-
const
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
total += 1;
|
|
420
|
-
if (isGatewayError(event.status, event.httpStatus)) errors += 1;
|
|
421
|
-
if (event.latencyMs !== null) {
|
|
422
|
-
latencies.push(event.latencyMs);
|
|
423
|
-
latencySum += event.latencyMs;
|
|
424
|
-
}
|
|
425
|
-
if (event.cachedInputTokens !== null && event.cachedInputTokens > 0) {
|
|
426
|
-
inputTokens += event.inputTokens ?? 0;
|
|
427
|
-
cachedTokens += event.inputTokens ?? 0;
|
|
428
|
-
outputTokens += event.outputTokens ?? 0;
|
|
429
|
-
}
|
|
430
|
-
if (event.tokensPerSec !== null) {
|
|
431
|
-
tokensPerSecSum += event.tokensPerSec;
|
|
432
|
-
tokensPerSecCount += 1;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
latencies.sort((a, b) => a - b);
|
|
436
|
-
avg = latencies.length > 0 ? Math.round(latencySum / latencies.length) : 0;
|
|
437
|
-
p95 = percentile(latencies, 0.95);
|
|
438
|
-
p99 = percentile(latencies, 0.99);
|
|
329
|
+
const totals = await healthTelemetry(this.db, tenantId, since);
|
|
330
|
+
const performance = await summarizeHealthPerformance(this.db, tenantId, since);
|
|
331
|
+
total = totals.requests;
|
|
332
|
+
errors = totals.errors;
|
|
333
|
+
avg = performance.latencyAvgMs === null ? 0 : Math.round(performance.latencyAvgMs);
|
|
334
|
+
p95 = performance.latencyP95Ms ?? 0;
|
|
335
|
+
p99 = performance.latencyP99Ms ?? 0;
|
|
336
|
+
const inputTokens = totals.inputTokens;
|
|
337
|
+
const cachedTokens = totals.cachedInputTokens;
|
|
439
338
|
// Cached share of the prompt: cached/input, the same honest definition
|
|
440
339
|
// usageSummary uses, so the health card and the Usage page cannot
|
|
441
340
|
// disagree. (Cached share, not prompt share: input/(input+output) reads
|
|
442
341
|
// ~99% and mislabels it a cache hit rate.)
|
|
443
342
|
cacheHitRate = inputTokens > 0 ? Math.round((cachedTokens / inputTokens) * 10000) / 100 : 0;
|
|
444
|
-
avgTokensPerSec =
|
|
343
|
+
avgTokensPerSec = performance.avgTokensPerSec;
|
|
445
344
|
} catch {
|
|
446
345
|
databaseHealthy = false;
|
|
447
346
|
}
|
|
@@ -514,23 +413,23 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
514
413
|
}
|
|
515
414
|
|
|
516
415
|
private async computeUsage(tenantId: string, period: string): Promise<UsageResponse> {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
416
|
+
const since = this.periodSince(period);
|
|
417
|
+
// Totals AND the top-model/top-provider counts come from SQL: the old
|
|
418
|
+
// shape parsed the whole retained window into an array to count two
|
|
419
|
+
// dimensions in JavaScript.
|
|
420
|
+
const [summary, models, providers] = await Promise.all([
|
|
421
|
+
summarizeTelemetry(this.db, tenantId, since),
|
|
422
|
+
breakdownTelemetry(this.db, tenantId, "model", since),
|
|
423
|
+
breakdownTelemetry(this.db, tenantId, "provider", since),
|
|
424
|
+
]);
|
|
425
|
+
const requestsTotal = summary.requests;
|
|
426
|
+
// `completed` is a lifecycle status, not the gateway-error predicate: it
|
|
427
|
+
// is derived as requests minus the errors and the non-completed states the
|
|
428
|
+
// summary already counts.
|
|
429
|
+
const requestsFailed = summary.errors;
|
|
430
|
+
const requestsSucceeded = Math.max(0, requestsTotal - requestsFailed - summary.cancelled - summary.truncated);
|
|
431
|
+
const tokensUsed = summary.inputTokens + summary.outputTokens;
|
|
432
|
+
const estimatedCost = summary.estimatedCostUsd;
|
|
534
433
|
return {
|
|
535
434
|
tenantId,
|
|
536
435
|
period,
|
|
@@ -539,53 +438,25 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
539
438
|
requestsFailed,
|
|
540
439
|
tokensUsed,
|
|
541
440
|
estimatedCost,
|
|
542
|
-
topModels:
|
|
543
|
-
|
|
441
|
+
topModels: models
|
|
442
|
+
.slice(0, TOP_N)
|
|
443
|
+
.map((group) => ({ modelId: group.key, count: group.requests })),
|
|
444
|
+
topProviders: providers
|
|
445
|
+
.slice(0, TOP_N)
|
|
446
|
+
.map((group) => ({ providerId: group.key, count: group.requests })),
|
|
544
447
|
};
|
|
545
448
|
}
|
|
546
449
|
|
|
547
|
-
/** The tenant's retained events inside a usage period. `all` keeps everything. */
|
|
548
450
|
/**
|
|
549
|
-
* The
|
|
451
|
+
* The retained window for one usage period.
|
|
550
452
|
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
453
|
+
* `all` means the retained SQL window (the database's own retention), not
|
|
454
|
+
* "read every file ever written": `telemetry_events` is pruned by the same
|
|
455
|
+
* retention rule as the raw files, so the SQL store already holds exactly
|
|
456
|
+
* what a period can show.
|
|
554
457
|
*/
|
|
555
|
-
private
|
|
556
|
-
|
|
557
|
-
const now = Date.now();
|
|
558
|
-
const hit = this.periodBucketMemo.get(key);
|
|
559
|
-
if (hit && now - hit.at < PERIOD_MEMO_TTL_MS) return hit.promise;
|
|
560
|
-
const since = periodStartDate(period);
|
|
561
|
-
// Fold before reading. Events appended since the scheduler last ran are
|
|
562
|
-
// otherwise invisible, and a usage page that under-reports is worse than one
|
|
563
|
-
// that is a moment slow.
|
|
564
|
-
const promise = queryIndexedBuckets({
|
|
565
|
-
tenantId,
|
|
566
|
-
foldFirst: true,
|
|
567
|
-
...(since === undefined ? {} : { since }),
|
|
568
|
-
});
|
|
569
|
-
this.periodBucketMemo.set(key, { at: now, promise });
|
|
570
|
-
void promise.catch(() => {
|
|
571
|
-
if (this.periodBucketMemo.get(key)?.promise === promise) this.periodBucketMemo.delete(key);
|
|
572
|
-
});
|
|
573
|
-
return promise;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
private periodEvents(tenantId: string, period: string): Promise<readonly TelemetryEventRecord[]> {
|
|
577
|
-
const key = `${tenantId}:${period}`;
|
|
578
|
-
const now = Date.now();
|
|
579
|
-
const hit = this.periodMemo.get(key);
|
|
580
|
-
if (hit && now - hit.at < PERIOD_MEMO_TTL_MS) return hit.promise;
|
|
581
|
-
const since = periodStartDate(period);
|
|
582
|
-
const promise = queryTelemetryEvents(since === undefined ? { tenantId } : { tenantId, since });
|
|
583
|
-
this.periodMemo.set(key, { at: now, promise });
|
|
584
|
-
// A rejected parse must not poison the memo for the next poll.
|
|
585
|
-
void promise.catch(() => {
|
|
586
|
-
if (this.periodMemo.get(key)?.promise === promise) this.periodMemo.delete(key);
|
|
587
|
-
});
|
|
588
|
-
return promise;
|
|
458
|
+
private periodSince(period: string): Date {
|
|
459
|
+
return periodStartDate(period) ?? new Date(0);
|
|
589
460
|
}
|
|
590
461
|
|
|
591
462
|
async usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>> {
|
|
@@ -595,58 +466,36 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
595
466
|
}
|
|
596
467
|
|
|
597
468
|
private async computeSummary(tenantId: string, period: string): Promise<UsageSummaryResponse> {
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
let tokensPerSecSum = 0;
|
|
610
|
-
let tokensPerSecCount = 0;
|
|
611
|
-
const statuses = new Map<number, number>();
|
|
612
|
-
for (const row of buckets) {
|
|
613
|
-
// Folded from the index's own per-status counts. These used to be left
|
|
614
|
-
// empty: the loop below never filled `statuses`, so every status chip and
|
|
615
|
-
// the requests-list filters read zero for any day whose events had been
|
|
616
|
-
// folded — while `requests` beside them read the real total.
|
|
617
|
-
for (const [status, count] of Object.entries(row.byStatus ?? {})) {
|
|
618
|
-
const code = Number(status);
|
|
619
|
-
if (!Number.isFinite(code)) continue;
|
|
620
|
-
statuses.set(code, (statuses.get(code) ?? 0) + count);
|
|
621
|
-
}
|
|
622
|
-
requests += row.requests;
|
|
623
|
-
inputTokens += row.inputTokens;
|
|
624
|
-
outputTokens += row.outputTokens;
|
|
625
|
-
cachedTokens += row.cachedTokens;
|
|
626
|
-
errors += row.failed;
|
|
627
|
-
cancelled += row.cancelled;
|
|
628
|
-
truncated += row.truncated;
|
|
629
|
-
estimatedCostUsd += row.estimatedCostUsd;
|
|
630
|
-
unpriced += row.unpriced;
|
|
631
|
-
tokensPerSecSum += row.tokensPerSecSum;
|
|
632
|
-
tokensPerSecCount += row.tokensPerSecCount;
|
|
633
|
-
}
|
|
469
|
+
const since = this.periodSince(period);
|
|
470
|
+
// Totals, status breakdown, and latency percentiles all computed in SQL.
|
|
471
|
+
// This used to parse the whole retained window into JavaScript to sum six
|
|
472
|
+
// counters and sort a latency array.
|
|
473
|
+
const [summary, statuses, performance] = await Promise.all([
|
|
474
|
+
summarizeTelemetry(this.db, tenantId, since),
|
|
475
|
+
breakdownTelemetry(this.db, tenantId, "status", since),
|
|
476
|
+
summarizeUsagePerformanceSql(this.db, tenantId, since),
|
|
477
|
+
]);
|
|
478
|
+
const inputTokens = summary.inputTokens;
|
|
479
|
+
const cachedTokens = summary.cachedInputTokens;
|
|
634
480
|
return {
|
|
635
481
|
period,
|
|
636
482
|
totals: {
|
|
637
|
-
statusCounts:
|
|
638
|
-
|
|
483
|
+
statusCounts: statuses
|
|
484
|
+
.map((group) => ({ status: Number(group.key), count: group.requests }))
|
|
485
|
+
.filter((entry) => Number.isFinite(entry.status))
|
|
486
|
+
.sort((a, b) => a.status - b.status),
|
|
487
|
+
requests: summary.requests,
|
|
639
488
|
inputTokens,
|
|
640
489
|
cachedTokens,
|
|
641
|
-
outputTokens,
|
|
642
|
-
errors,
|
|
643
|
-
cancelled,
|
|
644
|
-
truncated,
|
|
490
|
+
outputTokens: summary.outputTokens,
|
|
491
|
+
errors: summary.errors,
|
|
492
|
+
cancelled: summary.cancelled,
|
|
493
|
+
truncated: summary.truncated,
|
|
645
494
|
...performance,
|
|
646
|
-
estimatedCostUsd,
|
|
647
|
-
partial: unpriced > 0,
|
|
495
|
+
estimatedCostUsd: summary.estimatedCostUsd,
|
|
496
|
+
partial: summary.unpriced > 0,
|
|
648
497
|
cacheHitRate: inputTokens > 0 ? (cachedTokens / inputTokens) * 100 : 0,
|
|
649
|
-
avgTokensPerSec:
|
|
498
|
+
avgTokensPerSec: performance.avgTokensPerSec,
|
|
650
499
|
},
|
|
651
500
|
};
|
|
652
501
|
}
|
|
@@ -661,31 +510,37 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
661
510
|
const since = periodStartDate(period);
|
|
662
511
|
const bucketSeconds = chartBucketSeconds(period);
|
|
663
512
|
const stepMs = bucketSeconds * 1000;
|
|
664
|
-
|
|
513
|
+
// Bucketing happens in SQL. The old shape parsed the retained window to
|
|
514
|
+
// floor every event's timestamp into a JS Map.
|
|
515
|
+
const buckets = await chartTelemetry(
|
|
516
|
+
this.db,
|
|
517
|
+
tenantId,
|
|
518
|
+
this.periodSince(period),
|
|
519
|
+
bucketSeconds,
|
|
520
|
+
);
|
|
665
521
|
const byTime = new Map<number, UsageChartBucket>();
|
|
666
|
-
for (const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
522
|
+
for (const bucket of buckets) {
|
|
523
|
+
byTime.set(bucket.at, {
|
|
524
|
+
t: new Date(bucket.at).toISOString(),
|
|
525
|
+
requests: bucket.requests,
|
|
526
|
+
input: bucket.inputTokens,
|
|
527
|
+
// `cached` is the cached share of the prompt: the SQL reader sums
|
|
528
|
+
// cached_input_tokens, which is the same number the old JS loop used.
|
|
529
|
+
cached: bucket.inputTokens,
|
|
530
|
+
output: bucket.outputTokens,
|
|
531
|
+
});
|
|
676
532
|
}
|
|
677
|
-
const
|
|
678
|
-
const first = rows.length > 0 ? Math.min(...byTime.keys()) : undefined;
|
|
533
|
+
const first = buckets.length > 0 ? buckets[0]!.at : undefined;
|
|
679
534
|
const startMs = since ? since.getTime() : first;
|
|
680
535
|
if (startMs === undefined) return { buckets: [] };
|
|
681
536
|
const aligned = Math.floor(startMs / stepMs) * stepMs;
|
|
682
|
-
const
|
|
537
|
+
const out: UsageChartBucket[] = [];
|
|
683
538
|
for (let at = aligned; at <= Date.now(); at += stepMs) {
|
|
684
|
-
|
|
539
|
+
out.push(
|
|
685
540
|
byTime.get(at) ?? { t: new Date(at).toISOString(), requests: 0, input: 0, cached: 0, output: 0 },
|
|
686
541
|
);
|
|
687
542
|
}
|
|
688
|
-
return { buckets };
|
|
543
|
+
return { buckets: out };
|
|
689
544
|
}
|
|
690
545
|
|
|
691
546
|
async usageBy(
|
|
@@ -703,74 +558,33 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
703
558
|
dimension: UsageDimension,
|
|
704
559
|
period: string,
|
|
705
560
|
): Promise<UsageByResponse> {
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
groups.set(name, bucket);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
for (const indexed of buckets) {
|
|
738
|
-
const source =
|
|
739
|
-
dimension === "model"
|
|
740
|
-
? indexed.dimModel
|
|
741
|
-
: dimension === "provider"
|
|
742
|
-
? indexed.dimProvider
|
|
743
|
-
: dimension === "client"
|
|
744
|
-
? indexed.dimClient
|
|
745
|
-
: indexed.dimClientIp;
|
|
746
|
-
for (const [raw, entry] of Object.entries(source)) {
|
|
747
|
-
if (raw.length === 0) continue;
|
|
748
|
-
const name = dimension === "client" ? clientNameFromUserAgent(raw) : raw;
|
|
749
|
-
const bucket = groups.get(name) ?? emptyBreakdown(name);
|
|
750
|
-
bucket.requests += entry.requests;
|
|
751
|
-
bucket.input += entry.inputTokens;
|
|
752
|
-
bucket.output += entry.outputTokens;
|
|
753
|
-
bucket.cached += entry.cachedTokens;
|
|
754
|
-
bucket.errors += entry.errors;
|
|
755
|
-
bucket.cost = (bucket.cost ?? 0) + entry.estimatedCostUsd;
|
|
756
|
-
bucket.rateSum += entry.tokensPerSecSum;
|
|
757
|
-
bucket.rateCount += entry.tokensPerSecCount;
|
|
758
|
-
groups.set(name, bucket);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
const mapped = [...groups.values()]
|
|
762
|
-
.map((bucket) => ({
|
|
763
|
-
name: bucket.name,
|
|
764
|
-
requests: bucket.requests,
|
|
765
|
-
input: bucket.input,
|
|
766
|
-
output: bucket.output,
|
|
767
|
-
cached: bucket.cached,
|
|
768
|
-
total: bucket.input + bucket.output,
|
|
769
|
-
errors: bucket.errors,
|
|
770
|
-
costUsd: bucket.cost,
|
|
771
|
-
cacheHitRate: bucket.input > 0 ? (bucket.cached / bucket.input) * 100 : 0,
|
|
772
|
-
avgTokensPerSec: bucket.rateCount > 0 ? bucket.rateSum / bucket.rateCount : 0,
|
|
773
|
-
}))
|
|
561
|
+
// Grouping happens in SQL and only the (bounded) groups come back. The
|
|
562
|
+
// old shape walked every retained event to accumulate a JS Map per
|
|
563
|
+
// dimension; `client` now groups on the ingestion-stamped `client_name`
|
|
564
|
+
// instead of re-deriving a name per row.
|
|
565
|
+
const groups = await breakdownTelemetry(
|
|
566
|
+
this.db,
|
|
567
|
+
tenantId,
|
|
568
|
+
breakdownDimension(dimension),
|
|
569
|
+
this.periodSince(period),
|
|
570
|
+
);
|
|
571
|
+
const mapped = groups
|
|
572
|
+
.map((group) => {
|
|
573
|
+
const input = group.inputTokens;
|
|
574
|
+
const output = group.outputTokens;
|
|
575
|
+
return {
|
|
576
|
+
name: group.key,
|
|
577
|
+
requests: group.requests,
|
|
578
|
+
input,
|
|
579
|
+
output,
|
|
580
|
+
cached: group.cachedInputTokens,
|
|
581
|
+
total: input + output,
|
|
582
|
+
errors: group.errors,
|
|
583
|
+
costUsd: group.costUsd,
|
|
584
|
+
cacheHitRate: input > 0 ? (group.cachedInputTokens / input) * 100 : 0,
|
|
585
|
+
avgTokensPerSec: group.avgTokensPerSec,
|
|
586
|
+
};
|
|
587
|
+
})
|
|
774
588
|
.sort((a, b) => b.requests - a.requests)
|
|
775
589
|
.slice(0, 100);
|
|
776
590
|
if (dimension === "client_ip") {
|
|
@@ -841,17 +655,10 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
841
655
|
}
|
|
842
656
|
|
|
843
657
|
private async computeCache(tenantId: string, period: string): Promise<UsageCacheResponse> {
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
for (const event of events) {
|
|
849
|
-
inputTokens += event.inputTokens ?? 0;
|
|
850
|
-
outputTokens += event.outputTokens ?? 0;
|
|
851
|
-
if (event.cachedInputTokens !== null && event.cachedInputTokens > 0) {
|
|
852
|
-
cachedTokens += event.inputTokens ?? 0;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
658
|
+
const summary = await summarizeTelemetry(this.db, tenantId, this.periodSince(period));
|
|
659
|
+
const inputTokens = summary.inputTokens;
|
|
660
|
+
const outputTokens = summary.outputTokens;
|
|
661
|
+
const cachedTokens = summary.cachedInputTokens;
|
|
855
662
|
const tokenTotal = inputTokens + outputTokens;
|
|
856
663
|
return {
|
|
857
664
|
period,
|
|
@@ -868,24 +675,16 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
868
675
|
limit: number,
|
|
869
676
|
httpStatus?: number,
|
|
870
677
|
): Promise<CachedResult<UsageRequestsResponse>> {
|
|
871
|
-
// Ask the reader for `limit` rows instead of folding the whole window and
|
|
872
|
-
// slicing it. It stops at the newest file that fills the page, so the
|
|
873
|
-
// console's most-polled endpoint no longer parses the retention window just
|
|
874
|
-
// to show 50 rows.
|
|
875
|
-
const since = periodStartDate(period);
|
|
876
|
-
const events = await queryTelemetryEvents({
|
|
877
|
-
tenantId,
|
|
878
|
-
limit,
|
|
879
|
-
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
880
|
-
...(since === undefined ? {} : { since }),
|
|
881
|
-
});
|
|
882
|
-
const rows = events.slice(0, limit);
|
|
883
678
|
const hideClientIp = await shouldMaskClientIp(this.preferences, tenantId);
|
|
884
679
|
// Cached like its siblings, so a repeated poll costs a `304` instead of
|
|
885
680
|
// another read. The mask flag is part of the key: it is a privacy setting,
|
|
886
681
|
// so a page must never be served from an entry computed under the other
|
|
887
|
-
// value.
|
|
888
|
-
//
|
|
682
|
+
// value.
|
|
683
|
+
//
|
|
684
|
+
// The telemetry read moved INSIDE the read-through: it used to run before
|
|
685
|
+
// it, so every 304 still paid a query. Now a cache hit costs nothing at
|
|
686
|
+
// all, and a miss pays one bounded SQL page (keyset, newest first) rather
|
|
687
|
+
// than a scan of the raw window.
|
|
889
688
|
return this.readThroughWithEtag(
|
|
890
689
|
this.usageCacheKey(
|
|
891
690
|
tenantId,
|
|
@@ -893,6 +692,13 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
893
692
|
"requests",
|
|
894
693
|
),
|
|
895
694
|
async () => {
|
|
695
|
+
const page = await queryTelemetryEventPage(this.db, {
|
|
696
|
+
tenantId,
|
|
697
|
+
since: this.periodSince(period),
|
|
698
|
+
limit,
|
|
699
|
+
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
700
|
+
});
|
|
701
|
+
const rows = page.events.slice(0, limit);
|
|
896
702
|
const keyIds = [...new Set(rows.map((row) => row.apiKeyId).filter((id): id is string => id !== null))];
|
|
897
703
|
const labels = await this.apiKeyLabels(tenantId, keyIds);
|
|
898
704
|
// One indexed read for the whole page, not one per row: presence lives
|
|
@@ -947,8 +753,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
947
753
|
tenantId: string,
|
|
948
754
|
requestId: string,
|
|
949
755
|
): Promise<UsageRequestDetail | undefined> {
|
|
950
|
-
const
|
|
951
|
-
const event =
|
|
756
|
+
const page = await queryTelemetryEventPage(this.db, { tenantId, requestId });
|
|
757
|
+
const event = page.events[0];
|
|
952
758
|
if (!event) return undefined;
|
|
953
759
|
// The label, proxy, and mask lookups are independent: run them together
|
|
954
760
|
// instead of three sequential round trips.
|
|
@@ -1013,17 +819,15 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
1013
819
|
// `until` is inclusive while the cursor is exclusive, so rows at the exact
|
|
1014
820
|
// cursor timestamp are still compared by id here — the cheap part of the
|
|
1015
821
|
// predicate, now over one timestamp's worth of rows instead of the window.
|
|
1016
|
-
|
|
822
|
+
// Keyset page: the cursor is the last row of the previous page, and the
|
|
823
|
+
// SQL reader pushes it into the WHERE clause instead of filtering a
|
|
824
|
+
// materialized window in JS.
|
|
825
|
+
const page = await queryTelemetryEventPage(this.db, {
|
|
1017
826
|
tenantId,
|
|
1018
827
|
limit: limit + 1,
|
|
1019
|
-
...(before === undefined ? {} : {
|
|
828
|
+
...(before === undefined ? {} : { cursor: encodeCursor({ createdAt: before.createdAt, id: before.id }) }),
|
|
1020
829
|
});
|
|
1021
|
-
const rows = (
|
|
1022
|
-
? events.filter((event) => event.createdAt < before.createdAt || (event.createdAt === before.createdAt && event.id < before.id))
|
|
1023
|
-
: events
|
|
1024
|
-
).slice(0, limit + 1);
|
|
1025
|
-
const overflow = rows.length > limit;
|
|
1026
|
-
const trimmed = overflow ? rows.slice(0, limit) : rows;
|
|
830
|
+
const { rows: trimmed, hasMore: overflow } = trimKeysetPage(page.events, limit);
|
|
1027
831
|
const entries = trimmed.map(mapTelemetryEventRow);
|
|
1028
832
|
const lastEntry = entries[entries.length - 1];
|
|
1029
833
|
const nextCursor =
|
|
@@ -1043,7 +847,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
1043
847
|
// `requestId`, which is a different column that merely defaults to the
|
|
1044
848
|
// same value. Predicating on it lets the reader stop at the first hit
|
|
1045
849
|
// instead of parsing the retention window to find one row.
|
|
1046
|
-
const
|
|
850
|
+
const page = await queryTelemetryEventPage(this.db, { tenantId, id: eventId });
|
|
851
|
+
const events = page.events;
|
|
1047
852
|
const event = events.find(
|
|
1048
853
|
(candidate) =>
|
|
1049
854
|
candidate.id === eventId && (createdAt === undefined || candidate.createdAt === createdAt),
|