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
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
import { mkdir, open, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
accumulate,
|
|
5
|
-
emptyBucket,
|
|
6
|
-
mergeBucketsInternal,
|
|
7
|
-
type CompactedBucket,
|
|
8
|
-
} from "./event-compaction";
|
|
9
|
-
import { TELEMETRY_EVENT_DIR, type TelemetryEventRecord } from "./event-log";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* One sidecar index per day, beside that day's append-only event file.
|
|
13
|
-
*
|
|
14
|
-
* The event file grows all day and is never rewritten while it is open. The
|
|
15
|
-
* index is the thing the dashboard reads: it holds the totals and the breakdown
|
|
16
|
-
* counts, maintained incrementally from a byte watermark rather than rebuilt by
|
|
17
|
-
* folding and deleting source files.
|
|
18
|
-
*
|
|
19
|
-
* The watermark is what makes that possible. Deciding which file was still
|
|
20
|
-
* being written by comparing file *names* lets the open file be folded and
|
|
21
|
-
* unlinked mid-write, so its requests never reach the summary. A byte offset
|
|
22
|
-
* has no such ambiguity: everything below it is counted, everything above it is
|
|
23
|
-
* not, and re-reading the tail is idempotent.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* An event whose tenant is known to be present.
|
|
28
|
-
*
|
|
29
|
-
* `tenantId` is optional on the record because a row can arrive without one,
|
|
30
|
-
* but the index is keyed by tenant, so only rows carrying one are folded.
|
|
31
|
-
*/
|
|
32
|
-
type FoldedEvent = TelemetryEventRecord & { readonly tenantId: string };
|
|
33
|
-
|
|
34
|
-
/** Marker so a reader can tell an index from anything else in the directory. */
|
|
35
|
-
const INDEX_KIND = "telemetry-day-index" as const;
|
|
36
|
-
|
|
37
|
-
/** Version bumped whenever the shape changes; an unreadable version is ignored. */
|
|
38
|
-
const INDEX_VERSION = 1 as const;
|
|
39
|
-
|
|
40
|
-
/** The event file for one UTC day. */
|
|
41
|
-
export function dayEventFileName(day: string): string {
|
|
42
|
-
return `${day}.events.jsonl`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** The sidecar index for one UTC day. */
|
|
46
|
-
export function dayIndexFileName(day: string): string {
|
|
47
|
-
return `${day}.index.json`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** The UTC day (`2026-10-22`) a moment belongs to. */
|
|
51
|
-
export function utcDay(at: Date): string {
|
|
52
|
-
return at.toISOString().slice(0, 10);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* One hour's share of a bucket.
|
|
57
|
-
*
|
|
58
|
-
* The Traffic chart asks how a day was spent, which the totals alone cannot
|
|
59
|
-
* answer. Twenty-four slots per bucket is small — four numbers each — so it is
|
|
60
|
-
* held *inside* the index rather than as separate per-hour files.
|
|
61
|
-
*/
|
|
62
|
-
/**
|
|
63
|
-
* One named dimension's share of a bucket.
|
|
64
|
-
*
|
|
65
|
-
* The breakdown asks for tokens, errors and cost per model/provider/client/IP,
|
|
66
|
-
* not merely how many requests each took. Counts alone would leave every
|
|
67
|
-
* breakdown column reading zero, so each named entry carries the same running
|
|
68
|
-
* sums the bucket does.
|
|
69
|
-
*/
|
|
70
|
-
export interface IndexedDimension {
|
|
71
|
-
readonly requests: number;
|
|
72
|
-
readonly inputTokens: number;
|
|
73
|
-
readonly cachedTokens: number;
|
|
74
|
-
readonly outputTokens: number;
|
|
75
|
-
readonly errors: number;
|
|
76
|
-
readonly estimatedCostUsd: number;
|
|
77
|
-
readonly tokensPerSecSum: number;
|
|
78
|
-
readonly tokensPerSecCount: number;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface IndexedHour {
|
|
82
|
-
readonly requests: number;
|
|
83
|
-
readonly inputTokens: number;
|
|
84
|
-
readonly cachedTokens: number;
|
|
85
|
-
readonly outputTokens: number;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** One tenant/key pair's running totals for the day. */
|
|
89
|
-
export interface IndexedBucket extends CompactedBucket {
|
|
90
|
-
readonly tenantId: string;
|
|
91
|
-
readonly apiKeyId: string | null;
|
|
92
|
-
/** Index 0-23, the UTC hour an event fell in. */
|
|
93
|
-
readonly hours: readonly IndexedHour[];
|
|
94
|
-
/** Per-name sums for each breakdown dimension. */
|
|
95
|
-
readonly dimModel: Readonly<Record<string, IndexedDimension>>;
|
|
96
|
-
readonly dimProvider: Readonly<Record<string, IndexedDimension>>;
|
|
97
|
-
readonly dimClient: Readonly<Record<string, IndexedDimension>>;
|
|
98
|
-
readonly dimClientIp: Readonly<Record<string, IndexedDimension>>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** The on-disk shape of a day index. */
|
|
102
|
-
export interface DayIndex {
|
|
103
|
-
readonly kind: typeof INDEX_KIND;
|
|
104
|
-
readonly version: typeof INDEX_VERSION;
|
|
105
|
-
readonly day: string;
|
|
106
|
-
/**
|
|
107
|
-
* Bytes of the event file already folded in.
|
|
108
|
-
*
|
|
109
|
-
* Everything past this offset is uncounted; a crash between append and index
|
|
110
|
-
* write only ever leaves it behind, never ahead, so the tail is re-read.
|
|
111
|
-
*/
|
|
112
|
-
readonly watermarkBytes: number;
|
|
113
|
-
/** Totals per tenant/key. */
|
|
114
|
-
readonly buckets: readonly IndexedBucket[];
|
|
115
|
-
/** Wall-clock of the last fold, for operators reading the file by hand. */
|
|
116
|
-
readonly updatedAt: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function bucketKey(tenantId: string, apiKeyId: string | null): string {
|
|
120
|
-
return `${tenantId}|${apiKeyId ?? ""}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Twenty-four empty hour slots. */
|
|
124
|
-
function emptyHours(): IndexedHour[] {
|
|
125
|
-
return Array.from({ length: 24 }, () => ({
|
|
126
|
-
requests: 0,
|
|
127
|
-
inputTokens: 0,
|
|
128
|
-
cachedTokens: 0,
|
|
129
|
-
outputTokens: 0,
|
|
130
|
-
}));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** The UTC hour (0-23) of an ISO timestamp. */
|
|
134
|
-
function hourOf(createdAt: string): number {
|
|
135
|
-
const at = Date.parse(createdAt);
|
|
136
|
-
if (!Number.isFinite(at)) return 0;
|
|
137
|
-
return Number.parseInt(createdAt.slice(11, 13), 10) || 0;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** An empty per-name entry. */
|
|
141
|
-
function emptyDimension(): IndexedDimension {
|
|
142
|
-
return {
|
|
143
|
-
requests: 0,
|
|
144
|
-
inputTokens: 0,
|
|
145
|
-
cachedTokens: 0,
|
|
146
|
-
outputTokens: 0,
|
|
147
|
-
errors: 0,
|
|
148
|
-
estimatedCostUsd: 0,
|
|
149
|
-
tokensPerSecSum: 0,
|
|
150
|
-
tokensPerSecCount: 0,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* A cost as a number.
|
|
156
|
-
*
|
|
157
|
-
* Costs arrive as strings from `numeric` columns and from restored rows, and
|
|
158
|
-
* `0 + "0.000966"` concatenates rather than sums. Coerced once, here, so the
|
|
159
|
-
* per-name breakdown cannot accumulate a string the way it did.
|
|
160
|
-
*/
|
|
161
|
-
function numericCost(value: unknown): number {
|
|
162
|
-
const asNumber = typeof value === "number" ? value : Number(value);
|
|
163
|
-
return Number.isFinite(asNumber) ? asNumber : 0;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Folds one event into one named dimension entry. */
|
|
167
|
-
function foldDimension(
|
|
168
|
-
into: Readonly<Record<string, IndexedDimension>> | undefined,
|
|
169
|
-
name: string | null | undefined,
|
|
170
|
-
event: FoldedEvent,
|
|
171
|
-
): Record<string, IndexedDimension> {
|
|
172
|
-
const out: Record<string, IndexedDimension> = { ...(into ?? {}) };
|
|
173
|
-
const key = typeof name === "string" ? name.trim() : "";
|
|
174
|
-
if (key === "") return out;
|
|
175
|
-
const prior = out[key] ?? emptyDimension();
|
|
176
|
-
const failed = event.status === "failed" ? 1 : 0;
|
|
177
|
-
out[key] = {
|
|
178
|
-
requests: prior.requests + 1,
|
|
179
|
-
inputTokens: prior.inputTokens + (event.inputTokens ?? 0),
|
|
180
|
-
cachedTokens: prior.cachedTokens + (event.cachedInputTokens ?? 0),
|
|
181
|
-
outputTokens: prior.outputTokens + (event.outputTokens ?? 0),
|
|
182
|
-
errors: prior.errors + failed,
|
|
183
|
-
estimatedCostUsd: prior.estimatedCostUsd + numericCost(event.estimatedCostUsd),
|
|
184
|
-
tokensPerSecSum: prior.tokensPerSecSum + (event.tokensPerSec ?? 0),
|
|
185
|
-
tokensPerSecCount: prior.tokensPerSecCount + (event.tokensPerSec === null ? 0 : 1),
|
|
186
|
-
};
|
|
187
|
-
return out;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Adds one event's tokens into its hour slot. */
|
|
191
|
-
function withHour(
|
|
192
|
-
hours: readonly IndexedHour[],
|
|
193
|
-
event: FoldedEvent,
|
|
194
|
-
): IndexedHour[] {
|
|
195
|
-
const base = hours.length === 24 ? [...hours] : emptyHours();
|
|
196
|
-
const index = hourOf(event.createdAt);
|
|
197
|
-
const prior = base[index] ?? { requests: 0, inputTokens: 0, cachedTokens: 0, outputTokens: 0 };
|
|
198
|
-
base[index] = {
|
|
199
|
-
requests: prior.requests + 1,
|
|
200
|
-
inputTokens: prior.inputTokens + (event.inputTokens ?? 0),
|
|
201
|
-
cachedTokens: prior.cachedTokens + (event.cachedInputTokens ?? 0),
|
|
202
|
-
outputTokens: prior.outputTokens + (event.outputTokens ?? 0),
|
|
203
|
-
};
|
|
204
|
-
return base;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** An empty index for a day with nothing counted yet. */
|
|
208
|
-
export function emptyDayIndex(day: string): DayIndex {
|
|
209
|
-
return {
|
|
210
|
-
kind: INDEX_KIND,
|
|
211
|
-
version: INDEX_VERSION,
|
|
212
|
-
day,
|
|
213
|
-
watermarkBytes: 0,
|
|
214
|
-
buckets: [],
|
|
215
|
-
updatedAt: new Date().toISOString(),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** True when the index is one this version can read. */
|
|
220
|
-
function isDayIndex(value: unknown): value is DayIndex {
|
|
221
|
-
if (typeof value !== "object" || value === null) return false;
|
|
222
|
-
const candidate = value as Partial<DayIndex>;
|
|
223
|
-
return (
|
|
224
|
-
candidate.kind === INDEX_KIND &&
|
|
225
|
-
candidate.version === INDEX_VERSION &&
|
|
226
|
-
typeof candidate.day === "string" &&
|
|
227
|
-
typeof candidate.watermarkBytes === "number" &&
|
|
228
|
-
Array.isArray(candidate.buckets)
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Reads a day index, or an empty one when it is absent or unreadable. */
|
|
233
|
-
export async function readDayIndex(day: string): Promise<DayIndex> {
|
|
234
|
-
const text = await readFile(join(TELEMETRY_EVENT_DIR, dayIndexFileName(day)), "utf8").catch(
|
|
235
|
-
() => undefined,
|
|
236
|
-
);
|
|
237
|
-
if (text === undefined) return emptyDayIndex(day);
|
|
238
|
-
try {
|
|
239
|
-
const parsed: unknown = JSON.parse(text);
|
|
240
|
-
return isDayIndex(parsed) ? parsed : emptyDayIndex(day);
|
|
241
|
-
} catch {
|
|
242
|
-
// A torn index costs nothing to rebuild — the event file is the record — so
|
|
243
|
-
// a parse failure starts over rather than failing the read.
|
|
244
|
-
return emptyDayIndex(day);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Writes an index atomically.
|
|
250
|
-
*
|
|
251
|
-
* Written to a pid-suffixed temp file and renamed, so a reader never sees a
|
|
252
|
-
* half-written index and a crash mid-write leaves the previous one in place.
|
|
253
|
-
*/
|
|
254
|
-
export async function writeDayIndex(index: DayIndex): Promise<void> {
|
|
255
|
-
await mkdir(TELEMETRY_EVENT_DIR, { recursive: true });
|
|
256
|
-
const target = join(TELEMETRY_EVENT_DIR, dayIndexFileName(index.day));
|
|
257
|
-
const temp = `${target}.${process.pid}.tmp`;
|
|
258
|
-
await writeFile(temp, `${JSON.stringify(index)}\n`, "utf8");
|
|
259
|
-
await rename(temp, target);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Byte size of a day's event file; 0 when it does not exist. */
|
|
263
|
-
export async function dayEventFileBytes(day: string): Promise<number> {
|
|
264
|
-
const size = await stat(join(TELEMETRY_EVENT_DIR, dayEventFileName(day)))
|
|
265
|
-
.then((value) => value.size)
|
|
266
|
-
.catch(() => undefined);
|
|
267
|
-
return size ?? 0;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Reads the uncounted tail of a day's event file.
|
|
272
|
-
*
|
|
273
|
-
* Only bytes past `fromBytes` are read, and a partial line at the very end is
|
|
274
|
-
* dropped rather than counted: the writer may still be appending to it, and a
|
|
275
|
-
* line that is not yet terminated is not yet an event.
|
|
276
|
-
*/
|
|
277
|
-
async function* readTail(
|
|
278
|
-
day: string,
|
|
279
|
-
fromBytes: number,
|
|
280
|
-
): AsyncGenerator<{ readonly event: FoldedEvent; readonly endBytes: number }> {
|
|
281
|
-
const handle = await open(join(TELEMETRY_EVENT_DIR, dayEventFileName(day)), "r").catch(
|
|
282
|
-
() => undefined,
|
|
283
|
-
);
|
|
284
|
-
if (handle === undefined) return;
|
|
285
|
-
try {
|
|
286
|
-
const size = (await handle.stat().catch(() => undefined))?.size ?? 0;
|
|
287
|
-
if (size <= fromBytes) return;
|
|
288
|
-
const length = size - fromBytes;
|
|
289
|
-
const buffer = Buffer.alloc(length);
|
|
290
|
-
await handle.read(buffer, 0, length, fromBytes);
|
|
291
|
-
const text = buffer.toString("utf8");
|
|
292
|
-
const lines = text.split("\n");
|
|
293
|
-
// The final element is either empty or a line the writer has not finished;
|
|
294
|
-
// a unterminated line is not an event yet, so it is left for the next fold.
|
|
295
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
296
|
-
else lines.pop();
|
|
297
|
-
let consumed = 0;
|
|
298
|
-
for (const line of lines) {
|
|
299
|
-
consumed += Buffer.byteLength(line, "utf8") + 1;
|
|
300
|
-
if (line.length === 0) continue;
|
|
301
|
-
const event = parseEventLine(line);
|
|
302
|
-
if (event !== undefined) yield { event, endBytes: fromBytes + consumed };
|
|
303
|
-
}
|
|
304
|
-
} finally {
|
|
305
|
-
await handle.close().catch(() => undefined);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Parses one event line, or `undefined` when it is not a usable event. */
|
|
310
|
-
function parseEventLine(line: string): FoldedEvent | undefined {
|
|
311
|
-
try {
|
|
312
|
-
const parsed: unknown = JSON.parse(line);
|
|
313
|
-
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
314
|
-
const candidate = parsed as Record<string, unknown>;
|
|
315
|
-
if (typeof candidate["tenantId"] !== "string") return undefined;
|
|
316
|
-
if (typeof candidate["requestId"] !== "string") return undefined;
|
|
317
|
-
if (typeof candidate["createdAt"] !== "string") return undefined;
|
|
318
|
-
// `tenantId` is checked above but declared optional on the record, so the
|
|
319
|
-
// narrow shape is asserted here rather than at every use site.
|
|
320
|
-
return candidate as unknown as FoldedEvent;
|
|
321
|
-
} catch {
|
|
322
|
-
return undefined;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Folds every event written since the watermark into the day index.
|
|
328
|
-
*
|
|
329
|
-
* Safe to call as often as you like: the watermark advances only after the
|
|
330
|
-
* index is on disk, so a crash means some events are folded twice at worst —
|
|
331
|
-
* and folding is a sum, so that is corrected by running again with the tail,
|
|
332
|
-
* not by any compensating logic.
|
|
333
|
-
*/
|
|
334
|
-
export async function foldDayIndex(day: string): Promise<{ readonly folded: number }> {
|
|
335
|
-
const index = await readDayIndex(day);
|
|
336
|
-
const byKey = new Map<string, IndexedBucket>();
|
|
337
|
-
for (const bucket of index.buckets) {
|
|
338
|
-
byKey.set(bucketKey(bucket.tenantId, bucket.apiKeyId), bucket);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
let watermark = index.watermarkBytes;
|
|
342
|
-
let folded = 0;
|
|
343
|
-
for await (const { event, endBytes } of readTail(day, index.watermarkBytes)) {
|
|
344
|
-
const key = bucketKey(event.tenantId, event.apiKeyId ?? null);
|
|
345
|
-
const prior = byKey.get(key);
|
|
346
|
-
const base: IndexedBucket =
|
|
347
|
-
prior ?? {
|
|
348
|
-
...emptyBucket(),
|
|
349
|
-
tenantId: event.tenantId,
|
|
350
|
-
apiKeyId: event.apiKeyId ?? null,
|
|
351
|
-
hours: emptyHours(),
|
|
352
|
-
dimModel: {},
|
|
353
|
-
dimProvider: {},
|
|
354
|
-
dimClient: {},
|
|
355
|
-
dimClientIp: {},
|
|
356
|
-
};
|
|
357
|
-
byKey.set(key, {
|
|
358
|
-
...accumulate(base, event),
|
|
359
|
-
tenantId: base.tenantId,
|
|
360
|
-
apiKeyId: base.apiKeyId,
|
|
361
|
-
hours: withHour(base.hours, event),
|
|
362
|
-
dimModel: foldDimension(base.dimModel, event.requestedModel ?? event.reportedModel, event),
|
|
363
|
-
dimProvider: foldDimension(base.dimProvider, event.providerId, event),
|
|
364
|
-
dimClient: foldDimension(base.dimClient, event.sourceSurface, event),
|
|
365
|
-
dimClientIp: foldDimension(base.dimClientIp, event.clientIp, event),
|
|
366
|
-
});
|
|
367
|
-
watermark = Math.max(watermark, endBytes);
|
|
368
|
-
folded += 1;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (folded === 0) return { folded: 0 };
|
|
372
|
-
await writeDayIndex({
|
|
373
|
-
kind: INDEX_KIND,
|
|
374
|
-
version: INDEX_VERSION,
|
|
375
|
-
day,
|
|
376
|
-
watermarkBytes: watermark,
|
|
377
|
-
buckets: [...byKey.values()],
|
|
378
|
-
updatedAt: new Date().toISOString(),
|
|
379
|
-
});
|
|
380
|
-
return { folded };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/** The days whose index should be folded, newest last. */
|
|
384
|
-
export async function listEventDays(): Promise<readonly string[]> {
|
|
385
|
-
const { readdir } = await import("node:fs/promises");
|
|
386
|
-
const files = await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]);
|
|
387
|
-
const days = new Set<string>();
|
|
388
|
-
for (const file of files) {
|
|
389
|
-
if (file.endsWith(".events.jsonl")) days.add(file.slice(0, 10));
|
|
390
|
-
}
|
|
391
|
-
return [...days].sort();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Every bucket for one tenant, across the days a period covers.
|
|
396
|
-
*
|
|
397
|
-
* Reads only the index files for those days — a few small JSON reads — rather
|
|
398
|
-
* than parsing the event files, which is what made a usage query cost grow with
|
|
399
|
-
* traffic.
|
|
400
|
-
*/
|
|
401
|
-
export async function queryIndexedBuckets(options: {
|
|
402
|
-
/** Omit to read every tenant — a caller that filters by key, not by tenant. */
|
|
403
|
-
readonly tenantId?: string | undefined;
|
|
404
|
-
readonly since?: Date | undefined;
|
|
405
|
-
/** Fold each day's tail first, so a read never reports a stale total. */
|
|
406
|
-
readonly foldFirst?: boolean;
|
|
407
|
-
}): Promise<readonly IndexedBucket[]> {
|
|
408
|
-
const days = await listEventDays();
|
|
409
|
-
const sinceDay = options.since === undefined ? undefined : utcDay(options.since);
|
|
410
|
-
const out: IndexedBucket[] = [];
|
|
411
|
-
for (const day of days) {
|
|
412
|
-
if (sinceDay !== undefined && day < sinceDay) continue;
|
|
413
|
-
if (options.foldFirst === true) await foldDayIndex(day).catch(() => ({ folded: 0 }));
|
|
414
|
-
const index = await readDayIndex(day);
|
|
415
|
-
for (const bucket of index.buckets) {
|
|
416
|
-
if (options.tenantId !== undefined && bucket.tenantId !== options.tenantId) continue;
|
|
417
|
-
out.push(bucket);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return out;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/** The days a period spans, oldest first. */
|
|
424
|
-
export function daysSince(since: Date | undefined, now = new Date()): readonly string[] {
|
|
425
|
-
const start = since === undefined ? now : since;
|
|
426
|
-
const out: string[] = [];
|
|
427
|
-
const cursor = new Date(Date.UTC(
|
|
428
|
-
start.getUTCFullYear(),
|
|
429
|
-
start.getUTCMonth(),
|
|
430
|
-
start.getUTCDate(),
|
|
431
|
-
));
|
|
432
|
-
const end = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
433
|
-
while (cursor.getTime() <= end && out.length < 400) {
|
|
434
|
-
out.push(utcDay(cursor));
|
|
435
|
-
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
436
|
-
}
|
|
437
|
-
return out;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
export { mergeBucketsInternal as mergeBuckets };
|