cartethyia 2.5.2 → 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-MsQaeQTs.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-DfWHTJ7v.js → CliToolsPage-BV6pxVQo.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-Bsan4679.js} +1 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.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-C8hKyXBe.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-CXjUlfqr.js → SetupPage-DDbMQsoo.js} +1 -1
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-CB3Z7VTf.js} +1 -1
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-D95UCPHI.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BEVLVrwP.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-DCBtROoK.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-DsX5QT8K.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 +244 -246
- package/scripts/cli/command/tray.ts +208 -204
- 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 +81 -2
- 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/backup/routes.ts +52 -1
- package/src/console/backup/service.ts +60 -4
- 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/share/share-router.ts +6 -9
- 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 +373 -344
- package/src/observability/payload-store.ts +440 -366
- package/src/observability/telemetry-status.ts +28 -16
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/schema.ts +1402 -1360
- package/src/persistence/stores/api-key-store.ts +6 -0
- package/src/persistence/stores/share-store.ts +583 -556
- 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/protocol/response/gemini.ts +18 -4
- 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/antigravity/antigravity-quota.ts +11 -3
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
- package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
- package/src/providers/integrations/cline/cline-quota.ts +132 -128
- 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 +531 -532
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae-catalog.ts +5 -3
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/operations/account-health-service.ts +1523 -1457
- package/src/providers/operations/client-versions.ts +9 -2
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +13 -1
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/api-key-auth.ts +504 -494
- package/src/transport/dispatch/attempt-finalize.ts +46 -5
- package/src/transport/dispatch/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +33 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- package/src/transport/dispatch/streaming/stream.ts +30 -2
- 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 +854 -799
- package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +0 -7
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BEVLVrwP.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-Dj9AR0gY.js +0 -4
- package/dist/dashboard/assets/entry-Dj9AR0gY.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-DCBtROoK.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-DsX5QT8K.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
|
@@ -1,1018 +1,1115 @@
|
|
|
1
|
-
import { loadavg, cpus, totalmem } from "node:os";
|
|
2
|
-
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
3
|
-
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
4
|
-
import type { RedisClient } from "../../persistence/redis";
|
|
5
|
-
import { apiKeys, networkPools, telemetryPayloads } from "../../persistence/schema";
|
|
6
|
-
import {
|
|
7
|
-
queryTelemetryEvents,
|
|
8
|
-
type TelemetryEventRecord,
|
|
9
|
-
} from "../../observability/event-log";
|
|
10
|
-
import { isGatewayError } from "../../observability/telemetry-status";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...(event.
|
|
50
|
-
...(event.
|
|
51
|
-
...(event.
|
|
52
|
-
...(event.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
...(event.
|
|
56
|
-
...(event.
|
|
57
|
-
...(event.
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return 86_400;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
...(event.
|
|
138
|
-
...(event.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
...(event.
|
|
143
|
-
...(event.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
...(event.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
...(event.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
...(
|
|
159
|
-
...(event.
|
|
160
|
-
...(event.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
...(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
...(event.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
...(event.
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
private
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
period,
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
)
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if (
|
|
807
|
-
const
|
|
808
|
-
.select({
|
|
809
|
-
.from(
|
|
810
|
-
.where(
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
// `
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
.
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1
|
+
import { loadavg, cpus, totalmem } from "node:os";
|
|
2
|
+
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
3
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
4
|
+
import type { RedisClient } from "../../persistence/redis";
|
|
5
|
+
import { apiKeys, networkPools, telemetryPayloads } from "../../persistence/schema";
|
|
6
|
+
import {
|
|
7
|
+
queryTelemetryEvents,
|
|
8
|
+
type TelemetryEventRecord,
|
|
9
|
+
} from "../../observability/event-log";
|
|
10
|
+
import { isGatewayError } from "../../observability/telemetry-status";
|
|
11
|
+
import { queryIndexedBuckets, type IndexedBucket } from "../../observability/event-day-index";
|
|
12
|
+
import {
|
|
13
|
+
LATENCY_BUCKETS_MS,
|
|
14
|
+
percentileFromHistogram,
|
|
15
|
+
} from "../../observability/event-compaction";
|
|
16
|
+
import { cachedReadWithEtag, type CachedResult } from "../shared/read-through-cache";
|
|
17
|
+
import { decodeDatedCursor, encodeCursor } from "../shared/cursor";
|
|
18
|
+
import { CARTETHYIA_VERSION } from "../../version";
|
|
19
|
+
import { CachedPreferencesReader, DrizzlePreferencesReader, type PreferencesReader } from "../../persistence/tenant-preferences";
|
|
20
|
+
import type {
|
|
21
|
+
ObservabilityStore,
|
|
22
|
+
UsageDimension,
|
|
23
|
+
SystemHealthResponse,
|
|
24
|
+
TelemetryEventView,
|
|
25
|
+
TelemetryEventDetail,
|
|
26
|
+
TelemetryEventListPage,
|
|
27
|
+
UsageByResponse,
|
|
28
|
+
UsageCacheResponse,
|
|
29
|
+
UsageChartBucket,
|
|
30
|
+
UsageChartResponse,
|
|
31
|
+
UsageRequestDetail,
|
|
32
|
+
UsageRequestItem,
|
|
33
|
+
UsageRequestsResponse,
|
|
34
|
+
UsageResponse,
|
|
35
|
+
UsageSummaryResponse,
|
|
36
|
+
} from "./contracts";
|
|
37
|
+
import { maskClientIp } from "../../observability/redaction";
|
|
38
|
+
import { splitEndpointConfig } from "../../network/pool/agent";
|
|
39
|
+
import { resolveDbMode } from "../../persistence/db-mode";
|
|
40
|
+
import { payloadReferenceFromRow, readPayloadFrame } from "../../observability/payload-store";
|
|
41
|
+
import { shouldMaskClientIp } from "../shared/ip-privacy";
|
|
42
|
+
|
|
43
|
+
function mapTelemetryEventRow(event: TelemetryEventRecord): TelemetryEventView {
|
|
44
|
+
return {
|
|
45
|
+
id: event.id,
|
|
46
|
+
createdAt: event.createdAt,
|
|
47
|
+
tenantId: event.tenantId ?? "",
|
|
48
|
+
requestId: event.requestId,
|
|
49
|
+
...(event.sourceSurface ? { sourceSurface: event.sourceSurface } : {}),
|
|
50
|
+
...(event.requestedModel ? { requestedModel: event.requestedModel } : {}),
|
|
51
|
+
...(event.reportedModel ? { reportedModel: event.reportedModel } : {}),
|
|
52
|
+
...(event.providerId ? { providerId: event.providerId } : {}),
|
|
53
|
+
...(event.latencyMs !== null ? { latencyMs: event.latencyMs ?? undefined } : {}),
|
|
54
|
+
...(event.status ? { status: event.status } : {}),
|
|
55
|
+
...(event.inputTokens !== null ? { inputTokens: event.inputTokens ?? undefined } : {}),
|
|
56
|
+
...(event.outputTokens !== null ? { outputTokens: event.outputTokens ?? undefined } : {}),
|
|
57
|
+
...(event.estimatedCostUsd !== null
|
|
58
|
+
? { estimatedCost: Number(event.estimatedCostUsd ?? 0) }
|
|
59
|
+
: {}),
|
|
60
|
+
...(event.tokensPerSec !== null ? { tokensPerSec: Number(event.tokensPerSec) } : {}),
|
|
61
|
+
...(event.firstContentDeltaAtMs !== null ? { firstContentDeltaAtMs: event.firstContentDeltaAtMs } : {}),
|
|
62
|
+
...(event.lastEventAtMs !== null ? { lastEventAtMs: event.lastEventAtMs } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** The HTTP status a row renders as when the wire status was never recorded. */
|
|
66
|
+
function effectiveHttpStatus(event: TelemetryEventRecord): number {
|
|
67
|
+
if (event.httpStatus !== null) return event.httpStatus;
|
|
68
|
+
if (event.status === "completed") return 200;
|
|
69
|
+
if (event.status === "cancelled") return 499;
|
|
70
|
+
if (event.status === "truncated") return 502;
|
|
71
|
+
return 500;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Opaque, monotonic (createdAt, id) cursor. `createdAt` alone is not unique
|
|
76
|
+
* — two telemetry events in the same millisecond would silently skip one
|
|
77
|
+
* row across pages — so we tie-break on id and encode both, matching
|
|
78
|
+
* `DrizzleAuditReadStore`'s cursor.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/** Resolves a `usage` period token ("24h", "7d", "30d", "all") to its inclusive start date. */
|
|
82
|
+
function periodStartDate(period: string): Date | undefined {
|
|
83
|
+
const match = /^(\d+)([hd])$/.exec(period);
|
|
84
|
+
if (!match) return undefined;
|
|
85
|
+
const amount = Number(match[1]);
|
|
86
|
+
const unitMs = match[2] === "h" ? 3_600_000 : 86_400_000;
|
|
87
|
+
return new Date(Date.now() - amount * unitMs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Client identity straight from the stored `User-Agent`: the first RFC 9110
|
|
93
|
+
* product token carries both the client and its version (`bun/1.2.3`,
|
|
94
|
+
* `codex-cli/0.1.0`), so no prefix catalog can drift from what the client
|
|
95
|
+
* actually sent. Browser agents start with the historical `Mozilla/5.0`
|
|
96
|
+
* token, which names no real client, so they report as `Browser`.
|
|
97
|
+
*/
|
|
98
|
+
export function clientNameFromUserAgent(userAgent: string | null | undefined): string {
|
|
99
|
+
const raw = userAgent?.trim();
|
|
100
|
+
if (!raw) return "unknown";
|
|
101
|
+
if (/^mozilla\//i.test(raw)) return "Browser";
|
|
102
|
+
const product = raw.split(/\s+/, 1)[0] ?? "";
|
|
103
|
+
const slash = product.indexOf("/");
|
|
104
|
+
const token = slash <= 0 ? product : product.slice(0, slash + 1).concat(product.slice(slash + 1).split(/[;,)]/, 1)[0] ?? "");
|
|
105
|
+
return token || "unknown";
|
|
106
|
+
}
|
|
107
|
+
/** Time-series bucket width for the usage chart, chosen from the window length. */
|
|
108
|
+
function chartBucketSeconds(period: string): number {
|
|
109
|
+
const since = periodStartDate(period);
|
|
110
|
+
if (!since) return 86_400;
|
|
111
|
+
const windowMs = Date.now() - since.getTime();
|
|
112
|
+
if (windowMs <= 2 * 3_600_000) return 300;
|
|
113
|
+
if (windowMs <= 2 * 86_400_000) return 3_600;
|
|
114
|
+
if (windowMs <= 10 * 86_400_000) return 21_600;
|
|
115
|
+
return 86_400;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The model id with the client's requested reasoning effort as a display
|
|
120
|
+
* suffix — only when the request actually carried one.
|
|
121
|
+
*/
|
|
122
|
+
function modelWithEffort(event: TelemetryEventRecord): string | undefined {
|
|
123
|
+
const model = event.requestedModel;
|
|
124
|
+
if (!model) return undefined;
|
|
125
|
+
return event.requestedEffort ? `${model} (${event.requestedEffort})` : model;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mapUsageRequestItem(
|
|
129
|
+
event: TelemetryEventRecord,
|
|
130
|
+
hideClientIp: boolean,
|
|
131
|
+
): UsageRequestItem {
|
|
132
|
+
const input = event.inputTokens ?? 0;
|
|
133
|
+
const output = event.outputTokens ?? 0;
|
|
134
|
+
return {
|
|
135
|
+
requestId: event.requestId,
|
|
136
|
+
...(event.sourceSurface ? { surface: event.sourceSurface } : {}),
|
|
137
|
+
...(event.providerId ? { providerId: event.providerId } : {}),
|
|
138
|
+
...(event.accountId ? { accountId: event.accountId } : {}),
|
|
139
|
+
...(modelWithEffort(event) === undefined ? {} : { model: modelWithEffort(event) as string }),
|
|
140
|
+
...(event.reportedModel ? { reportedModel: event.reportedModel } : {}),
|
|
141
|
+
status: event.status ?? "unknown",
|
|
142
|
+
...(event.errorCategory ? { errorKind: event.errorCategory } : {}),
|
|
143
|
+
...(event.errorOrigin ? { errorOrigin: event.errorOrigin } : {}),
|
|
144
|
+
httpStatus: effectiveHttpStatus(event),
|
|
145
|
+
mode: event.stream ? "stream" : "non_stream",
|
|
146
|
+
startedAt: event.createdAt,
|
|
147
|
+
...(event.latencyMs !== null ? { durationMs: event.latencyMs ?? undefined } : {}),
|
|
148
|
+
...(event.inputTokens !== null ? { inputTokens: event.inputTokens ?? undefined } : {}),
|
|
149
|
+
...(event.outputTokens !== null ? { outputTokens: event.outputTokens ?? undefined } : {}),
|
|
150
|
+
...(event.cachedInputTokens !== null
|
|
151
|
+
? { cachedTokens: event.cachedInputTokens ?? undefined }
|
|
152
|
+
: {}),
|
|
153
|
+
...(event.reasoningTokens !== null
|
|
154
|
+
? { reasoningTokens: event.reasoningTokens ?? undefined }
|
|
155
|
+
: {}),
|
|
156
|
+
...(event.endpoint ? { endpoint: event.endpoint } : {}),
|
|
157
|
+
...(event.apiKeyId ? { apiKeyId: event.apiKeyId } : {}),
|
|
158
|
+
...(event.userAgent ? { userAgent: event.userAgent } : {}),
|
|
159
|
+
...(event.userAgent ? { clientName: clientNameFromUserAgent(event.userAgent) } : {}),
|
|
160
|
+
...(event.clientIp
|
|
161
|
+
? { clientIp: hideClientIp ? maskClientIp(event.clientIp) : event.clientIp }
|
|
162
|
+
: {}),
|
|
163
|
+
...(input + output > 0 ? { totalTokens: input + output } : {}),
|
|
164
|
+
...(event.ttfbMs !== null ? { ttfbMs: event.ttfbMs ?? undefined } : {}),
|
|
165
|
+
...(event.tokensPerSec !== null ? { tokensPerSec: Number(event.tokensPerSec) } : {}),
|
|
166
|
+
...(event.firstContentDeltaAtMs !== null ? { firstContentDeltaAtMs: event.firstContentDeltaAtMs } : {}),
|
|
167
|
+
...(event.lastEventAtMs !== null ? { lastEventAtMs: event.lastEventAtMs } : {}),
|
|
168
|
+
...(event.estimatedCostUsd !== null && event.estimatedCostUsd !== undefined
|
|
169
|
+
? { estimatedCost: Number(event.estimatedCostUsd) }
|
|
170
|
+
: {}),
|
|
171
|
+
...(event.creditUsed !== null && event.creditUsed !== undefined
|
|
172
|
+
? { creditUsed: Number(event.creditUsed) }
|
|
173
|
+
: {}),
|
|
174
|
+
...(event.resolveMs !== null && event.resolveMs !== undefined
|
|
175
|
+
? { resolveMs: event.resolveMs }
|
|
176
|
+
: {}),
|
|
177
|
+
// Why the drawer is empty for this request — see the contract field. This is
|
|
178
|
+
// what makes the blank panels diagnosable without server logs.
|
|
179
|
+
...(event.payloadCaptureState ? { payloadCaptureState: event.payloadCaptureState } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function resolveProxyLabel(
|
|
183
|
+
db: CartethyiaDatabase,
|
|
184
|
+
tenantId: string,
|
|
185
|
+
poolId: string,
|
|
186
|
+
): Promise<string> {
|
|
187
|
+
const rows = await db
|
|
188
|
+
.select({ id: networkPools.id, endpointConfig: networkPools.endpointConfig })
|
|
189
|
+
.from(networkPools)
|
|
190
|
+
.where(and(eq(networkPools.id, poolId), eq(networkPools.tenantId, tenantId)))
|
|
191
|
+
.limit(1);
|
|
192
|
+
const row = rows[0];
|
|
193
|
+
if (!row) return `${poolId.slice(0, 8)}…`;
|
|
194
|
+
const { endpoint, label } = splitEndpointConfig((row.endpointConfig ?? {}) as Record<string, unknown>);
|
|
195
|
+
return label ?? endpoint ?? `${poolId.slice(0, 8)}…`;
|
|
196
|
+
}
|
|
197
|
+
const HEALTH_WINDOW_MS = 24 * 3_600_000;
|
|
198
|
+
const TOP_N = 50;
|
|
199
|
+
/**
|
|
200
|
+
* How long a parsed window is shared. A dashboard poll fires ~8 usage
|
|
201
|
+
* endpoints at once, and a slow parse used to outlive a 1.5 s window — so the
|
|
202
|
+
* endpoints that missed it each re-read and re-parsed the same files, which is
|
|
203
|
+
* how one page load became several full parses of the retention window.
|
|
204
|
+
*/
|
|
205
|
+
const PERIOD_MEMO_TTL_MS = 5_000;
|
|
206
|
+
|
|
207
|
+
/** Nearest-rank percentile over a sorted array, matching `percentile_cont` closely enough for a health gauge. */
|
|
208
|
+
function percentile(sorted: readonly number[], fraction: number): number {
|
|
209
|
+
if (sorted.length === 0) return 0;
|
|
210
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));
|
|
211
|
+
return Math.round(sorted[index] ?? 0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface UsagePerformanceSummary {
|
|
215
|
+
avgResolveMs: number;
|
|
216
|
+
avgDurationMs: number;
|
|
217
|
+
latencyP50Ms: number;
|
|
218
|
+
latencyP95Ms: number;
|
|
219
|
+
latencyP99Ms: number;
|
|
220
|
+
/** Events each series was computed from; see `UsageSummaryTotals`. */
|
|
221
|
+
latencySampleCount: number;
|
|
222
|
+
resolveSampleCount: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Summarizes routing delay and request latency for an already-filtered period. */
|
|
226
|
+
export function summarizeUsagePerformance(
|
|
227
|
+
events: readonly Pick<TelemetryEventRecord, "latencyMs" | "resolveMs">[],
|
|
228
|
+
): UsagePerformanceSummary {
|
|
229
|
+
const latencies: number[] = [];
|
|
230
|
+
let latencySum = 0;
|
|
231
|
+
let resolveSum = 0;
|
|
232
|
+
let resolveCount = 0;
|
|
233
|
+
for (const event of events) {
|
|
234
|
+
if (event.latencyMs !== null) {
|
|
235
|
+
latencies.push(event.latencyMs);
|
|
236
|
+
latencySum += event.latencyMs;
|
|
237
|
+
}
|
|
238
|
+
if (event.resolveMs !== null) {
|
|
239
|
+
resolveSum += event.resolveMs;
|
|
240
|
+
resolveCount += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
latencies.sort((a, b) => a - b);
|
|
244
|
+
return {
|
|
245
|
+
avgResolveMs: resolveCount > 0 ? resolveSum / resolveCount : 0,
|
|
246
|
+
avgDurationMs: latencies.length > 0 ? latencySum / latencies.length : 0,
|
|
247
|
+
latencyP50Ms: percentile(latencies, 0.5),
|
|
248
|
+
latencyP95Ms: percentile(latencies, 0.95),
|
|
249
|
+
latencyP99Ms: percentile(latencies, 0.99),
|
|
250
|
+
latencySampleCount: latencies.length,
|
|
251
|
+
resolveSampleCount: resolveCount,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The performance summary from indexed buckets rather than from events.
|
|
257
|
+
*
|
|
258
|
+
* The index keeps a latency sum, a sample count, and a histogram, so the mean
|
|
259
|
+
* stays exact and the percentiles stay bucketed — the same precision the
|
|
260
|
+
* per-event version had, without parsing the period's requests.
|
|
261
|
+
*/
|
|
262
|
+
export function summarizeBucketPerformance(
|
|
263
|
+
buckets: readonly IndexedBucket[],
|
|
264
|
+
): UsagePerformanceSummary {
|
|
265
|
+
let latencySum = 0;
|
|
266
|
+
let latencyCount = 0;
|
|
267
|
+
const histogram: number[] = [];
|
|
268
|
+
for (const bucket of buckets) {
|
|
269
|
+
latencySum += bucket.latencySum;
|
|
270
|
+
latencyCount += bucket.latencyCount;
|
|
271
|
+
for (let index = 0; index < bucket.latencyHistogram.length; index += 1) {
|
|
272
|
+
histogram[index] = (histogram[index] ?? 0) + (bucket.latencyHistogram[index] ?? 0);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const fill =
|
|
276
|
+
histogram.length > 0
|
|
277
|
+
? histogram
|
|
278
|
+
: new Array<number>(LATENCY_BUCKETS_MS.length + 1).fill(0);
|
|
279
|
+
return {
|
|
280
|
+
avgResolveMs: 0,
|
|
281
|
+
avgDurationMs: latencyCount > 0 ? latencySum / latencyCount : 0,
|
|
282
|
+
latencyP50Ms: percentileFromHistogram(fill, 50) ?? 0,
|
|
283
|
+
latencyP95Ms: percentileFromHistogram(fill, 95) ?? 0,
|
|
284
|
+
latencyP99Ms: percentileFromHistogram(fill, 99) ?? 0,
|
|
285
|
+
latencySampleCount: latencyCount,
|
|
286
|
+
resolveSampleCount: 0,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The largest entries of a count map, capped. */
|
|
291
|
+
function topCounts(counts: ReadonlyMap<string, number>, limit: number): Array<[string, number]> {
|
|
292
|
+
return [...counts].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The grouping value for one usage breakdown dimension. */
|
|
296
|
+
function dimensionName(dimension: UsageDimension, event: TelemetryEventRecord): string | null {
|
|
297
|
+
if (dimension === "model") return event.requestedModel;
|
|
298
|
+
if (dimension === "key") return event.apiKeyId;
|
|
299
|
+
if (dimension === "client") return event.userAgent;
|
|
300
|
+
if (dimension === "client_ip") return event.clientIp;
|
|
301
|
+
return event.providerId;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
interface BreakdownBucket {
|
|
305
|
+
name: string;
|
|
306
|
+
requests: number;
|
|
307
|
+
input: number;
|
|
308
|
+
output: number;
|
|
309
|
+
cached: number;
|
|
310
|
+
errors: number;
|
|
311
|
+
cost: number | null;
|
|
312
|
+
rateSum: number;
|
|
313
|
+
rateCount: number;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function emptyBreakdown(name: string): BreakdownBucket {
|
|
317
|
+
return { name, requests: 0, input: 0, output: 0, cached: 0, errors: 0, cost: null, rateSum: 0, rateCount: 0 };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Real Drizzle-backed observability repository used by the production console. */
|
|
321
|
+
export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
322
|
+
/**
|
|
323
|
+
* Per-(tenant, period) parsed event set, shared by the ~8 usage endpoints
|
|
324
|
+
* one dashboard poll fires in parallel. 1.5 s window: a poll burst shares
|
|
325
|
+
* one file parse; the next poll re-reads. In-flight shares one promise.
|
|
326
|
+
*/
|
|
327
|
+
private readonly periodMemo = new Map<string, { at: number; promise: Promise<readonly TelemetryEventRecord[]> }>();
|
|
328
|
+
private readonly periodBucketMemo = new Map<string, { at: number; promise: Promise<readonly IndexedBucket[]> }>();
|
|
329
|
+
constructor(
|
|
330
|
+
private readonly db: CartethyiaDatabase,
|
|
331
|
+
private readonly redis: RedisClient | undefined,
|
|
332
|
+
/**
|
|
333
|
+
* Tenant preferences, revision-cached. The Usage page reads the privacy
|
|
334
|
+
* gate on the list, detail and breakdown paths, so a direct SELECT here
|
|
335
|
+
* re-read the same row per request; the cache collapses that.
|
|
336
|
+
*/
|
|
337
|
+
private readonly preferences: PreferencesReader = new CachedPreferencesReader(
|
|
338
|
+
new DrizzlePreferencesReader(db),
|
|
339
|
+
),
|
|
340
|
+
) {}
|
|
341
|
+
|
|
342
|
+
async health(tenantId: string): Promise<CachedResult<SystemHealthResponse>> {
|
|
343
|
+
// The only usage-family read that used to bypass both the 5 s in-process
|
|
344
|
+
// memo and the Redis read-through, so it re-parsed 24 hourly files every
|
|
345
|
+
// page load. Same 10 s TTL as `/system/usage`.
|
|
346
|
+
return this.readThroughWithEtag(
|
|
347
|
+
this.usageCacheKey(tenantId, "24h", "health"),
|
|
348
|
+
() => this.computeHealth(tenantId),
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Same two numbers `computeHealth` reports, read fresh.
|
|
354
|
+
*
|
|
355
|
+
* Deliberately not `computeHealth`: that also scans 24 h of telemetry files,
|
|
356
|
+
* which is exactly the cost the cache exists to avoid, and a memory watcher
|
|
357
|
+
* polling every 10 s must not pay it. Kept in the store so the percentage
|
|
358
|
+
* has one definition rather than two.
|
|
359
|
+
*/
|
|
360
|
+
async liveMemory(): Promise<{ memory_bytes: number; memory_percent: number }> {
|
|
361
|
+
const memory = process.memoryUsage();
|
|
362
|
+
return {
|
|
363
|
+
memory_bytes: memory.rss,
|
|
364
|
+
memory_percent: Math.round((memory.rss / Math.max(totalmem(), 1)) * 10000) / 100,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private async computeHealth(tenantId: string): Promise<SystemHealthResponse> {
|
|
369
|
+
const memory = process.memoryUsage();
|
|
370
|
+
// `loadavg()` is [0, 0, 0] on every Windows build — there is no such
|
|
371
|
+
// syscall and Node reports a zeroed tuple rather than failing. Divided by
|
|
372
|
+
// cores that is a confident-looking 0.0% on a busy gateway, which is worse
|
|
373
|
+
// than saying nothing: an operator reads it as "idle" and stops looking.
|
|
374
|
+
// Reported as null instead of as a measurement.
|
|
375
|
+
//
|
|
376
|
+
// A real zero load is *not* the same thing and is reported as zero: an
|
|
377
|
+
// idle machine genuinely has nothing queued, and collapsing that into
|
|
378
|
+
// "unavailable" would hide a number the platform did supply.
|
|
379
|
+
const cpuPercent: number | null =
|
|
380
|
+
process.platform === "win32"
|
|
381
|
+
? null
|
|
382
|
+
: Math.round(((loadavg()[0] ?? 0) / Math.max(cpus().length, 1)) * 10000) / 100;
|
|
383
|
+
|
|
384
|
+
let databaseHealthy = true;
|
|
385
|
+
let total = 0;
|
|
386
|
+
let errors = 0;
|
|
387
|
+
let avg = 0;
|
|
388
|
+
let p95 = 0;
|
|
389
|
+
let p99 = 0;
|
|
390
|
+
let cacheHitRate = 0;
|
|
391
|
+
let avgTokensPerSec = 0;
|
|
392
|
+
try {
|
|
393
|
+
// Bounded 24h rolling window (matches the Overview copy). The file is the
|
|
394
|
+
// source, so the percentiles are computed over the retained events.
|
|
395
|
+
const since = new Date(Date.now() - HEALTH_WINDOW_MS);
|
|
396
|
+
const events = await queryTelemetryEvents({ tenantId, since });
|
|
397
|
+
const latencies: number[] = [];
|
|
398
|
+
let latencySum = 0;
|
|
399
|
+
let inputTokens = 0;
|
|
400
|
+
let cachedTokens = 0;
|
|
401
|
+
let outputTokens = 0;
|
|
402
|
+
let tokensPerSecSum = 0;
|
|
403
|
+
let tokensPerSecCount = 0;
|
|
404
|
+
for (const event of events) {
|
|
405
|
+
total += 1;
|
|
406
|
+
if (isGatewayError(event.status, event.httpStatus)) errors += 1;
|
|
407
|
+
if (event.latencyMs !== null) {
|
|
408
|
+
latencies.push(event.latencyMs);
|
|
409
|
+
latencySum += event.latencyMs;
|
|
410
|
+
}
|
|
411
|
+
if (event.cachedInputTokens !== null && event.cachedInputTokens > 0) {
|
|
412
|
+
inputTokens += event.inputTokens ?? 0;
|
|
413
|
+
cachedTokens += event.inputTokens ?? 0;
|
|
414
|
+
outputTokens += event.outputTokens ?? 0;
|
|
415
|
+
}
|
|
416
|
+
if (event.tokensPerSec !== null) {
|
|
417
|
+
tokensPerSecSum += event.tokensPerSec;
|
|
418
|
+
tokensPerSecCount += 1;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
latencies.sort((a, b) => a - b);
|
|
422
|
+
avg = latencies.length > 0 ? Math.round(latencySum / latencies.length) : 0;
|
|
423
|
+
p95 = percentile(latencies, 0.95);
|
|
424
|
+
p99 = percentile(latencies, 0.99);
|
|
425
|
+
// Cached share of the prompt: cached/input, the same honest definition
|
|
426
|
+
// usageSummary uses, so the health card and the Usage page cannot
|
|
427
|
+
// disagree. (This used to be prompt-share input/(input+output), which
|
|
428
|
+
// read ~99% and was mislabeled a cache hit rate.)
|
|
429
|
+
cacheHitRate = inputTokens > 0 ? Math.round((cachedTokens / inputTokens) * 10000) / 100 : 0;
|
|
430
|
+
avgTokensPerSec = tokensPerSecCount > 0 ? tokensPerSecSum / tokensPerSecCount : 0;
|
|
431
|
+
} catch {
|
|
432
|
+
databaseHealthy = false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// No client means the memory backend, which is healthy by construction.
|
|
436
|
+
let redisHealthy = this.redis === undefined;
|
|
437
|
+
if (this.redis !== undefined) {
|
|
438
|
+
try {
|
|
439
|
+
redisHealthy = (await this.redis.ping()) === "PONG";
|
|
440
|
+
} catch {
|
|
441
|
+
redisHealthy = false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const status: SystemHealthResponse["status"] = !databaseHealthy
|
|
446
|
+
? "unhealthy"
|
|
447
|
+
: !redisHealthy
|
|
448
|
+
? "degraded"
|
|
449
|
+
: "healthy";
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
version: CARTETHYIA_VERSION,
|
|
453
|
+
status,
|
|
454
|
+
uptime_seconds: Math.floor(process.uptime()),
|
|
455
|
+
database_healthy: databaseHealthy,
|
|
456
|
+
redis_healthy: redisHealthy,
|
|
457
|
+
redis_configured: this.redis !== undefined,
|
|
458
|
+
memory_heap_bytes: memory.heapUsed,
|
|
459
|
+
memory_external_bytes: memory.external,
|
|
460
|
+
memory_bytes: memory.rss,
|
|
461
|
+
memory_percent: Math.round((memory.rss / Math.max(totalmem(), 1)) * 10000) / 100,
|
|
462
|
+
// Reported so the console can explain a large RSS instead of leaving the
|
|
463
|
+
// operator to read it as the gateway alone. In lite mode this process
|
|
464
|
+
// hosts the database too, so the resident set covers both.
|
|
465
|
+
database_in_process: resolveDbMode() === "lite",
|
|
466
|
+
cpu_percent: cpuPercent,
|
|
467
|
+
cpu_cores: cpus().length,
|
|
468
|
+
pid: process.pid,
|
|
469
|
+
platform: `Bun ${Bun.version} · ${process.platform}`,
|
|
470
|
+
request_count: total,
|
|
471
|
+
error_count: errors,
|
|
472
|
+
latency_avg_ms: avg,
|
|
473
|
+
latency_p95_ms: p95,
|
|
474
|
+
latency_p99_ms: p99,
|
|
475
|
+
cache_hit_rate_percent: cacheHitRate,
|
|
476
|
+
avg_tokens_per_sec: avgTokensPerSec,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async usage(tenantId: string, period: string): Promise<CachedResult<UsageResponse>> {
|
|
480
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, period, "usage"), () =>
|
|
481
|
+
this.computeUsage(tenantId, period),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Cache key for one usage response. Versioned so a shape change can never be
|
|
487
|
+
* read back from a deployment that still holds the old one.
|
|
488
|
+
*/
|
|
489
|
+
private usageCacheKey(tenantId: string, period: string, shape: string): string {
|
|
490
|
+
return `cartethyia:usage:v1:${shape}:${tenantId}:${period}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Reads a usage response through Redis — computing when no cache is
|
|
495
|
+
* configured — and returns the strong validator alongside it, so a repeated
|
|
496
|
+
* poll can be answered with a `304` instead of the whole payload.
|
|
497
|
+
*/
|
|
498
|
+
private readThroughWithEtag<T>(key: string, compute: () => Promise<T>): Promise<CachedResult<T>> {
|
|
499
|
+
return cachedReadWithEtag(this.redis, key, compute);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private async computeUsage(tenantId: string, period: string): Promise<UsageResponse> {
|
|
503
|
+
const events = await this.periodEvents(tenantId, period);
|
|
504
|
+
let requestsTotal = 0;
|
|
505
|
+
let requestsSucceeded = 0;
|
|
506
|
+
let requestsFailed = 0;
|
|
507
|
+
let tokensUsed = 0;
|
|
508
|
+
let estimatedCost = 0;
|
|
509
|
+
const models = new Map<string, number>();
|
|
510
|
+
const providers = new Map<string, number>();
|
|
511
|
+
for (const event of events) {
|
|
512
|
+
requestsTotal += 1;
|
|
513
|
+
if (event.status === "completed") requestsSucceeded += 1;
|
|
514
|
+
if (isGatewayError(event.status, event.httpStatus)) requestsFailed += 1;
|
|
515
|
+
tokensUsed += (event.inputTokens ?? 0) + (event.outputTokens ?? 0);
|
|
516
|
+
estimatedCost += event.estimatedCostUsd ?? 0;
|
|
517
|
+
if (event.requestedModel) models.set(event.requestedModel, (models.get(event.requestedModel) ?? 0) + 1);
|
|
518
|
+
if (event.providerId) providers.set(event.providerId, (providers.get(event.providerId) ?? 0) + 1);
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
tenantId,
|
|
522
|
+
period,
|
|
523
|
+
requestsTotal,
|
|
524
|
+
requestsSucceeded,
|
|
525
|
+
requestsFailed,
|
|
526
|
+
tokensUsed,
|
|
527
|
+
estimatedCost,
|
|
528
|
+
topModels: topCounts(models, TOP_N).map(([modelId, count]) => ({ modelId, count })),
|
|
529
|
+
topProviders: topCounts(providers, TOP_N).map(([providerId, count]) => ({ providerId, count })),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** The tenant's retained events inside a usage period. `all` keeps everything. */
|
|
534
|
+
/**
|
|
535
|
+
* The indexed buckets covering a period, for one tenant.
|
|
536
|
+
*
|
|
537
|
+
* Reads the per-day index files instead of the event files. A usage query no
|
|
538
|
+
* longer parses every request in the window and filters it in memory, so its
|
|
539
|
+
* cost no longer grows with traffic.
|
|
540
|
+
*/
|
|
541
|
+
private periodBuckets(tenantId: string, period: string): Promise<readonly IndexedBucket[]> {
|
|
542
|
+
const key = `${tenantId}:${period}`;
|
|
543
|
+
const now = Date.now();
|
|
544
|
+
const hit = this.periodBucketMemo.get(key);
|
|
545
|
+
if (hit && now - hit.at < PERIOD_MEMO_TTL_MS) return hit.promise;
|
|
546
|
+
const since = periodStartDate(period);
|
|
547
|
+
// Fold before reading. Events appended since the scheduler last ran are
|
|
548
|
+
// otherwise invisible, and a usage page that under-reports is worse than one
|
|
549
|
+
// that is a moment slow.
|
|
550
|
+
const promise = queryIndexedBuckets({
|
|
551
|
+
tenantId,
|
|
552
|
+
foldFirst: true,
|
|
553
|
+
...(since === undefined ? {} : { since }),
|
|
554
|
+
});
|
|
555
|
+
this.periodBucketMemo.set(key, { at: now, promise });
|
|
556
|
+
void promise.catch(() => {
|
|
557
|
+
if (this.periodBucketMemo.get(key)?.promise === promise) this.periodBucketMemo.delete(key);
|
|
558
|
+
});
|
|
559
|
+
return promise;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private periodEvents(tenantId: string, period: string): Promise<readonly TelemetryEventRecord[]> {
|
|
563
|
+
const key = `${tenantId}:${period}`;
|
|
564
|
+
const now = Date.now();
|
|
565
|
+
const hit = this.periodMemo.get(key);
|
|
566
|
+
if (hit && now - hit.at < PERIOD_MEMO_TTL_MS) return hit.promise;
|
|
567
|
+
const since = periodStartDate(period);
|
|
568
|
+
const promise = queryTelemetryEvents(since === undefined ? { tenantId } : { tenantId, since });
|
|
569
|
+
this.periodMemo.set(key, { at: now, promise });
|
|
570
|
+
// A rejected parse must not poison the memo for the next poll.
|
|
571
|
+
void promise.catch(() => {
|
|
572
|
+
if (this.periodMemo.get(key)?.promise === promise) this.periodMemo.delete(key);
|
|
573
|
+
});
|
|
574
|
+
return promise;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>> {
|
|
578
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, period, "summary"), () =>
|
|
579
|
+
this.computeSummary(tenantId, period),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private async computeSummary(tenantId: string, period: string): Promise<UsageSummaryResponse> {
|
|
584
|
+
const buckets = await this.periodBuckets(tenantId, period);
|
|
585
|
+
const performance = summarizeBucketPerformance(buckets);
|
|
586
|
+
let requests = 0;
|
|
587
|
+
let inputTokens = 0;
|
|
588
|
+
let cachedTokens = 0;
|
|
589
|
+
let outputTokens = 0;
|
|
590
|
+
let errors = 0;
|
|
591
|
+
let cancelled = 0;
|
|
592
|
+
let truncated = 0;
|
|
593
|
+
let estimatedCostUsd = 0;
|
|
594
|
+
let unpriced = 0;
|
|
595
|
+
let tokensPerSecSum = 0;
|
|
596
|
+
let tokensPerSecCount = 0;
|
|
597
|
+
const statuses = new Map<number, number>();
|
|
598
|
+
for (const row of buckets) {
|
|
599
|
+
requests += row.requests;
|
|
600
|
+
inputTokens += row.inputTokens;
|
|
601
|
+
outputTokens += row.outputTokens;
|
|
602
|
+
cachedTokens += row.cachedTokens;
|
|
603
|
+
errors += row.failed;
|
|
604
|
+
cancelled += row.cancelled;
|
|
605
|
+
truncated += row.truncated;
|
|
606
|
+
estimatedCostUsd += row.estimatedCostUsd;
|
|
607
|
+
unpriced += row.unpriced;
|
|
608
|
+
tokensPerSecSum += row.tokensPerSecSum;
|
|
609
|
+
tokensPerSecCount += row.tokensPerSecCount;
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
period,
|
|
613
|
+
totals: {
|
|
614
|
+
statusCounts: [...statuses].sort(([a], [b]) => a - b).map(([status, count]) => ({ status, count })),
|
|
615
|
+
requests,
|
|
616
|
+
inputTokens,
|
|
617
|
+
cachedTokens,
|
|
618
|
+
outputTokens,
|
|
619
|
+
errors,
|
|
620
|
+
cancelled,
|
|
621
|
+
truncated,
|
|
622
|
+
...performance,
|
|
623
|
+
estimatedCostUsd,
|
|
624
|
+
partial: unpriced > 0,
|
|
625
|
+
cacheHitRate: inputTokens > 0 ? (cachedTokens / inputTokens) * 100 : 0,
|
|
626
|
+
avgTokensPerSec: tokensPerSecCount > 0 ? tokensPerSecSum / tokensPerSecCount : 0,
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async usageChart(tenantId: string, period: string): Promise<CachedResult<UsageChartResponse>> {
|
|
632
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, period, "chart"), () =>
|
|
633
|
+
this.computeChart(tenantId, period),
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private async computeChart(tenantId: string, period: string): Promise<UsageChartResponse> {
|
|
638
|
+
const since = periodStartDate(period);
|
|
639
|
+
const bucketSeconds = chartBucketSeconds(period);
|
|
640
|
+
const stepMs = bucketSeconds * 1000;
|
|
641
|
+
const events = await this.periodEvents(tenantId, period);
|
|
642
|
+
const byTime = new Map<number, UsageChartBucket>();
|
|
643
|
+
for (const event of events) {
|
|
644
|
+
const at = new Date(event.createdAt).getTime();
|
|
645
|
+
if (!Number.isFinite(at)) continue;
|
|
646
|
+
const start = Math.floor(at / stepMs) * stepMs;
|
|
647
|
+
const bucket = byTime.get(start) ?? { t: new Date(start).toISOString(), requests: 0, input: 0, cached: 0, output: 0 };
|
|
648
|
+
bucket.requests += 1;
|
|
649
|
+
bucket.input += event.inputTokens ?? 0;
|
|
650
|
+
bucket.cached += event.cachedInputTokens !== null && event.cachedInputTokens > 0 ? (event.inputTokens ?? 0) : 0;
|
|
651
|
+
bucket.output += event.outputTokens ?? 0;
|
|
652
|
+
byTime.set(start, bucket);
|
|
653
|
+
}
|
|
654
|
+
const rows = [...byTime.keys()];
|
|
655
|
+
const first = rows.length > 0 ? Math.min(...byTime.keys()) : undefined;
|
|
656
|
+
const startMs = since ? since.getTime() : first;
|
|
657
|
+
if (startMs === undefined) return { buckets: [] };
|
|
658
|
+
const aligned = Math.floor(startMs / stepMs) * stepMs;
|
|
659
|
+
const buckets: UsageChartBucket[] = [];
|
|
660
|
+
for (let at = aligned; at <= Date.now(); at += stepMs) {
|
|
661
|
+
buckets.push(
|
|
662
|
+
byTime.get(at) ?? { t: new Date(at).toISOString(), requests: 0, input: 0, cached: 0, output: 0 },
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return { buckets };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async usageBy(
|
|
669
|
+
tenantId: string,
|
|
670
|
+
dimension: UsageDimension,
|
|
671
|
+
period: string,
|
|
672
|
+
): Promise<CachedResult<UsageByResponse>> {
|
|
673
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, `${dimension}:${period}`, "by"), () =>
|
|
674
|
+
this.computeBy(tenantId, dimension, period),
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private async computeBy(
|
|
679
|
+
tenantId: string,
|
|
680
|
+
dimension: UsageDimension,
|
|
681
|
+
period: string,
|
|
682
|
+
): Promise<UsageByResponse> {
|
|
683
|
+
// The breakdown is read from the per-name sums the day index maintains, not
|
|
684
|
+
// from the events: the console asks how much went to each model, and the
|
|
685
|
+
// index already carries exactly that. Parsing the window to recount it was
|
|
686
|
+
// the whole cost of this endpoint.
|
|
687
|
+
const buckets = await this.periodBuckets(tenantId, period);
|
|
688
|
+
const groups = new Map<string, BreakdownBucket>();
|
|
689
|
+
// Fall back to the events only when the index carries nothing for the
|
|
690
|
+
// period — an install that has not folded yet, or a day whose index is
|
|
691
|
+
// missing. Never alongside it: the same request is in both, and counting it
|
|
692
|
+
// twice would double every column.
|
|
693
|
+
if (buckets.length === 0) {
|
|
694
|
+
for (const event of await this.periodEvents(tenantId, period)) {
|
|
695
|
+
const raw = dimensionName(dimension, event);
|
|
696
|
+
if (raw === null || raw.length === 0) continue;
|
|
697
|
+
const name = dimension === "client" ? clientNameFromUserAgent(raw) : raw;
|
|
698
|
+
const bucket = groups.get(name) ?? emptyBreakdown(name);
|
|
699
|
+
bucket.requests += 1;
|
|
700
|
+
bucket.input += event.inputTokens ?? 0;
|
|
701
|
+
bucket.output += event.outputTokens ?? 0;
|
|
702
|
+
if (event.cachedInputTokens !== null && event.cachedInputTokens > 0) {
|
|
703
|
+
bucket.cached += event.inputTokens ?? 0;
|
|
704
|
+
}
|
|
705
|
+
if (isGatewayError(event.status, event.httpStatus)) bucket.errors += 1;
|
|
706
|
+
if (event.estimatedCostUsd !== null) bucket.cost = (bucket.cost ?? 0) + event.estimatedCostUsd;
|
|
707
|
+
if (event.tokensPerSec !== null) {
|
|
708
|
+
bucket.rateSum += event.tokensPerSec;
|
|
709
|
+
bucket.rateCount += 1;
|
|
710
|
+
}
|
|
711
|
+
groups.set(name, bucket);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const indexed of buckets) {
|
|
715
|
+
const source =
|
|
716
|
+
dimension === "model"
|
|
717
|
+
? indexed.dimModel
|
|
718
|
+
: dimension === "provider"
|
|
719
|
+
? indexed.dimProvider
|
|
720
|
+
: dimension === "client"
|
|
721
|
+
? indexed.dimClient
|
|
722
|
+
: indexed.dimClientIp;
|
|
723
|
+
for (const [raw, entry] of Object.entries(source)) {
|
|
724
|
+
if (raw.length === 0) continue;
|
|
725
|
+
const name = dimension === "client" ? clientNameFromUserAgent(raw) : raw;
|
|
726
|
+
const bucket = groups.get(name) ?? emptyBreakdown(name);
|
|
727
|
+
bucket.requests += entry.requests;
|
|
728
|
+
bucket.input += entry.inputTokens;
|
|
729
|
+
bucket.output += entry.outputTokens;
|
|
730
|
+
bucket.cached += entry.cachedTokens;
|
|
731
|
+
bucket.errors += entry.errors;
|
|
732
|
+
bucket.cost = (bucket.cost ?? 0) + entry.estimatedCostUsd;
|
|
733
|
+
bucket.rateSum += entry.tokensPerSecSum;
|
|
734
|
+
bucket.rateCount += entry.tokensPerSecCount;
|
|
735
|
+
groups.set(name, bucket);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const mapped = [...groups.values()]
|
|
739
|
+
.map((bucket) => ({
|
|
740
|
+
name: bucket.name,
|
|
741
|
+
requests: bucket.requests,
|
|
742
|
+
input: bucket.input,
|
|
743
|
+
output: bucket.output,
|
|
744
|
+
cached: bucket.cached,
|
|
745
|
+
total: bucket.input + bucket.output,
|
|
746
|
+
errors: bucket.errors,
|
|
747
|
+
costUsd: bucket.cost,
|
|
748
|
+
cacheHitRate: bucket.input > 0 ? (bucket.cached / bucket.input) * 100 : 0,
|
|
749
|
+
avgTokensPerSec: bucket.rateCount > 0 ? bucket.rateSum / bucket.rateCount : 0,
|
|
750
|
+
}))
|
|
751
|
+
.sort((a, b) => b.requests - a.requests)
|
|
752
|
+
.slice(0, 100);
|
|
753
|
+
if (dimension === "client_ip") {
|
|
754
|
+
// Same fail-closed gate as the request list: storage keeps the raw
|
|
755
|
+
// address, only presentation masks it.
|
|
756
|
+
const hideClientIp = await shouldMaskClientIp(this.preferences, tenantId);
|
|
757
|
+
if (!hideClientIp) return { dimension, rows: mapped };
|
|
758
|
+
// Grouping happened on the raw address, so masking can collapse distinct
|
|
759
|
+
// hosts into one display name (`203.0.113.7` and `203.0.113.9` both become
|
|
760
|
+
// `203.0.113.xxx`). Re-aggregate by the masked name: two rows the operator
|
|
761
|
+
// cannot tell apart are one row, and the totals must not be understated.
|
|
762
|
+
const merged = new Map<string, (typeof mapped)[number]>();
|
|
763
|
+
for (const row of mapped) {
|
|
764
|
+
const masked = maskClientIp(row.name);
|
|
765
|
+
const existing = merged.get(masked);
|
|
766
|
+
if (existing === undefined) {
|
|
767
|
+
merged.set(masked, { ...row, name: masked });
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const input = existing.input + row.input;
|
|
771
|
+
const output = existing.output + row.output;
|
|
772
|
+
merged.set(masked, {
|
|
773
|
+
...existing,
|
|
774
|
+
requests: existing.requests + row.requests,
|
|
775
|
+
input,
|
|
776
|
+
output,
|
|
777
|
+
cached: existing.cached + row.cached,
|
|
778
|
+
total: input + output,
|
|
779
|
+
errors: existing.errors + row.errors,
|
|
780
|
+
costUsd:
|
|
781
|
+
existing.costUsd === null && row.costUsd === null
|
|
782
|
+
? null
|
|
783
|
+
: (existing.costUsd ?? 0) + (row.costUsd ?? 0),
|
|
784
|
+
cacheHitRate: input > 0 ? (existing.cached + row.cached) / input * 100 : 0,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
dimension,
|
|
789
|
+
rows: [...merged.values()].sort((a, b) => b.requests - a.requests),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
if (dimension !== "key") return { dimension, rows: mapped };
|
|
793
|
+
const ids = [...new Set(mapped.map((row) => row.name))];
|
|
794
|
+
const labels = ids.length > 0 ? await this.apiKeyLabels(tenantId, ids) : new Map<string, string>();
|
|
795
|
+
return {
|
|
796
|
+
dimension,
|
|
797
|
+
rows: mapped.map((row) => {
|
|
798
|
+
const label = labels.get(row.name);
|
|
799
|
+
return label === undefined ? row : { ...row, label };
|
|
800
|
+
}),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private async apiKeyLabels(tenantId: string, ids: readonly string[]): Promise<Map<string, string>> {
|
|
805
|
+
const unique = [...new Set(ids)];
|
|
806
|
+
if (unique.length === 0) return new Map<string, string>();
|
|
807
|
+
const keyRows = await this.db
|
|
808
|
+
.select({ id: apiKeys.id, label: apiKeys.label })
|
|
809
|
+
.from(apiKeys)
|
|
810
|
+
.where(and(inArray(apiKeys.id, [...unique]), eq(apiKeys.tenantId, tenantId)));
|
|
811
|
+
return new Map(keyRows.map((keyRow) => [keyRow.id, keyRow.label] as const));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async usageCache(tenantId: string, period: string): Promise<CachedResult<UsageCacheResponse>> {
|
|
815
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, period, "cache"), () =>
|
|
816
|
+
this.computeCache(tenantId, period),
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private async computeCache(tenantId: string, period: string): Promise<UsageCacheResponse> {
|
|
821
|
+
const events = await this.periodEvents(tenantId, period);
|
|
822
|
+
let inputTokens = 0;
|
|
823
|
+
let cachedTokens = 0;
|
|
824
|
+
let outputTokens = 0;
|
|
825
|
+
for (const event of events) {
|
|
826
|
+
inputTokens += event.inputTokens ?? 0;
|
|
827
|
+
outputTokens += event.outputTokens ?? 0;
|
|
828
|
+
if (event.cachedInputTokens !== null && event.cachedInputTokens > 0) {
|
|
829
|
+
cachedTokens += event.inputTokens ?? 0;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const tokenTotal = inputTokens + outputTokens;
|
|
833
|
+
return {
|
|
834
|
+
period,
|
|
835
|
+
inputTokens,
|
|
836
|
+
cachedTokens,
|
|
837
|
+
cacheWriteTokens: 0,
|
|
838
|
+
hitRate: tokenTotal > 0 ? (cachedTokens / tokenTotal) * 100 : 0,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async usageRequests(
|
|
843
|
+
tenantId: string,
|
|
844
|
+
period: string,
|
|
845
|
+
limit: number,
|
|
846
|
+
httpStatus?: number,
|
|
847
|
+
): Promise<CachedResult<UsageRequestsResponse>> {
|
|
848
|
+
// Ask the reader for `limit` rows instead of folding the whole window and
|
|
849
|
+
// slicing it. It stops at the newest file that fills the page, so the
|
|
850
|
+
// console's most-polled endpoint no longer parses the retention window just
|
|
851
|
+
// to show 50 rows.
|
|
852
|
+
const since = periodStartDate(period);
|
|
853
|
+
const events = await queryTelemetryEvents({
|
|
854
|
+
tenantId,
|
|
855
|
+
limit,
|
|
856
|
+
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
857
|
+
...(since === undefined ? {} : { since }),
|
|
858
|
+
});
|
|
859
|
+
const rows = events.slice(0, limit);
|
|
860
|
+
const hideClientIp = await shouldMaskClientIp(this.preferences, tenantId);
|
|
861
|
+
// Cached like its siblings, so a repeated poll costs a `304` instead of
|
|
862
|
+
// another read. The mask flag is part of the key: it is a privacy setting,
|
|
863
|
+
// so a page must never be served from an entry computed under the other
|
|
864
|
+
// value. The rows were already read above — this bounds the label lookup
|
|
865
|
+
// and the mapping, not the telemetry read.
|
|
866
|
+
return this.readThroughWithEtag(
|
|
867
|
+
this.usageCacheKey(
|
|
868
|
+
tenantId,
|
|
869
|
+
`${period}:${limit}:${httpStatus ?? "*"}:${hideClientIp}`,
|
|
870
|
+
"requests",
|
|
871
|
+
),
|
|
872
|
+
async () => {
|
|
873
|
+
const keyIds = [...new Set(rows.map((row) => row.apiKeyId).filter((id): id is string => id !== null))];
|
|
874
|
+
const labels = await this.apiKeyLabels(tenantId, keyIds);
|
|
875
|
+
// One indexed read for the whole page, not one per row: presence lives
|
|
876
|
+
// on the payload row so the list can render it without inflating a
|
|
877
|
+
// frame for every row it shows.
|
|
878
|
+
const requestIds = [...new Set(rows.map((row) => row.requestId).filter((id): id is string => id !== null))];
|
|
879
|
+
const stages = await this.payloadStages(tenantId, requestIds);
|
|
880
|
+
return {
|
|
881
|
+
items: rows.map((row) => {
|
|
882
|
+
const item = mapUsageRequestItem(row, hideClientIp);
|
|
883
|
+
const label = row.apiKeyId ? labels.get(row.apiKeyId) : undefined;
|
|
884
|
+
const labeled = label === undefined ? item : { ...item, apiKeyLabel: label };
|
|
885
|
+
const stage = stages.get(row.requestId);
|
|
886
|
+
// Omitted rather than zero when the row is unknown: absence means
|
|
887
|
+
// "no payload was captured", and 0 would say the same thing while
|
|
888
|
+
// claiming it had been measured.
|
|
889
|
+
return stage === undefined ? labeled : { ...labeled, payloadStages: stage };
|
|
890
|
+
}),
|
|
891
|
+
};
|
|
892
|
+
},
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Captured-stage bitmask per request, newest payload row winning.
|
|
898
|
+
* One indexed read for a whole page. Presence is on the payload row
|
|
899
|
+
* specifically so the list never has to inflate a frame to answer "does this
|
|
900
|
+
* panel have content".
|
|
901
|
+
*/
|
|
902
|
+
private async payloadStages(tenantId: string, requestIds: readonly string[]): Promise<Map<string, number>> {
|
|
903
|
+
if (requestIds.length === 0) return new Map();
|
|
904
|
+
const rows = await this.db
|
|
905
|
+
.select({ requestId: telemetryPayloads.requestId, stages: telemetryPayloads.stages })
|
|
906
|
+
.from(telemetryPayloads)
|
|
907
|
+
.where(
|
|
908
|
+
and(
|
|
909
|
+
eq(telemetryPayloads.tenantId, tenantId),
|
|
910
|
+
inArray(telemetryPayloads.requestId, [...requestIds]),
|
|
911
|
+
),
|
|
912
|
+
)
|
|
913
|
+
.orderBy(desc(telemetryPayloads.capturedAt));
|
|
914
|
+
const byRequest = new Map<string, number>();
|
|
915
|
+
for (const row of rows) {
|
|
916
|
+
if (row.requestId === null || row.stages === null) continue;
|
|
917
|
+
// Ordered newest-first, so the first hit is the one the drawer reads.
|
|
918
|
+
if (!byRequest.has(row.requestId)) byRequest.set(row.requestId, row.stages);
|
|
919
|
+
}
|
|
920
|
+
return byRequest;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async usageRequestDetail(
|
|
924
|
+
tenantId: string,
|
|
925
|
+
requestId: string,
|
|
926
|
+
): Promise<UsageRequestDetail | undefined> {
|
|
927
|
+
const rows = await queryTelemetryEvents({ tenantId, requestId });
|
|
928
|
+
const event = rows[0];
|
|
929
|
+
if (!event) return undefined;
|
|
930
|
+
// The label, proxy, and mask lookups are independent: run them together
|
|
931
|
+
// instead of three sequential round trips.
|
|
932
|
+
const [hideClientIp, keyLabels, proxy] = await Promise.all([
|
|
933
|
+
shouldMaskClientIp(this.preferences, tenantId),
|
|
934
|
+
event.apiKeyId ? this.apiKeyLabels(tenantId, [event.apiKeyId]) : undefined,
|
|
935
|
+
event.networkPoolId ? resolveProxyLabel(this.db, tenantId, event.networkPoolId) : ("direct" as const),
|
|
936
|
+
]);
|
|
937
|
+
const item = mapUsageRequestItem(event, hideClientIp);
|
|
938
|
+
const keyLabel = event.apiKeyId ? keyLabels?.get(event.apiKeyId) : undefined;
|
|
939
|
+
const labeled = keyLabel === undefined ? item : { ...item, apiKeyLabel: keyLabel };
|
|
940
|
+
const presented = { ...labeled, proxy };
|
|
941
|
+
// Filter the payload table directly instead of joining `telemetry_events`
|
|
942
|
+
// only to re-apply the tenant predicate: `telemetry_payloads` carries
|
|
943
|
+
// `tenant_id` itself, so the join added a second table's rows to filter for
|
|
944
|
+
// no additional constraint. This matches `getEvent` above and uses
|
|
945
|
+
// `telemetry_payloads_tenant_request_idx`.
|
|
946
|
+
const linkedPayloadRows = await this.db
|
|
947
|
+
.select({ payload: telemetryPayloads })
|
|
948
|
+
.from(telemetryPayloads)
|
|
949
|
+
.where(
|
|
950
|
+
and(
|
|
951
|
+
eq(telemetryPayloads.requestId, requestId),
|
|
952
|
+
eq(telemetryPayloads.tenantId, tenantId),
|
|
953
|
+
),
|
|
954
|
+
)
|
|
955
|
+
.orderBy(desc(telemetryPayloads.capturedAt))
|
|
956
|
+
.limit(1);
|
|
957
|
+
const payloadRow = linkedPayloadRows[0]?.payload;
|
|
958
|
+
const captured = await readCapturedBodies(payloadRow);
|
|
959
|
+
if (!captured) {
|
|
960
|
+
return { ...presented, payloads: null };
|
|
961
|
+
}
|
|
962
|
+
return {
|
|
963
|
+
...presented,
|
|
964
|
+
payloads: {
|
|
965
|
+
...(captured.request !== undefined ? { request: captured.request } : {}),
|
|
966
|
+
...(captured.response !== undefined ? { response: captured.response } : {}),
|
|
967
|
+
...(captured.clientResponse !== undefined ? { clientResponse: captured.clientResponse } : {}),
|
|
968
|
+
...(captured.providerRequest !== undefined
|
|
969
|
+
? { providerRequest: captured.providerRequest }
|
|
970
|
+
: {}),
|
|
971
|
+
...(captured.providerResponse !== undefined
|
|
972
|
+
? { providerResponse: captured.providerResponse }
|
|
973
|
+
: {}),
|
|
974
|
+
},
|
|
975
|
+
...(captured.signals === undefined ? {} : { payloadSignals: captured.signals }),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async listEvents(
|
|
980
|
+
tenantId: string,
|
|
981
|
+
limit: number,
|
|
982
|
+
cursor?: string,
|
|
983
|
+
): Promise<TelemetryEventListPage> {
|
|
984
|
+
const before = decodeDatedCursor(cursor);
|
|
985
|
+
// The cursor becomes an `until` bound inside the query, so `limit` applies
|
|
986
|
+
// to every page and not only the first. Filtering after the read forced the
|
|
987
|
+
// previous shape: a cursor page had to fetch the whole window to know where
|
|
988
|
+
// it ended, so `limit` was dropped the moment a cursor appeared.
|
|
989
|
+
//
|
|
990
|
+
// `until` is inclusive while the cursor is exclusive, so rows at the exact
|
|
991
|
+
// cursor timestamp are still compared by id here — the cheap part of the
|
|
992
|
+
// predicate, now over one timestamp's worth of rows instead of the window.
|
|
993
|
+
const events = await queryTelemetryEvents({
|
|
994
|
+
tenantId,
|
|
995
|
+
limit: limit + 1,
|
|
996
|
+
...(before === undefined ? {} : { until: new Date(before.createdAt) }),
|
|
997
|
+
});
|
|
998
|
+
const rows = (before
|
|
999
|
+
? events.filter((event) => event.createdAt < before.createdAt || (event.createdAt === before.createdAt && event.id < before.id))
|
|
1000
|
+
: events
|
|
1001
|
+
).slice(0, limit + 1);
|
|
1002
|
+
const overflow = rows.length > limit;
|
|
1003
|
+
const trimmed = overflow ? rows.slice(0, limit) : rows;
|
|
1004
|
+
const entries = trimmed.map(mapTelemetryEventRow);
|
|
1005
|
+
const lastEntry = entries[entries.length - 1];
|
|
1006
|
+
const nextCursor =
|
|
1007
|
+
overflow && lastEntry !== undefined
|
|
1008
|
+
? encodeCursor({ createdAt: lastEntry.createdAt, id: lastEntry.id })
|
|
1009
|
+
: undefined;
|
|
1010
|
+
return nextCursor ? { entries, nextCursor } : { entries };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async getEvent(
|
|
1014
|
+
tenantId: string,
|
|
1015
|
+
eventId: string,
|
|
1016
|
+
includePayload = false,
|
|
1017
|
+
createdAt?: string,
|
|
1018
|
+
): Promise<TelemetryEventDetail | undefined> {
|
|
1019
|
+
// Matched by `id`, which is the field the list cursor encodes — not by
|
|
1020
|
+
// `requestId`, which is a different column that merely defaults to the
|
|
1021
|
+
// same value. Predicating on it lets the reader stop at the first hit
|
|
1022
|
+
// instead of parsing the retention window to find one row.
|
|
1023
|
+
const events = await queryTelemetryEvents({ tenantId, id: eventId });
|
|
1024
|
+
const event = events.find(
|
|
1025
|
+
(candidate) =>
|
|
1026
|
+
candidate.id === eventId && (createdAt === undefined || candidate.createdAt === createdAt),
|
|
1027
|
+
);
|
|
1028
|
+
if (!event) return undefined;
|
|
1029
|
+
const base = mapTelemetryEventRow(event);
|
|
1030
|
+
if (!includePayload) return base;
|
|
1031
|
+
const payloadRows = await this.db
|
|
1032
|
+
.select()
|
|
1033
|
+
.from(telemetryPayloads)
|
|
1034
|
+
.where(
|
|
1035
|
+
and(
|
|
1036
|
+
eq(telemetryPayloads.requestId, event.requestId),
|
|
1037
|
+
eq(telemetryPayloads.tenantId, tenantId),
|
|
1038
|
+
),
|
|
1039
|
+
)
|
|
1040
|
+
.orderBy(desc(telemetryPayloads.capturedAt))
|
|
1041
|
+
.limit(1);
|
|
1042
|
+
const payloadRow = payloadRows[0];
|
|
1043
|
+
if (!payloadRow) return base;
|
|
1044
|
+
const captured = await readCapturedBodies(payloadRow);
|
|
1045
|
+
if (!captured) return base;
|
|
1046
|
+
return {
|
|
1047
|
+
...base,
|
|
1048
|
+
payload: {
|
|
1049
|
+
...(captured.request !== undefined ? { requestBody: captured.request } : {}),
|
|
1050
|
+
...(captured.response !== undefined ? { responseBody: captured.response } : {}),
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Reads the captured bodies behind one `telemetry_payloads` row.
|
|
1058
|
+
*
|
|
1059
|
+
* The row stores typed frame-reference columns only; every body lives in the
|
|
1060
|
+
* frame file those columns name. Returns `undefined` when there is no row, no
|
|
1061
|
+
* usable reference, or the frame cannot be read — all three mean "nothing to
|
|
1062
|
+
* show", and the caller renders the request without a payload section. The raw
|
|
1063
|
+
* reference is never returned to a caller, so it cannot reach the dashboard.
|
|
1064
|
+
*/
|
|
1065
|
+
async function readCapturedBodies(
|
|
1066
|
+
row:
|
|
1067
|
+
| {
|
|
1068
|
+
readonly storage: string;
|
|
1069
|
+
readonly file: string;
|
|
1070
|
+
readonly offset: number;
|
|
1071
|
+
readonly length: number;
|
|
1072
|
+
readonly checksum: string;
|
|
1073
|
+
readonly version: number;
|
|
1074
|
+
}
|
|
1075
|
+
| null
|
|
1076
|
+
| undefined,
|
|
1077
|
+
): Promise<
|
|
1078
|
+
| {
|
|
1079
|
+
request: unknown;
|
|
1080
|
+
response: unknown;
|
|
1081
|
+
clientResponse: unknown;
|
|
1082
|
+
providerRequest: unknown;
|
|
1083
|
+
providerResponse: unknown;
|
|
1084
|
+
signals?: { toolCalls: number; images: number; attachments: number };
|
|
1085
|
+
}
|
|
1086
|
+
| undefined
|
|
1087
|
+
> {
|
|
1088
|
+
if (!row) return undefined;
|
|
1089
|
+
const reference = payloadReferenceFromRow(row);
|
|
1090
|
+
if (!reference) return undefined;
|
|
1091
|
+
try {
|
|
1092
|
+
const stored = await readPayloadFrame(reference);
|
|
1093
|
+
if (!stored || typeof stored !== "object") return undefined;
|
|
1094
|
+
const record = stored as Record<string, unknown>;
|
|
1095
|
+
const bodies = {
|
|
1096
|
+
request: record["request_body"] ?? undefined,
|
|
1097
|
+
response: record["response_body"] ?? undefined,
|
|
1098
|
+
clientResponse: record["client_response_body"] ?? undefined,
|
|
1099
|
+
providerRequest: record["provider_request_body"] ?? undefined,
|
|
1100
|
+
providerResponse: record["provider_response_body"] ?? undefined,
|
|
1101
|
+
};
|
|
1102
|
+
const signals =
|
|
1103
|
+
typeof record["signals"] === "object" &&
|
|
1104
|
+
record["signals"] !== null &&
|
|
1105
|
+
!Array.isArray(record["signals"])
|
|
1106
|
+
? (record["signals"] as { toolCalls: number; images: number; attachments: number })
|
|
1107
|
+
: undefined;
|
|
1108
|
+
return {
|
|
1109
|
+
...bodies,
|
|
1110
|
+
...(signals === undefined ? {} : { signals }),
|
|
1111
|
+
};
|
|
1112
|
+
} catch {
|
|
1113
|
+
return undefined;
|
|
1114
|
+
}
|
|
1115
|
+
}
|