cartethyia 2.5.3 → 2.5.6
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 +13 -4
- package/README.md +2 -2
- package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
- package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
- package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
- package/dist/dashboard/assets/tokens-COzario9.js +1 -0
- package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +57 -57
- package/migrations/README.md +52 -52
- package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
- package/package.json +96 -95
- package/scripts/cli/admin.ts +125 -0
- package/scripts/cli/autostart.ts +237 -0
- package/scripts/cli/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
- package/scripts/cli/doctor.ts +37 -0
- package/scripts/cli/{support/env.ts → env.ts} +99 -99
- package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
- package/scripts/cli/instance.ts +224 -0
- package/scripts/cli/main.ts +88 -18
- package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
- package/scripts/cli/port.ts +100 -0
- package/scripts/cli/{support/services.ts → services.ts} +88 -88
- package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
- package/scripts/cli/{command/start.ts → start.ts} +10 -5
- package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
- package/scripts/cli/tray/index.ts +62 -12
- package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
- package/scripts/cli/update.ts +40 -0
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +11 -7
- package/src/boot/dependencies.ts +50 -19
- package/src/boot/timeout.ts +2 -13
- package/src/config/cooldowns.ts +186 -138
- package/src/config/network.ts +8 -14
- package/src/config/providers.ts +7 -10
- package/src/config/runtime.ts +76 -76
- package/src/config/security.ts +15 -0
- package/src/config/spec.ts +52 -7
- package/src/config/telemetry.ts +26 -0
- package/src/config/timeouts.ts +0 -6
- package/src/console/admin/model-abuse/contracts.ts +28 -6
- package/src/console/admin/model-abuse/routes.ts +56 -3
- package/src/console/api-keys/bucket-spend.ts +15 -4
- package/src/console/api-keys/contracts.ts +410 -414
- package/src/console/api-keys/routes.ts +881 -880
- package/src/console/auth/credentials.ts +3 -3
- package/src/console/auth/setup-service.ts +2 -2
- package/src/console/backup/contracts.ts +15 -2
- package/src/console/backup/export-endpoint.ts +6 -2
- package/src/console/backup/nine-router.ts +2 -5
- package/src/console/backup/service.ts +4 -0
- package/src/console/backup/store.ts +192 -29
- package/src/console/backup/validate.ts +3 -45
- package/src/console/cli-tools/fs-ops.ts +487 -487
- package/src/console/console-router-guards.ts +1 -1
- package/src/console/console-router.ts +8 -0
- package/src/console/data-management/contracts.ts +100 -0
- package/src/console/data-management/operations.ts +234 -0
- package/src/console/data-management/routes.ts +103 -0
- package/src/console/domain-registration.ts +45 -1
- package/src/console/observability/contracts.ts +524 -485
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +1138 -1018
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/contracts.ts +6 -0
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +24 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/routes.ts +7 -0
- package/src/console/providers/catalog/store.ts +1553 -1532
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-shared.ts +52 -39
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/quota/refresh.ts +441 -429
- package/src/console/remote-support/contracts.ts +61 -0
- package/src/console/remote-support/routes.ts +65 -0
- package/src/console/remote-support/service.ts +207 -0
- package/src/console/response-compression.ts +0 -3
- package/src/console/routing/pools/import-parse.ts +206 -0
- package/src/console/routing/pools/operations.ts +543 -501
- package/src/console/routing/pools/probe-result.ts +3 -2
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -164
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +95 -2
- package/src/console/settings/store.ts +81 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- package/src/console/updates/routes.ts +16 -0
- package/src/main.ts +10 -0
- package/src/network/http2-bidi.ts +2 -4
- package/src/network/pool/agent.ts +478 -478
- package/src/network/pool/selector.ts +5 -4
- package/src/network/retry.ts +1 -2
- package/src/network/ssrf.ts +5 -4
- package/src/observability/event-compaction.ts +317 -0
- package/src/observability/event-day-index.ts +440 -0
- package/src/observability/event-log.ts +155 -6
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +158 -33
- package/src/observability/payload-store.ts +82 -61
- package/src/observability/redaction.ts +5 -6
- package/src/observability/request-notify.ts +43 -0
- package/src/observability/telemetry-status.ts +8 -9
- package/src/observability/token-speed.ts +5 -4
- package/src/persistence/db-mode.ts +7 -7
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/schema.ts +75 -29
- package/src/persistence/shared-cache.ts +1 -2
- package/src/persistence/stores/api-key-store.ts +18 -6
- package/src/persistence/stores/share-store.ts +4 -3
- package/src/persistence/stores/telemetry-store.ts +33 -11
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +139 -6
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/image-clamp.ts +19 -1
- package/src/protocol/registry.ts +8 -0
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/gemini.ts +9 -2
- package/src/protocol/request/messages.ts +18 -7
- package/src/protocol/request/responses.ts +9 -1
- package/src/protocol/response/chat.ts +34 -4
- package/src/protocol/response/codex.ts +7 -4
- package/src/protocol/response/dsml.ts +140 -28
- package/src/protocol/response/responses.ts +11 -5
- package/src/protocol/stream-error-frames.ts +4 -5
- package/src/providers/adapters/compatible-adapter.ts +3 -0
- package/src/providers/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/bundled-discovery-providers.ts +122 -119
- package/src/providers/configured-openai-providers.ts +4 -4
- package/src/providers/default-registry.ts +5 -4
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/catalog/catalog-format.ts +3 -6
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/openai-model-discovery.ts +5 -0
- package/src/providers/discovery/openrouter-free-roster.ts +15 -17
- package/src/providers/discovery/probe-phases.ts +4 -4
- package/src/providers/discovery/probe-wire.ts +6 -13
- package/src/providers/discovery/probing-service.ts +1117 -1045
- package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
- package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
- package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
- package/src/providers/integrations/buddy/buddy-media.ts +2 -2
- package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
- package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
- package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
- package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
- package/src/providers/integrations/claude/claude.ts +608 -608
- package/src/providers/integrations/cline/cline-quota.ts +27 -20
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-headers.ts +25 -39
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +2 -29
- package/src/providers/integrations/commandcode/commandcode.ts +674 -675
- package/src/providers/integrations/commandcode/discovery.ts +117 -116
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
- package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
- package/src/providers/integrations/cursor/cursor-request.ts +616 -471
- package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
- package/src/providers/integrations/cursor/cursor.ts +1333 -960
- package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/factory/factory.ts +8 -1
- package/src/providers/integrations/gemini.ts +6 -1
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- package/src/providers/integrations/grok/grok-quota.ts +279 -177
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
- package/src/providers/integrations/grok/grok.ts +6 -7
- package/src/providers/integrations/hermes.ts +14 -18
- package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
- package/src/providers/integrations/kiro/kiro.ts +1 -1
- package/src/providers/integrations/mistral.ts +6 -6
- package/src/providers/integrations/opencode/fingerprint.ts +3 -3
- package/src/providers/integrations/opencode/provider.ts +19 -20
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
- package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/qoder/qoder.ts +884 -865
- package/src/providers/integrations/shared/connect.ts +6 -6
- package/src/providers/integrations/trae/trae-catalog.ts +1 -2
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/integrations/zcode/oauth.ts +6 -1
- package/src/providers/operations/account-health-service.ts +75 -27
- package/src/providers/operations/account-reset-service.ts +1 -1
- package/src/providers/operations/client-versions.ts +6 -6
- package/src/providers/operations/custom-cli-headers.ts +3 -4
- package/src/providers/operations/provider-catalog-seeder.ts +95 -95
- package/src/providers/operations/provider-catalog-service.ts +3 -4
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +10 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/abuse.ts +15 -10
- package/src/security/access-control.ts +144 -146
- package/src/security/admission/contracts.ts +11 -0
- package/src/security/admission/in-memory-store.ts +4 -3
- package/src/security/admission/lease-sweep.ts +6 -6
- package/src/security/admission/reasons.ts +5 -1
- package/src/security/admission/redis-store.ts +211 -210
- package/src/security/admission/service.ts +4 -0
- package/src/security/api-key-auth.ts +27 -17
- package/src/security/ip-boundary.ts +155 -15
- package/src/security/model-abuse.ts +366 -94
- package/src/security/outbound-headers.ts +3 -4
- package/src/shared/sleep.ts +4 -0
- package/src/transport/dispatch/attempt-finalize.ts +47 -5
- package/src/transport/dispatch/attempt-loop.ts +531 -530
- package/src/transport/dispatch/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +24 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- package/src/transport/dispatch/stream-bridge.ts +1 -3
- package/src/transport/dispatch/streaming/finalize.ts +1 -1
- package/src/transport/dispatch/streaming/stream.ts +19 -6
- package/src/transport/dispatch/upstream.ts +9 -0
- package/src/transport/failure-policy.ts +162 -8
- package/src/transport/guards/auth-guard.ts +7 -4
- package/src/transport/guards/concurrency-guard.ts +0 -5
- package/src/transport/guards/error-lifecycle.ts +13 -6
- package/src/transport/guards/pipeline.ts +15 -3
- package/src/transport/guards/request-context.ts +8 -2
- package/src/transport/request/inflight.ts +5 -6
- package/src/transport/request/preparer.ts +9 -8
- package/src/transport/request/state.ts +21 -8
- package/src/transport/request/tenant-preferences.ts +60 -0
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +65 -10
- package/src/transport/routing/route-model.ts +46 -14
- package/src/transport/routing/router.ts +21 -14
- package/src/transport/surface/completion.ts +2 -3
- package/src/transport/surface/dialects.ts +5 -6
- package/src/transport/surface/responses/encode.ts +38 -0
- package/src/transport/surface/responses/parse.ts +4 -0
- package/src/transport/translation/capabilities.ts +2 -3
- package/src/transport/translation/quirks.ts +3 -3
- package/src/workers/buddy-growth-tasks.ts +5 -10
- package/src/workers/daily-checkin.ts +17 -23
- package/src/workers/quota-refresh-sweep.ts +26 -26
- package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
- package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
- package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
- package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
- package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
- package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
- package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
- package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
- package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
- package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
- package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
- package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
- package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
- package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
- package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
- package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
- package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
- package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
- package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
- package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
- package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- package/scripts/cli/command/update.ts +0 -23
- package/scripts/cli/gateway/drain-token.ts +0 -78
- package/scripts/cli/gateway/instance-lock.ts +0 -105
- package/scripts/cli/gateway/stop.ts +0 -51
- package/scripts/cli/support/bun.ts +0 -27
- package/scripts/cli/support/version.ts +0 -37
- /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
- /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
|
@@ -0,0 +1,440 @@
|
|
|
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 };
|
|
@@ -1,13 +1,44 @@
|
|
|
1
1
|
import { open, readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { resolveTelemetryRetentionDays } from "../config/telemetry";
|
|
4
|
+
import { resolveDataDir } from "../persistence/db-mode";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Per-request telemetry lives in hourly JSONL files, not in Postgres. The
|
|
6
|
-
* database keeps only the durable aggregate
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* database keeps only the durable aggregate; the files are the single source
|
|
9
|
+
* for recent requests and readers must go through this module, not a
|
|
10
|
+
* `telemetry_events` table.
|
|
9
11
|
*/
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Where hourly event files and their compacted summaries live.
|
|
14
|
+
*
|
|
15
|
+
* Resolved from the configured data directory, never relative to the working
|
|
16
|
+
* directory: the gateway can be started from anywhere, and a relative path
|
|
17
|
+
* makes the written history depend on where the operator happened to be —
|
|
18
|
+
* start it from another folder and every past request reads as zero, because
|
|
19
|
+
* the files were written somewhere else entirely.
|
|
20
|
+
*/
|
|
21
|
+
export const TELEMETRY_EVENT_DIR = join(resolveDataDir(), "telemetry-events");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The source file an event lands in: one per closed `windowMinutes` bucket.
|
|
25
|
+
*
|
|
26
|
+
* The name is the bucket's own start (`YYYY-MM-DDTHH-MM`), so both the writer
|
|
27
|
+
* and the compactor derive it from a timestamp and cannot disagree. It is also
|
|
28
|
+
* what makes "is this window closed?" a name comparison rather than a read.
|
|
29
|
+
*/
|
|
30
|
+
export function windowFileName(at: Date, windowMinutes: number): string {
|
|
31
|
+
// `2026-10-22T05-15.jsonl`: ISO to the minute with the colon replaced, so the
|
|
32
|
+
// name sorts chronologically and needs no separate timestamp column.
|
|
33
|
+
return `${bucketStart(at, windowMinutes).toISOString().slice(0, 16).replace(":", "-")}.jsonl`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The start of the window containing `at`, floored to `windowMinutes`. */
|
|
37
|
+
export function bucketStart(at: Date, windowMinutes: number): Date {
|
|
38
|
+
const ms = Math.max(1, Math.floor(windowMinutes)) * 60_000;
|
|
39
|
+
return new Date(Math.floor(at.getTime() / ms) * ms);
|
|
40
|
+
}
|
|
41
|
+
|
|
11
42
|
|
|
12
43
|
export interface TelemetryEventRecord {
|
|
13
44
|
readonly id: string;
|
|
@@ -100,8 +131,17 @@ function normalize(raw: Record<string, unknown>): TelemetryEventRecord | undefin
|
|
|
100
131
|
};
|
|
101
132
|
}
|
|
102
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The comparable hour of an event file's name.
|
|
136
|
+
*
|
|
137
|
+
* A name is either `2026-10-22T05-15.jsonl` (an older window file) or
|
|
138
|
+
* `2026-10-22.events.jsonl` (a day file). Both sort against `hourStamp`: only
|
|
139
|
+
* the leading date and hour are taken, so the day file's `.events` suffix cannot
|
|
140
|
+
* make it compare as later or earlier than the window files beside it.
|
|
141
|
+
*/
|
|
103
142
|
function eventFileHour(file: string): string {
|
|
104
|
-
|
|
143
|
+
const stamp = file.endsWith(".events.jsonl") ? `${file.slice(0, 10)}T00` : file.slice(0, 13);
|
|
144
|
+
return stamp;
|
|
105
145
|
}
|
|
106
146
|
|
|
107
147
|
function hourStamp(date: Date): string {
|
|
@@ -111,6 +151,71 @@ function hourStamp(date: Date): string {
|
|
|
111
151
|
/** Chunk size for the streaming scan: one line can be long, a file cannot. */
|
|
112
152
|
const READ_CHUNK_BYTES = 256 * 1024;
|
|
113
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The newest matching events in one file, read backwards from the end.
|
|
156
|
+
*
|
|
157
|
+
* Reads fixed blocks from the tail until the page is full, so a day file of any
|
|
158
|
+
* size costs one block per page rather than a full parse. A partial line at the
|
|
159
|
+
* very end is dropped: the writer may still be appending to it.
|
|
160
|
+
*/
|
|
161
|
+
async function* readEventFileTail(
|
|
162
|
+
file: string,
|
|
163
|
+
tenantId: string | undefined,
|
|
164
|
+
want: number,
|
|
165
|
+
since: number | undefined,
|
|
166
|
+
until: number | undefined,
|
|
167
|
+
): AsyncGenerator<TelemetryEventRecord> {
|
|
168
|
+
if (want <= 0) return;
|
|
169
|
+
const handle = await open(join(TELEMETRY_EVENT_DIR, file), "r").catch(() => undefined);
|
|
170
|
+
if (handle === undefined) return;
|
|
171
|
+
try {
|
|
172
|
+
const size = (await handle.stat().catch(() => undefined))?.size ?? 0;
|
|
173
|
+
if (size === 0) return;
|
|
174
|
+
const BLOCK = 64 * 1024;
|
|
175
|
+
let end = size;
|
|
176
|
+
let carry = "";
|
|
177
|
+
const found: TelemetryEventRecord[] = [];
|
|
178
|
+
while (end > 0 && found.length < want) {
|
|
179
|
+
const start = Math.max(0, end - BLOCK);
|
|
180
|
+
const length = end - start;
|
|
181
|
+
const buffer = Buffer.alloc(length);
|
|
182
|
+
await handle.read(buffer, 0, length, start);
|
|
183
|
+
const text = buffer.toString("utf8") + carry;
|
|
184
|
+
const lines = text.split("\n");
|
|
185
|
+
carry = lines.shift() ?? "";
|
|
186
|
+
// Newest first: the block was read from the end, so its lines run
|
|
187
|
+
// backwards through the file.
|
|
188
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
189
|
+
const line = lines[index] ?? "";
|
|
190
|
+
if (line.length === 0) continue;
|
|
191
|
+
if (tenantId !== undefined && !line.includes(tenantId)) continue;
|
|
192
|
+
let parsed: TelemetryEventRecord | undefined;
|
|
193
|
+
try {
|
|
194
|
+
const value: unknown = JSON.parse(line);
|
|
195
|
+
if (typeof value === "object" && value !== null) {
|
|
196
|
+
parsed = value as TelemetryEventRecord;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (parsed === undefined) continue;
|
|
202
|
+
if (parsed.tenantId !== tenantId) continue;
|
|
203
|
+
const at = Date.parse(parsed.createdAt);
|
|
204
|
+
if (Number.isFinite(at)) {
|
|
205
|
+
if (since !== undefined && at < since) continue;
|
|
206
|
+
if (until !== undefined && at > until) continue;
|
|
207
|
+
}
|
|
208
|
+
found.push(parsed);
|
|
209
|
+
if (found.length >= want) break;
|
|
210
|
+
}
|
|
211
|
+
end = start;
|
|
212
|
+
}
|
|
213
|
+
for (const event of found) yield event;
|
|
214
|
+
} finally {
|
|
215
|
+
await handle.close().catch(() => undefined);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
114
219
|
async function* readEventFiles(
|
|
115
220
|
files: readonly string[],
|
|
116
221
|
allow?: (line: string) => boolean,
|
|
@@ -213,8 +318,29 @@ function matches(event: TelemetryEventRecord, query: EventQuery, since: number |
|
|
|
213
318
|
|
|
214
319
|
/** The events matching a query, already newest first. */
|
|
215
320
|
export async function queryTelemetryEvents(query: EventQuery): Promise<TelemetryEventRecord[]> {
|
|
321
|
+
// Hourly request files only. A `.compacted.jsonl` also ends in `.jsonl`, and
|
|
322
|
+
// letting one through meant every summary row was parsed as if it were a
|
|
323
|
+
// request — a row with no `tenantId`/`createdAt` that then failed `matches`,
|
|
324
|
+
// but only after paying for the parse.
|
|
325
|
+
//
|
|
326
|
+
// The lower bound is the retention window, not just `since`: a caller that
|
|
327
|
+
// asks for all time still must not scan files that have already been aged
|
|
328
|
+
// out, and one that passes no `since` at all must not read every file ever
|
|
329
|
+
// written. Files older than retention are gone or compacted; either way they
|
|
330
|
+
// are not here.
|
|
331
|
+
const retentionFloor = hourStamp(
|
|
332
|
+
new Date(Date.now() - resolveTelemetryRetentionDays() * 24 * 60 * 60_000),
|
|
333
|
+
);
|
|
334
|
+
const sinceFloor =
|
|
335
|
+
query.since === undefined ? retentionFloor : hourStamp(query.since);
|
|
336
|
+
const floor = sinceFloor > retentionFloor ? sinceFloor : retentionFloor;
|
|
216
337
|
const files = (await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]))
|
|
217
|
-
.filter(
|
|
338
|
+
.filter(
|
|
339
|
+
(file) =>
|
|
340
|
+
file.endsWith(".jsonl") &&
|
|
341
|
+
!file.endsWith(".compacted.jsonl") &&
|
|
342
|
+
eventFileHour(file) >= floor,
|
|
343
|
+
)
|
|
218
344
|
.sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
|
|
219
345
|
const since = query.since?.getTime();
|
|
220
346
|
const until = query.until?.getTime();
|
|
@@ -235,6 +361,27 @@ export async function queryTelemetryEvents(query: EventQuery): Promise<Telemetry
|
|
|
235
361
|
// A single-row lookup: stop at the first (newest) match instead of parsing
|
|
236
362
|
// the whole retention window.
|
|
237
363
|
const oneRow = query.requestId !== undefined || query.id !== undefined;
|
|
364
|
+
// With one file per day, a plain page of the newest rows would otherwise
|
|
365
|
+
// parse the whole day to return fifty lines. When nothing filters on
|
|
366
|
+
// content — no status, no key, no account — the newest rows are the last
|
|
367
|
+
// lines, so they are read backwards from the end and the scan stops once the
|
|
368
|
+
// page is full.
|
|
369
|
+
const tailOnly =
|
|
370
|
+
query.limit !== undefined &&
|
|
371
|
+
!oneRow &&
|
|
372
|
+
query.httpStatus === undefined &&
|
|
373
|
+
query.apiKeyIds === undefined &&
|
|
374
|
+
query.accountIds === undefined;
|
|
375
|
+
if (tailOnly) {
|
|
376
|
+
const newest: TelemetryEventRecord[] = [];
|
|
377
|
+
for (const file of files) {
|
|
378
|
+
for await (const event of readEventFileTail(file, query.tenantId, query.limit - newest.length, since, until)) {
|
|
379
|
+
newest.push(event);
|
|
380
|
+
}
|
|
381
|
+
if (newest.length >= query.limit) break;
|
|
382
|
+
}
|
|
383
|
+
return newest.slice(0, query.limit);
|
|
384
|
+
}
|
|
238
385
|
for (const file of files) {
|
|
239
386
|
const matched: TelemetryEventRecord[] = [];
|
|
240
387
|
for await (const event of readEventFiles([file], allow)) {
|
|
@@ -270,6 +417,8 @@ export function emptyTotals(): EventTotals {
|
|
|
270
417
|
return { requests: 0, succeeded: 0, failed: 0, cancelled: 0, truncated: 0, inputTokens: 0, cachedTokens: 0, outputTokens: 0, estimatedCost: 0, unpriced: 0, latencySum: 0, latencyCount: 0, tokensPerSecSum: 0, tokensPerSecCount: 0 };
|
|
271
418
|
}
|
|
272
419
|
|
|
420
|
+
/**
|
|
421
|
+
|
|
273
422
|
/** Folds one event into a totals bucket. `failed` counts any non-completed terminal status. */
|
|
274
423
|
export function accumulate(totals: EventTotals, event: TelemetryEventRecord): void {
|
|
275
424
|
totals.requests += 1;
|
|
@@ -115,12 +115,12 @@ function logArgs(
|
|
|
115
115
|
msg: string,
|
|
116
116
|
args: readonly unknown[],
|
|
117
117
|
): void {
|
|
118
|
-
// The level is consulted first. Building the safe copy
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
118
|
+
// The level is consulted first. Building the safe copy unconditionally would
|
|
119
|
+
// make a `debug` call at the production `info` default still pay a
|
|
120
|
+
// depth-capped deep copy plus a JSON.stringify of every argument, on every
|
|
121
|
+
// request that reaches it — the surface-detection debug line fires on almost
|
|
122
|
+
// every chat request. Pino's own level check happens inside `write`, which is
|
|
123
|
+
// already too late to skip the work.
|
|
124
124
|
if (!logger.isLevelEnabled(level)) return;
|
|
125
125
|
const safeArgs = safeLogArgs(args);
|
|
126
126
|
ring(level, msg, safeArgs);
|