cartethyia 2.5.3 → 2.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-C2orBsI2.js} +1 -1
- package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-BV6pxVQo.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CfuYiQ8Z.js → CombosPage-Bsan4679.js} +1 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-0p8uX7vB.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-BvzCx8Kr.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-CWkTUtpX.js → ModelPicker-BY1aKEkP.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +7 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +1 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-DDbMQsoo.js} +1 -1
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-CB3Z7VTf.js} +1 -1
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D95UCPHI.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-B47uOydr.js → UsagePage-Cnae9SWA.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-uNywYlfv.js → api-keys-VY68Mhd1.js} +1 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +2 -0
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-Chi7pCnt.js} +1 -1
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-D4pQapKj.js} +1 -1
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-DH6cg-_B.js} +1 -1
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/{card--AtFedAq.js → card-BKqlUOe4.js} +1 -1
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-fTgljTGA.js} +1 -1
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-D5Ys_sM0.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.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-BtpsJU98.js → dialog-BYj-8WuK.js} +1 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-WwwtzihF.js → drawer-CmXIZ6H1.js} +1 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +1 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js +1 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +4 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-BlptL8Uf.js} +1 -1
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/{index-BydyKaDz.js → index-js6TJvdr.js} +2 -2
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-DUWDSmDV.js} +1 -1
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/{input-BG41et9G.js → input-LVjgZq4f.js} +1 -1
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/{layout-C8mer_Ak.js → layout-CPfxpsfy.js} +1 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-DupvLfDx.js} +1 -1
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/{live-BJGYRBnR.js → live-C4pEJUqI.js} +1 -1
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js +1 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-B4ROwk8L.js} +1 -1
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +2 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-gmjzC3RS.js} +1 -1
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js.gz → query-keys-gmjzC3RS.js.gz} +0 -0
- package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-C7tBT5o-.js} +1 -1
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/{select-BO6h8yNv.js → select-WUCXorXw.js} +2 -2
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-C_zKl1GS.js} +1 -1
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-_3O0F8Xn.js} +1 -1
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/{state-DYdQmdAl.js → state-BgTxHcJS.js} +1 -1
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js +1 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.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/{useMutation-w4u4CVue.js → useMutation-BWSC0UAS.js} +1 -1
- package/dist/dashboard/assets/useMutation-BWSC0UAS.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 +54 -54
- package/package.json +1 -1
- package/scripts/cli/autostart.ts +237 -0
- package/scripts/cli/command/console-menu.ts +4 -8
- package/scripts/cli/command/update.ts +27 -10
- package/scripts/cli/main.ts +28 -1
- package/src/boot/app.ts +693 -693
- package/src/boot/dependencies.ts +646 -616
- package/src/config/spec.ts +55 -0
- package/src/config/telemetry.ts +46 -0
- package/src/console/api-keys/bucket-spend.ts +15 -4
- package/src/console/backup/contracts.ts +15 -2
- package/src/console/console-router.ts +8 -0
- package/src/console/data-management/contracts.ts +93 -0
- package/src/console/data-management/operations.ts +229 -0
- package/src/console/data-management/routes.ts +104 -0
- package/src/console/domain-registration.ts +39 -0
- package/src/console/observability/contracts.ts +524 -485
- package/src/console/observability/store.ts +1115 -1018
- package/src/console/providers/catalog/contracts.ts +6 -0
- package/src/console/providers/catalog/provider-operations.ts +20 -0
- package/src/console/providers/catalog/routes.ts +7 -0
- package/src/console/providers/catalog/store.ts +22 -1
- package/src/console/quota/account-quota-shared.ts +52 -39
- package/src/console/quota/refresh.ts +441 -429
- package/src/console/routing/pools/import-parse.ts +206 -0
- package/src/console/routing/pools/operations.ts +543 -501
- package/src/console/routing/pools/routes.ts +181 -164
- package/src/console/settings/contracts.ts +37 -0
- package/src/console/settings/store.ts +51 -0
- package/src/console/updates/routes.ts +16 -0
- package/src/main.ts +10 -0
- package/src/observability/event-compaction.ts +315 -0
- package/src/observability/event-day-index.ts +442 -0
- package/src/observability/event-log.ts +152 -3
- package/src/observability/payload-capture.ts +8 -1
- package/src/observability/payload-store.ts +440 -376
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/schema.ts +1402 -1360
- package/src/persistence/stores/telemetry-store.ts +237 -214
- package/src/protocol/codecs.ts +1 -2
- package/src/protocol/image-clamp.ts +19 -1
- package/src/protocol/registry.ts +8 -0
- package/src/protocol/request/gemini.ts +9 -2
- package/src/protocol/request/messages.ts +14 -2
- package/src/providers/adapters/compatible-adapter.ts +3 -0
- package/src/providers/bundled-discovery-providers.ts +122 -119
- package/src/providers/configured-openai-providers.ts +136 -134
- package/src/providers/default-registry.ts +5 -4
- package/src/providers/discovery/openai-model-discovery.ts +5 -0
- package/src/providers/discovery/openrouter-free-roster.ts +15 -17
- package/src/providers/integrations/cline/cline-quota.ts +132 -125
- package/src/providers/integrations/codex/codex-headers.ts +25 -39
- package/src/providers/integrations/codex/codex.ts +2 -29
- package/src/providers/integrations/cursor/cursor-quota.ts +310 -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 +1485 -960
- package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
- package/src/providers/integrations/factory/factory.ts +8 -1
- package/src/providers/integrations/gemini.ts +6 -1
- 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/opencode/provider.ts +12 -13
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/operations/account-health-service.ts +1523 -1480
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +6 -0
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/api-key-auth.ts +504 -494
- 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/streaming/stream.ts +14 -0
- package/src/transport/dispatch/upstream.ts +9 -0
- package/src/transport/request/preparer.ts +7 -6
- package/src/transport/request/state.ts +9 -0
- package/src/transport/request/tenant-preferences.ts +60 -0
- package/src/transport/routing/route-catalog.ts +58 -10
- 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.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.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.gz +0 -0
- 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.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.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/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.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/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/useMutation-w4u4CVue.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction for hourly telemetry files: old per-request rows become one
|
|
3
|
+
* summary row per (tenant, API key, day).
|
|
4
|
+
*
|
|
5
|
+
* The problem this solves is growth, and growth alone. Hourly JSONL files were
|
|
6
|
+
* never deleted, so every read of the usage list scanned every file ever
|
|
7
|
+
* written — the cost grew with total volume, not with the number of users. An
|
|
8
|
+
* install serving 20 users got slower purely by serving them longer.
|
|
9
|
+
*
|
|
10
|
+
* Compaction is **accumulation, not sampling**. A compacted row carries the
|
|
11
|
+
* running sums of everything it replaced, so a total that read 2B tokens
|
|
12
|
+
* before compaction still reads 2B after it. Nothing is estimated and nothing
|
|
13
|
+
* is averaged away — the per-request rows are gone, their totals are not.
|
|
14
|
+
*
|
|
15
|
+
* What compaction cannot preserve is anything that needs the individual row:
|
|
16
|
+
* the request list, per-request filters, and the detail drawer. Latency is the
|
|
17
|
+
* interesting middle case — a percentile needs every sample, so compaction
|
|
18
|
+
* keeps a fixed histogram instead. P50 stays available as an approximation and
|
|
19
|
+
* is marked as one rather than silently reported as exact.
|
|
20
|
+
*
|
|
21
|
+
* Retention becomes tiered:
|
|
22
|
+
*
|
|
23
|
+
* 0 .. N days hourly files, complete
|
|
24
|
+
* N .. M days compacted daily summary
|
|
25
|
+
* beyond M deleted
|
|
26
|
+
*/
|
|
27
|
+
import type { TelemetryEventRecord } from "./event-log";
|
|
28
|
+
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
29
|
+
import { consoleSettings } from "../persistence/schema";
|
|
30
|
+
import { resolveTelemetryWindowMinutes } from "../config/telemetry";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The compaction age to apply on a pass with no explicit age given.
|
|
34
|
+
*
|
|
35
|
+
* Hourly files are **not** partitioned per tenant — one file per hour holds
|
|
36
|
+
* every tenant's rows — so a per-tenant age cannot be honoured per file. The
|
|
37
|
+
* safest reading of several tenants' preferences is the **largest**: compacting
|
|
38
|
+
* smaller than one tenant asked for moves its requests into the summary before
|
|
39
|
+
* that tenant expected, losing the per-request view of them, while keeping them
|
|
40
|
+
* longer than another wanted only costs disk and a slower scan. Losing detail
|
|
41
|
+
* is not reversible; using more disk is.
|
|
42
|
+
*
|
|
43
|
+
* Falls back to the deployment default when no tenant overrides, and never
|
|
44
|
+
* exceeds the retention window (compacting at or past the delete age would
|
|
45
|
+
* fold a file and discard it in the same pass).
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveEffectiveWindowMinutes(db: CartethyiaDatabase): Promise<number> {
|
|
48
|
+
const fallback = resolveTelemetryWindowMinutes();
|
|
49
|
+
try {
|
|
50
|
+
const rows = await db
|
|
51
|
+
.select({ preferences: consoleSettings.preferences })
|
|
52
|
+
.from(consoleSettings);
|
|
53
|
+
let widest = fallback;
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
const minutes = row.preferences?.telemetryWindowMinutes;
|
|
56
|
+
if (typeof minutes === "number" && Number.isFinite(minutes) && minutes > widest) {
|
|
57
|
+
widest = minutes;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Math.floor(widest);
|
|
61
|
+
} catch {
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Discriminator so a reader can tell a summary from a request row at a glance. */
|
|
67
|
+
const COMPACTED_KIND = "compacted" as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Histogram bucket edges in milliseconds.
|
|
71
|
+
*
|
|
72
|
+
* Fixed rather than adaptive: a reader that wants P50/P95/P99 has to be able to
|
|
73
|
+
* interpret buckets it did not write, and an adaptive layout would have to be
|
|
74
|
+
* stored alongside every row. These follow the gateway's own latency
|
|
75
|
+
* histogram, so a compacted period and a live one read comparably.
|
|
76
|
+
*/
|
|
77
|
+
export const LATENCY_BUCKETS_MS = [
|
|
78
|
+
10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000,
|
|
79
|
+
] as const;
|
|
80
|
+
|
|
81
|
+
export interface CompactedBucket {
|
|
82
|
+
readonly requests: number;
|
|
83
|
+
readonly inputTokens: number;
|
|
84
|
+
readonly cachedTokens: number;
|
|
85
|
+
readonly outputTokens: number;
|
|
86
|
+
readonly reasoningTokens: number;
|
|
87
|
+
readonly estimatedCostUsd: number;
|
|
88
|
+
readonly creditUsed: number;
|
|
89
|
+
readonly succeeded: number;
|
|
90
|
+
readonly failed: number;
|
|
91
|
+
readonly cancelled: number;
|
|
92
|
+
readonly truncated: number;
|
|
93
|
+
readonly latencySum: number;
|
|
94
|
+
readonly latencyCount: number;
|
|
95
|
+
readonly latencyMinMs: number | null;
|
|
96
|
+
readonly latencyMaxMs: number | null;
|
|
97
|
+
/** Counts per fixed latency bucket; `+Inf` is the last, overflow, bucket. */
|
|
98
|
+
readonly latencyHistogram: readonly number[];
|
|
99
|
+
readonly tokensPerSecSum: number;
|
|
100
|
+
readonly tokensPerSecCount: number;
|
|
101
|
+
/** Requests whose cost could not be priced. */
|
|
102
|
+
readonly unpriced: number;
|
|
103
|
+
/**
|
|
104
|
+
* Requests per breakdown dimension, kept so the Usage breakdown survives the
|
|
105
|
+
* source files being deleted.
|
|
106
|
+
*
|
|
107
|
+
* Only the *counts* are kept, never the events: a day's breakdown asks how
|
|
108
|
+
* many requests went to a model, not which ones. That is what makes this
|
|
109
|
+
* small enough to keep — a full event is ~1KB, a count is a few bytes.
|
|
110
|
+
*/
|
|
111
|
+
readonly byModel: Readonly<Record<string, number>>;
|
|
112
|
+
readonly byProvider: Readonly<Record<string, number>>;
|
|
113
|
+
readonly byClient: Readonly<Record<string, number>>;
|
|
114
|
+
readonly byClientIp: Readonly<Record<string, number>>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One compacted row: the totals of everything it replaced, for one key/day. */
|
|
118
|
+
export interface CompactedRow extends CompactedBucket {
|
|
119
|
+
readonly kind: typeof COMPACTED_KIND;
|
|
120
|
+
/** ISO day (UTC) this row summarises. */
|
|
121
|
+
readonly day: string;
|
|
122
|
+
readonly tenantId: string;
|
|
123
|
+
readonly apiKeyId: string | null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A number from a value that may be a numeric string; 0 when unusable. */
|
|
127
|
+
function numeric(value: unknown): number {
|
|
128
|
+
const asNumber = typeof value === "number" ? value : Number(value);
|
|
129
|
+
return Number.isFinite(asNumber) ? asNumber : 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Adds one occurrence to a count map, ignoring an absent key. */
|
|
133
|
+
function bump(
|
|
134
|
+
counts: Readonly<Record<string, number>> | undefined,
|
|
135
|
+
key: string | null | undefined,
|
|
136
|
+
): Record<string, number> {
|
|
137
|
+
const out: Record<string, number> = { ...(counts ?? {}) };
|
|
138
|
+
const name = typeof key === "string" ? key.trim() : "";
|
|
139
|
+
if (name !== "") out[name] = (out[name] ?? 0) + 1;
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Adds two count maps. */
|
|
144
|
+
function mergeCounts(
|
|
145
|
+
a: Readonly<Record<string, number>> | undefined,
|
|
146
|
+
b: Readonly<Record<string, number>> | undefined,
|
|
147
|
+
): Record<string, number> {
|
|
148
|
+
const out: Record<string, number> = { ...(a ?? {}) };
|
|
149
|
+
for (const [key, value] of Object.entries(b ?? {})) out[key] = (out[key] ?? 0) + value;
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function emptyBucket(): CompactedBucket {
|
|
154
|
+
return {
|
|
155
|
+
byModel: {},
|
|
156
|
+
byProvider: {},
|
|
157
|
+
byClient: {},
|
|
158
|
+
byClientIp: {},
|
|
159
|
+
requests: 0,
|
|
160
|
+
inputTokens: 0,
|
|
161
|
+
cachedTokens: 0,
|
|
162
|
+
outputTokens: 0,
|
|
163
|
+
reasoningTokens: 0,
|
|
164
|
+
estimatedCostUsd: 0,
|
|
165
|
+
creditUsed: 0,
|
|
166
|
+
succeeded: 0,
|
|
167
|
+
failed: 0,
|
|
168
|
+
cancelled: 0,
|
|
169
|
+
truncated: 0,
|
|
170
|
+
latencySum: 0,
|
|
171
|
+
latencyCount: 0,
|
|
172
|
+
latencyMinMs: null,
|
|
173
|
+
latencyMaxMs: null,
|
|
174
|
+
latencyHistogram: new Array<number>(LATENCY_BUCKETS_MS.length + 1).fill(0),
|
|
175
|
+
tokensPerSecSum: 0,
|
|
176
|
+
tokensPerSecCount: 0,
|
|
177
|
+
unpriced: 0,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function bucketIndex(latencyMs: number): number {
|
|
182
|
+
for (let index = 0; index < LATENCY_BUCKETS_MS.length; index += 1) {
|
|
183
|
+
if (latencyMs <= LATENCY_BUCKETS_MS[index]!) return index;
|
|
184
|
+
}
|
|
185
|
+
return LATENCY_BUCKETS_MS.length;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Folds one request row into a bucket.
|
|
190
|
+
*
|
|
191
|
+
* Every field is a running sum or a min/max, never an average of averages: an
|
|
192
|
+
* average of per-file averages would drift from the true mean as soon as the
|
|
193
|
+
* files held different request counts, and the whole point of compaction is
|
|
194
|
+
* that the totals survive intact.
|
|
195
|
+
*/
|
|
196
|
+
export function accumulate(
|
|
197
|
+
into: CompactedBucket,
|
|
198
|
+
event: TelemetryEventRecord,
|
|
199
|
+
): CompactedBucket {
|
|
200
|
+
const histogram = [...into.latencyHistogram];
|
|
201
|
+
const latency = event.latencyMs;
|
|
202
|
+
if (latency !== null && Number.isFinite(latency)) {
|
|
203
|
+
histogram[bucketIndex(latency)] = (histogram[bucketIndex(latency)] ?? 0) + 1;
|
|
204
|
+
}
|
|
205
|
+
// A cost can arrive as a string: Postgres `numeric` columns come back that
|
|
206
|
+
// way, and so do restored rows. Adding one to a number concatenates instead of
|
|
207
|
+
// summing, which turned a cost of 0.000966 into "00.000966". Coerced here so
|
|
208
|
+
// every downstream sum is arithmetic.
|
|
209
|
+
const priced = numeric(event.estimatedCostUsd);
|
|
210
|
+
return {
|
|
211
|
+
byModel: bump(into.byModel, event.requestedModel ?? event.reportedModel),
|
|
212
|
+
byProvider: bump(into.byProvider, event.providerId),
|
|
213
|
+
byClient: bump(into.byClient, event.sourceSurface),
|
|
214
|
+
byClientIp: bump(into.byClientIp, event.clientIp),
|
|
215
|
+
requests: into.requests + 1,
|
|
216
|
+
inputTokens: into.inputTokens + (event.inputTokens ?? 0),
|
|
217
|
+
cachedTokens: into.cachedTokens + (event.cachedInputTokens ?? 0),
|
|
218
|
+
outputTokens: into.outputTokens + (event.outputTokens ?? 0),
|
|
219
|
+
reasoningTokens: into.reasoningTokens + (event.reasoningTokens ?? 0),
|
|
220
|
+
estimatedCostUsd: into.estimatedCostUsd + priced,
|
|
221
|
+
creditUsed: into.creditUsed + (event.creditUsed ?? 0),
|
|
222
|
+
succeeded: into.succeeded + (event.status === "completed" ? 1 : 0),
|
|
223
|
+
failed: into.failed + (event.status === "failed" ? 1 : 0),
|
|
224
|
+
cancelled: into.cancelled + (event.status === "cancelled" ? 1 : 0),
|
|
225
|
+
truncated: into.truncated + (event.status === "truncated" ? 1 : 0),
|
|
226
|
+
latencySum: into.latencySum + (latency ?? 0),
|
|
227
|
+
latencyCount: into.latencyCount + (latency === null ? 0 : 1),
|
|
228
|
+
latencyMinMs:
|
|
229
|
+
latency === null
|
|
230
|
+
? into.latencyMinMs
|
|
231
|
+
: into.latencyMinMs === null
|
|
232
|
+
? latency
|
|
233
|
+
: Math.min(into.latencyMinMs, latency),
|
|
234
|
+
latencyMaxMs:
|
|
235
|
+
latency === null
|
|
236
|
+
? into.latencyMaxMs
|
|
237
|
+
: into.latencyMaxMs === null
|
|
238
|
+
? latency
|
|
239
|
+
: Math.max(into.latencyMaxMs, latency),
|
|
240
|
+
latencyHistogram: histogram,
|
|
241
|
+
tokensPerSecSum: into.tokensPerSecSum + (event.tokensPerSec ?? 0),
|
|
242
|
+
tokensPerSecCount: into.tokensPerSecCount + (event.tokensPerSec === null ? 0 : 1),
|
|
243
|
+
// A completed request with no cost is unpriced, not free. Counting it keeps
|
|
244
|
+
// the dashboard's "partial cost" warning honest after compaction.
|
|
245
|
+
unpriced: into.unpriced + (event.status === "completed" && priced === 0 ? 1 : 0),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* An approximate percentile over a compacted histogram.
|
|
251
|
+
*
|
|
252
|
+
* Returns the upper edge of the bucket that contains it — never a value
|
|
253
|
+
* between edges, because a histogram cannot be more precise than the bucket
|
|
254
|
+
* that holds the sample. `null` when there are no samples, and `null` when the
|
|
255
|
+
* percentile falls in the overflow bucket, whose upper edge is unbounded.
|
|
256
|
+
*/
|
|
257
|
+
export function percentileFromHistogram(
|
|
258
|
+
histogram: readonly number[],
|
|
259
|
+
percentile: number,
|
|
260
|
+
): (typeof LATENCY_BUCKETS_MS)[number] | null {
|
|
261
|
+
const total = histogram.reduce((sum, count) => sum + count, 0);
|
|
262
|
+
if (total === 0) return null;
|
|
263
|
+
const target = Math.ceil((percentile / 100) * total);
|
|
264
|
+
let seen = 0;
|
|
265
|
+
for (let index = 0; index < histogram.length; index += 1) {
|
|
266
|
+
seen += histogram[index] ?? 0;
|
|
267
|
+
if (seen >= target) {
|
|
268
|
+
// The bucket's upper edge: a percentile from a histogram can never be
|
|
269
|
+
// more precise than the bucket that contains it.
|
|
270
|
+
return index < LATENCY_BUCKETS_MS.length ? LATENCY_BUCKETS_MS[index]! : null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Newline count of a file, so a torn or unparsable line is detectable. */
|
|
277
|
+
/** Adds two buckets. Exported so the day index can merge without duplicating it. */
|
|
278
|
+
export function mergeBucketsInternal(a: CompactedBucket, b: CompactedBucket): CompactedBucket {
|
|
279
|
+
const histogram = a.latencyHistogram.map((count, index) => count + (b.latencyHistogram[index] ?? 0));
|
|
280
|
+
return {
|
|
281
|
+
byModel: mergeCounts(a.byModel, b.byModel),
|
|
282
|
+
byProvider: mergeCounts(a.byProvider, b.byProvider),
|
|
283
|
+
byClient: mergeCounts(a.byClient, b.byClient),
|
|
284
|
+
byClientIp: mergeCounts(a.byClientIp, b.byClientIp),
|
|
285
|
+
requests: a.requests + b.requests,
|
|
286
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
287
|
+
cachedTokens: a.cachedTokens + b.cachedTokens,
|
|
288
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
289
|
+
reasoningTokens: a.reasoningTokens + b.reasoningTokens,
|
|
290
|
+
estimatedCostUsd: a.estimatedCostUsd + b.estimatedCostUsd,
|
|
291
|
+
creditUsed: a.creditUsed + b.creditUsed,
|
|
292
|
+
succeeded: a.succeeded + b.succeeded,
|
|
293
|
+
failed: a.failed + b.failed,
|
|
294
|
+
cancelled: a.cancelled + b.cancelled,
|
|
295
|
+
truncated: a.truncated + b.truncated,
|
|
296
|
+
latencySum: a.latencySum + b.latencySum,
|
|
297
|
+
latencyCount: a.latencyCount + b.latencyCount,
|
|
298
|
+
latencyMinMs:
|
|
299
|
+
a.latencyMinMs === null
|
|
300
|
+
? b.latencyMinMs
|
|
301
|
+
: b.latencyMinMs === null
|
|
302
|
+
? a.latencyMinMs
|
|
303
|
+
: Math.min(a.latencyMinMs, b.latencyMinMs),
|
|
304
|
+
latencyMaxMs:
|
|
305
|
+
a.latencyMaxMs === null
|
|
306
|
+
? b.latencyMaxMs
|
|
307
|
+
: b.latencyMaxMs === null
|
|
308
|
+
? a.latencyMaxMs
|
|
309
|
+
: Math.max(a.latencyMaxMs, b.latencyMaxMs),
|
|
310
|
+
latencyHistogram: histogram,
|
|
311
|
+
tokensPerSecSum: a.tokensPerSecSum + b.tokensPerSecSum,
|
|
312
|
+
tokensPerSecCount: a.tokensPerSecCount + b.tokensPerSecCount,
|
|
313
|
+
unpriced: a.unpriced + b.unpriced,
|
|
314
|
+
};
|
|
315
|
+
}
|