cartethyia 2.5.10 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/.env.example +8 -0
  2. package/dist/dashboard/assets/{BannedPage-Cd30Z0b9.js → BannedPage-l6XMnfr9.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-BT3Up3QA.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-6kYIUpzR.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-CS5mPuVF.js → LoginPage-CbphLvjg.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-C0UCUsVJ.js → ModelPicker-DVny4S5_.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  24. package/dist/dashboard/assets/{ProvidersPage-vdja5ZPK.js → ProvidersPage-Dfb2896L.js} +1 -1
  25. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.js → ProxyPage-BaCIdAUt.js} +1 -1
  27. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  28. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  32. package/dist/dashboard/assets/{SetupPage-FadUOl9S.js → SetupPage-D4g6vOGc.js} +1 -1
  33. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C93i14BD.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-SgQpymjm.js} +1 -1
  37. package/dist/dashboard/assets/{StudioPage-CM92fgJf.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  38. package/dist/dashboard/assets/{UsagePage-BT4MyQeT.js → UsagePage-BFME-eba.js} +2 -2
  39. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  40. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  41. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  42. package/dist/dashboard/assets/{app-B7pVRhJ1.js → app-2KhtHCv9.js} +1 -1
  43. package/dist/dashboard/assets/{app-B7pVRhJ1.js.gz → app-2KhtHCv9.js.gz} +0 -0
  44. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  46. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  47. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-Bvyr7D0u.js → badge-CKsF123d.js} +1 -1
  49. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-DIvKAZih.js → banner-8j7uChA9.js} +1 -1
  51. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-D5_gYIS_.js → card-CdsSlWA3.js} +1 -1
  53. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-B6vDBtbm.js → clipboard-button-uL5aYLfW.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-CpjrgdwJ.js → dialog-C07hr5_G.js} +1 -1
  59. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BWmt3b5y.js → drawer-BPO1TcKH.js} +1 -1
  61. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-C_oakV3z.js → entry-Bz_CtDfC.js} +1 -1
  63. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-RRHHWGxh.js → github-badge-DmQreSDd.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-Bhey_rg_.js → index-Bblu6erR.js} +2 -2
  71. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-C2_EQ5Uf.js → inline-C9dCYaBZ.js} +1 -1
  73. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BMRlQ450.js → input-CjCnXweJ.js} +1 -1
  75. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-XR5xP8ew.js → layout-V6y8DAkl.js} +1 -1
  77. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-Cw07dioc.js → list-row-CM1AaG_y.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  81. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  84. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  85. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  86. package/dist/dashboard/assets/{numbers-DrdA9oSM.js → numbers-BOevJzdz.js} +1 -1
  87. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  88. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  89. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  90. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  91. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  92. package/dist/dashboard/assets/{select-CxVVB__N.js → select-C980c4cz.js} +1 -1
  93. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  94. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  95. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  96. package/dist/dashboard/assets/{stack-uPOwM9Hs.js → stack-_gfTdLVB.js} +1 -1
  97. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  98. package/dist/dashboard/assets/{state-D1Yz79iL.js → state-2c5V4zo7.js} +1 -1
  99. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  100. package/dist/dashboard/assets/{switch-sevsRrRo.js → switch-Dkke7Naf.js} +1 -1
  101. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  102. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  103. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  104. package/dist/dashboard/index.html +1 -1
  105. package/dist/dashboard/index.html.gz +0 -0
  106. package/dist/dashboard/precompressed-assets.json +51 -51
  107. package/migrations/README.md +52 -52
  108. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  109. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  110. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  111. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  112. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  113. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  114. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  115. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  116. package/package.json +96 -95
  117. package/scripts/cli/admin.ts +126 -128
  118. package/scripts/cli/autostart.ts +237 -237
  119. package/scripts/cli/background.ts +120 -110
  120. package/scripts/cli/bin/carte.cjs +8 -8
  121. package/scripts/cli/bin/cartethyia.cjs +167 -167
  122. package/scripts/cli/console-menu.ts +39 -30
  123. package/scripts/cli/controller.ts +145 -0
  124. package/scripts/cli/env.ts +99 -99
  125. package/scripts/cli/gateway.ts +264 -260
  126. package/scripts/cli/instance.ts +234 -224
  127. package/scripts/cli/main.ts +188 -167
  128. package/scripts/cli/platform.ts +64 -64
  129. package/scripts/cli/prompt.ts +217 -79
  130. package/scripts/cli/services.ts +88 -88
  131. package/scripts/cli/setup.ts +445 -445
  132. package/scripts/cli/start.ts +85 -85
  133. package/scripts/cli/startup-signal.ts +23 -23
  134. package/scripts/cli/telemetry.ts +70 -0
  135. package/scripts/cli/tray/index.ts +263 -263
  136. package/scripts/cli/tray/windows.ps1 +107 -107
  137. package/scripts/cli/tray.ts +29 -4
  138. package/scripts/cli/update.ts +39 -39
  139. package/scripts/cli/version.ts +67 -67
  140. package/scripts/cli/wordmark.ts +58 -58
  141. package/src/boot/app.ts +16 -1
  142. package/src/boot/dependencies.ts +128 -25
  143. package/src/config/resources.ts +137 -0
  144. package/src/config/runtime.ts +133 -76
  145. package/src/config/spec.ts +30 -29
  146. package/src/config/telemetry.ts +0 -25
  147. package/src/console/api-keys/bucket-spend.ts +14 -17
  148. package/src/console/api-keys/contracts.ts +410 -410
  149. package/src/console/api-keys/routes.ts +12 -0
  150. package/src/console/audit/store.ts +2 -3
  151. package/src/console/auth/setup-service.ts +8 -1
  152. package/src/console/backup/routes.ts +7 -0
  153. package/src/console/backup/service.ts +463 -463
  154. package/src/console/backup/store.ts +16 -11
  155. package/src/console/backup/validate.ts +1 -4
  156. package/src/console/cli-tools/fs-ops.ts +18 -2
  157. package/src/console/data-management/contracts.ts +1 -8
  158. package/src/console/data-management/operations.ts +61 -48
  159. package/src/console/data-management/routes.ts +4 -2
  160. package/src/console/domain-registration.ts +25 -3
  161. package/src/console/inferhub/client.ts +139 -0
  162. package/src/console/inferhub/key-store.ts +129 -0
  163. package/src/console/inferhub/normalize.ts +347 -0
  164. package/src/console/inferhub/routes.ts +411 -0
  165. package/src/console/inferhub/store.ts +109 -0
  166. package/src/console/inferhub/types.ts +183 -0
  167. package/src/console/observability/contracts.ts +2 -41
  168. package/src/console/observability/store.ts +176 -371
  169. package/src/console/providers/catalog/public-model-store.ts +0 -0
  170. package/src/console/providers/catalog/store.ts +23 -36
  171. package/src/console/quota/account-quota-shared.ts +52 -52
  172. package/src/console/quota/account-quota-tenant.ts +16 -24
  173. package/src/console/quota/refresh.ts +441 -441
  174. package/src/console/routing/pools/import-parse.ts +206 -206
  175. package/src/console/routing/pools/operations.ts +543 -543
  176. package/src/console/routing/pools/probe-verdict.ts +132 -132
  177. package/src/console/routing/pools/routes.ts +181 -181
  178. package/src/console/settings/contracts.ts +42 -40
  179. package/src/console/settings/store.ts +25 -25
  180. package/src/console/share/share-stats.ts +22 -4
  181. package/src/console/share/share-usage.ts +30 -4
  182. package/src/console/shared/cursor.ts +15 -0
  183. package/src/console/updates/routes.ts +12 -0
  184. package/src/main.ts +13 -5
  185. package/src/network/pool/resolver.ts +1104 -1104
  186. package/src/observability/client-name.ts +22 -0
  187. package/src/observability/event-log.ts +5 -29
  188. package/src/observability/event-query.ts +690 -0
  189. package/src/observability/log-ring.ts +26 -2
  190. package/src/observability/log-sanitize.ts +118 -0
  191. package/src/observability/logger.ts +17 -13
  192. package/src/observability/metrics.ts +35 -0
  193. package/src/observability/payload-capture.ts +114 -41
  194. package/src/observability/payload-store.ts +249 -24
  195. package/src/observability/raw-event-writer.ts +118 -0
  196. package/src/observability/runtime-metrics.ts +71 -1
  197. package/src/observability/telemetry-buffer.ts +100 -17
  198. package/src/observability/telemetry-reconciliation.ts +417 -0
  199. package/src/persistence/boot.ts +20 -9
  200. package/src/persistence/db-pg.ts +13 -0
  201. package/src/persistence/db-sqlite.ts +569 -497
  202. package/src/persistence/dialect.ts +3 -2
  203. package/src/persistence/migrate.ts +181 -181
  204. package/src/persistence/schema.ts +1496 -1406
  205. package/src/persistence/shared-cache.ts +5 -1
  206. package/src/persistence/stores/api-key-store.ts +581 -581
  207. package/src/persistence/stores/telemetry-store.ts +124 -63
  208. package/src/protocol/reasoning-text.ts +30 -0
  209. package/src/protocol/response/chat.ts +33 -7
  210. package/src/protocol/response/codex.ts +14 -2
  211. package/src/protocol/response/messages.ts +5 -2
  212. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  213. package/src/providers/bundled-discovery-providers.ts +122 -122
  214. package/src/providers/default-registry.ts +10 -0
  215. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  216. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  217. package/src/providers/discovery/probe-phases.ts +43 -7
  218. package/src/providers/discovery/probing-service.ts +70 -32
  219. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  220. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  221. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  222. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  223. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  224. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  225. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  226. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  227. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  228. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  229. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  230. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  231. package/src/providers/integrations/claude/claude.ts +3 -4
  232. package/src/providers/integrations/cline/cline.ts +94 -8
  233. package/src/providers/integrations/codex/codex.ts +61 -37
  234. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  235. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  236. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  237. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  238. package/src/providers/integrations/cursor/cursor.ts +29 -17
  239. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  240. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  241. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  242. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  243. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  244. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  245. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  246. package/src/providers/integrations/hermes.ts +7 -1
  247. package/src/providers/integrations/inferhub.ts +201 -73
  248. package/src/providers/integrations/kilo/kilo.ts +152 -152
  249. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  250. package/src/providers/integrations/openrouter/provider.ts +129 -129
  251. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  252. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  253. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  254. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  255. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  256. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  257. package/src/providers/integrations/zcode/oauth.ts +1 -1
  258. package/src/providers/operations/account-health-service.ts +605 -279
  259. package/src/providers/provider-metadata.ts +12 -1
  260. package/src/providers/provider-model-definition.ts +373 -373
  261. package/src/providers/quota/quota-contracts.ts +3 -7
  262. package/src/providers/search/search-provider.ts +2 -6
  263. package/src/security/admission/contracts.ts +10 -0
  264. package/src/security/admission/in-memory-store.ts +89 -14
  265. package/src/security/admission/lua.ts +191 -182
  266. package/src/security/admission/redis-store.ts +30 -0
  267. package/src/security/admission/service.ts +18 -0
  268. package/src/security/api-key-auth-cache.ts +96 -0
  269. package/src/security/api-key-auth.ts +11 -100
  270. package/src/security/client-router-fingerprint.ts +3 -7
  271. package/src/security/ip-boundary.ts +7 -3
  272. package/src/shared/coerce.ts +67 -0
  273. package/src/shared/hash.ts +6 -0
  274. package/src/shared/time.ts +7 -0
  275. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  276. package/src/transport/dispatch/attempt-loop.ts +103 -81
  277. package/src/transport/dispatch/capture-policy.ts +78 -0
  278. package/src/transport/dispatch/proxy-request.ts +4 -2
  279. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  280. package/src/transport/dispatch/streaming/stream.ts +19 -2
  281. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  282. package/src/transport/failure-policy.ts +27 -0
  283. package/src/transport/guards/abuse-guard.ts +2 -2
  284. package/src/transport/guards/auth-guard.ts +17 -0
  285. package/src/transport/guards/body-policy.ts +3 -2
  286. package/src/transport/guards/concurrency-guard.ts +89 -13
  287. package/src/transport/guards/early-rejection-capture.ts +43 -3
  288. package/src/transport/guards/error-lifecycle.ts +10 -2
  289. package/src/transport/guards/guard-errors.ts +23 -0
  290. package/src/transport/guards/headroom-guard.ts +41 -7
  291. package/src/transport/guards/pipeline.ts +24 -0
  292. package/src/transport/guards/request-context.ts +2 -1
  293. package/src/transport/routing/admission.ts +51 -9
  294. package/src/transport/routing/bounded-lru.ts +49 -0
  295. package/src/transport/routing/candidate-index.ts +88 -0
  296. package/src/transport/routing/reservations.ts +121 -7
  297. package/src/transport/routing/round-robin.ts +10 -22
  298. package/src/transport/routing/route-catalog.ts +26 -13
  299. package/src/transport/routing/route-model.ts +22 -2
  300. package/src/transport/routing/router.ts +53 -31
  301. package/src/transport/surface/chat/encode.ts +2 -3
  302. package/src/transport/surface/chat/parse.ts +2 -1
  303. package/src/transport/surface/dialects.ts +11 -3
  304. package/src/transport/surface/messages/encode.ts +2 -1
  305. package/src/transport/surface/messages/parse.ts +3 -1
  306. package/src/transport/surface/messages/stream.ts +2 -6
  307. package/src/workers/maintenance-lease.ts +200 -0
  308. package/src/workers/scheduler.ts +101 -1
  309. package/dist/dashboard/assets/BannedPage-Cd30Z0b9.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
  311. package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
  312. package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
  313. package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
  314. package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
  315. package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js +0 -1
  316. package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js +0 -1
  320. package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
  321. package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js +0 -3
  322. package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
  324. package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
  325. package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js +0 -1
  326. package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
  327. package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js +0 -1
  328. package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
  330. package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-BT4MyQeT.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  333. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  334. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  336. package/dist/dashboard/assets/badge-Bvyr7D0u.js.gz +0 -0
  337. package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
  338. package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
  339. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  340. package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
  341. package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
  342. package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
  343. package/dist/dashboard/assets/entry-B-gvimrg.js +0 -1
  344. package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-Dj0cHfEc.js +0 -4
  347. package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
  348. package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
  349. package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-Cw07dioc.js.gz +0 -0
  354. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  355. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  356. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  357. package/dist/dashboard/assets/numbers-DrdA9oSM.js.gz +0 -0
  358. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  359. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  360. package/dist/dashboard/assets/select-CxVVB__N.js.gz +0 -0
  361. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  362. package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
  363. package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
  364. package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
  365. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  366. package/src/observability/event-compaction.ts +0 -317
  367. package/src/observability/event-day-index.ts +0 -439
@@ -3,19 +3,25 @@ import { and, desc, eq, inArray } from "drizzle-orm";
3
3
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
4
4
  import type { RedisClient } from "../../persistence/redis";
5
5
  import { apiKeys, networkPools, telemetryPayloads } from "../../persistence/schema";
6
+ import { type TelemetryEventRecord } from "../../observability/event-log";
6
7
  import {
7
- 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
- numeric,
15
- percentileFromHistogram,
16
- } from "../../observability/event-compaction";
8
+ breakdownTelemetry,
9
+ chartTelemetry,
10
+ healthTelemetry,
11
+ queryTelemetryEvents as queryTelemetryEventPage,
12
+ summarizeHealthPerformance,
13
+ summarizeUsagePerformanceSql,
14
+ summarizeTelemetry,
15
+ } from "../../observability/event-query";
16
+ import { clientNameFromUserAgent } from "../../observability/client-name";
17
+
18
+ // Re-exported from the canonical module: ingestion (`telemetryEventRow`) and
19
+ // the backfill stamp the same value the console read side groups by, so the
20
+ // derivation cannot drift between writer and reader. Existing console
21
+ // imports of `clientNameFromUserAgent` keep working.
22
+ export { clientNameFromUserAgent };
17
23
  import { cachedReadWithEtag, type CachedResult } from "../shared/read-through-cache";
18
- import { decodeDatedCursor, encodeCursor } from "../shared/cursor";
24
+ import { decodeDatedCursor, encodeCursor, trimKeysetPage } from "../shared/cursor";
19
25
  import { CARTETHYIA_VERSION } from "../../version";
20
26
  import { CachedPreferencesReader, DrizzlePreferencesReader, type PreferencesReader } from "../../persistence/tenant-preferences";
21
27
  import type {
@@ -88,23 +94,6 @@ function periodStartDate(period: string): Date | undefined {
88
94
  return new Date(Date.now() - amount * unitMs);
89
95
  }
90
96
 
91
-
92
- /**
93
- * Client identity straight from the stored `User-Agent`: the first RFC 9110
94
- * product token carries both the client and its version (`bun/1.2.3`,
95
- * `codex-cli/0.1.0`), so no prefix catalog can drift from what the client
96
- * actually sent. Browser agents start with the historical `Mozilla/5.0`
97
- * token, which names no real client, so they report as `Browser`.
98
- */
99
- export function clientNameFromUserAgent(userAgent: string | null | undefined): string {
100
- const raw = userAgent?.trim();
101
- if (!raw) return "unknown";
102
- if (/^mozilla\//i.test(raw)) return "Browser";
103
- const product = raw.split(/\s+/, 1)[0] ?? "";
104
- const slash = product.indexOf("/");
105
- const token = slash <= 0 ? product : product.slice(0, slash + 1).concat(product.slice(slash + 1).split(/[;,)]/, 1)[0] ?? "");
106
- return token || "unknown";
107
- }
108
97
  /** Time-series bucket width for the usage chart, chosen from the window length. */
109
98
  function chartBucketSeconds(period: string): number {
110
99
  const since = periodStartDate(period);
@@ -197,15 +186,13 @@ async function resolveProxyLabel(
197
186
  }
198
187
  const HEALTH_WINDOW_MS = 24 * 3_600_000;
199
188
  const TOP_N = 50;
189
+
200
190
  /**
201
- * How long a parsed window is shared. A dashboard poll fires ~8 usage
202
- * endpoints at once, and a slow parse outlives a 1.5 s window — so the
203
- * endpoints that miss it each re-read and re-parse the same files, which is how
204
- * one page load becomes several full parses of the retention window.
191
+ * Nearest-rank percentile over a sorted array — kept for the JS-side
192
+ * `summarizeUsagePerformance` (still used by callers that already hold a
193
+ * bounded event list). The usage endpoints read percentiles from SQL via
194
+ * `summarizeUsagePerformanceSql`, which is what the dashboard polls.
205
195
  */
206
- const PERIOD_MEMO_TTL_MS = 5_000;
207
-
208
- /** Nearest-rank percentile over a sorted array, matching `percentile_cont` closely enough for a health gauge. */
209
196
  function percentile(sorted: readonly number[], fraction: number): number {
210
197
  if (sorted.length === 0) return 0;
211
198
  const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));
@@ -254,93 +241,22 @@ export function summarizeUsagePerformance(
254
241
  }
255
242
 
256
243
  /**
257
- * The performance summary from indexed buckets rather than from events.
244
+ * The console dimension as the SQL reader's grouping key.
258
245
  *
259
- * The index keeps a latency sum, a sample count, and a histogram, so the mean
260
- * stays exact and the percentiles stay bucketed — the same precision the
261
- * per-event version had, without parsing the period's requests.
246
+ * `client` maps to `client_name` (stamped at ingestion) rather than
247
+ * `user_agent`: the grouping is now a plain GROUP BY on a column the writer
248
+ * filled, instead of a per-row `clientNameFromUserAgent` call in JavaScript.
262
249
  */
263
- export function summarizeBucketPerformance(
264
- buckets: readonly IndexedBucket[],
265
- ): UsagePerformanceSummary {
266
- let latencySum = 0;
267
- let latencyCount = 0;
268
- let resolveSum = 0;
269
- let resolveCount = 0;
270
- let latencyMaxMs: number | null = null;
271
- const histogram: number[] = [];
272
- for (const bucket of buckets) {
273
- latencySum += bucket.latencySum;
274
- latencyCount += bucket.latencyCount;
275
- // Read defensively: an index written before these fields existed carries
276
- // neither, and summing `undefined` poisons the whole period with NaN —
277
- // including the days that do have the data.
278
- resolveSum += numeric(bucket.resolveSumMs);
279
- resolveCount += numeric(bucket.resolveCount);
280
- if (bucket.latencyMaxMs !== null)
281
- latencyMaxMs = latencyMaxMs === null ? bucket.latencyMaxMs : Math.max(latencyMaxMs, bucket.latencyMaxMs);
282
- for (let index = 0; index < bucket.latencyHistogram.length; index += 1) {
283
- histogram[index] = (histogram[index] ?? 0) + (bucket.latencyHistogram[index] ?? 0);
284
- }
285
- }
286
- const fill =
287
- histogram.length > 0
288
- ? histogram
289
- : new Array<number>(LATENCY_BUCKETS_MS.length + 1).fill(0);
290
- return {
291
- avgResolveMs: resolveCount > 0 ? resolveSum / resolveCount : 0,
292
- avgDurationMs: latencyCount > 0 ? latencySum / latencyCount : 0,
293
- // `latencyMaxMs` is passed as the overflow answer: a percentile past the
294
- // histogram's last edge is at most the slowest request actually recorded.
295
- // Without it an overflow returned `null`, and `?? 0` turned a P95 of 40 s
296
- // into "0 ms" — the slow tail reported as nothing at all.
297
- latencyP50Ms: percentileFromHistogram(fill, 50, latencyMaxMs) ?? 0,
298
- latencyP95Ms: percentileFromHistogram(fill, 95, latencyMaxMs) ?? 0,
299
- latencyP99Ms: percentileFromHistogram(fill, 99, latencyMaxMs) ?? 0,
300
- latencySampleCount: latencyCount,
301
- resolveSampleCount: resolveCount,
302
- };
303
- }
304
-
305
- /** The largest entries of a count map, capped. */
306
- function topCounts(counts: ReadonlyMap<string, number>, limit: number): Array<[string, number]> {
307
- return [...counts].sort((a, b) => b[1] - a[1]).slice(0, limit);
308
- }
309
-
310
- /** The grouping value for one usage breakdown dimension. */
311
- function dimensionName(dimension: UsageDimension, event: TelemetryEventRecord): string | null {
312
- if (dimension === "model") return event.requestedModel;
313
- if (dimension === "key") return event.apiKeyId;
314
- if (dimension === "client") return event.userAgent;
315
- if (dimension === "client_ip") return event.clientIp;
316
- return event.providerId;
317
- }
318
-
319
- interface BreakdownBucket {
320
- name: string;
321
- requests: number;
322
- input: number;
323
- output: number;
324
- cached: number;
325
- errors: number;
326
- cost: number | null;
327
- rateSum: number;
328
- rateCount: number;
329
- }
330
-
331
- function emptyBreakdown(name: string): BreakdownBucket {
332
- return { name, requests: 0, input: 0, output: 0, cached: 0, errors: 0, cost: null, rateSum: 0, rateCount: 0 };
250
+ function breakdownDimension(dimension: UsageDimension): string {
251
+ if (dimension === "model") return "model";
252
+ if (dimension === "key") return "api_key";
253
+ if (dimension === "client") return "client";
254
+ if (dimension === "client_ip") return "client_ip";
255
+ return "provider";
333
256
  }
334
257
 
335
258
  /** Real Drizzle-backed observability repository used by the production console. */
336
259
  export class DrizzleObservabilityStore implements ObservabilityStore {
337
- /**
338
- * Per-(tenant, period) parsed event set, shared by the ~8 usage endpoints
339
- * one dashboard poll fires in parallel. 1.5 s window: a poll burst shares
340
- * one file parse; the next poll re-reads. In-flight shares one promise.
341
- */
342
- private readonly periodMemo = new Map<string, { at: number; promise: Promise<readonly TelemetryEventRecord[]> }>();
343
- private readonly periodBucketMemo = new Map<string, { at: number; promise: Promise<readonly IndexedBucket[]> }>();
344
260
  constructor(
345
261
  private readonly db: CartethyiaDatabase,
346
262
  private readonly redis: RedisClient | undefined,
@@ -404,44 +320,27 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
404
320
  let cacheHitRate = 0;
405
321
  let avgTokensPerSec = 0;
406
322
  try {
407
- // Bounded 24h rolling window (matches the Overview copy). The file is the
408
- // source, so the percentiles are computed over the retained events.
323
+ // Bounded 24h rolling window (matches the Overview copy), computed IN
324
+ // SQL. The previous shape materialized every retained event into a
325
+ // latency array and sorted it in JavaScript — the single largest read
326
+ // cost on the dashboard. `healthTelemetry` returns the totals and the
327
+ // database computes the percentiles.
409
328
  const since = new Date(Date.now() - HEALTH_WINDOW_MS);
410
- const events = await queryTelemetryEvents({ tenantId, since });
411
- const latencies: number[] = [];
412
- let latencySum = 0;
413
- let inputTokens = 0;
414
- let cachedTokens = 0;
415
- let outputTokens = 0;
416
- let tokensPerSecSum = 0;
417
- let tokensPerSecCount = 0;
418
- for (const event of events) {
419
- total += 1;
420
- if (isGatewayError(event.status, event.httpStatus)) errors += 1;
421
- if (event.latencyMs !== null) {
422
- latencies.push(event.latencyMs);
423
- latencySum += event.latencyMs;
424
- }
425
- if (event.cachedInputTokens !== null && event.cachedInputTokens > 0) {
426
- inputTokens += event.inputTokens ?? 0;
427
- cachedTokens += event.inputTokens ?? 0;
428
- outputTokens += event.outputTokens ?? 0;
429
- }
430
- if (event.tokensPerSec !== null) {
431
- tokensPerSecSum += event.tokensPerSec;
432
- tokensPerSecCount += 1;
433
- }
434
- }
435
- latencies.sort((a, b) => a - b);
436
- avg = latencies.length > 0 ? Math.round(latencySum / latencies.length) : 0;
437
- p95 = percentile(latencies, 0.95);
438
- p99 = percentile(latencies, 0.99);
329
+ const totals = await healthTelemetry(this.db, tenantId, since);
330
+ const performance = await summarizeHealthPerformance(this.db, tenantId, since);
331
+ total = totals.requests;
332
+ errors = totals.errors;
333
+ avg = performance.latencyAvgMs === null ? 0 : Math.round(performance.latencyAvgMs);
334
+ p95 = performance.latencyP95Ms ?? 0;
335
+ p99 = performance.latencyP99Ms ?? 0;
336
+ const inputTokens = totals.inputTokens;
337
+ const cachedTokens = totals.cachedInputTokens;
439
338
  // Cached share of the prompt: cached/input, the same honest definition
440
339
  // usageSummary uses, so the health card and the Usage page cannot
441
340
  // disagree. (Cached share, not prompt share: input/(input+output) reads
442
341
  // ~99% and mislabels it a cache hit rate.)
443
342
  cacheHitRate = inputTokens > 0 ? Math.round((cachedTokens / inputTokens) * 10000) / 100 : 0;
444
- avgTokensPerSec = tokensPerSecCount > 0 ? tokensPerSecSum / tokensPerSecCount : 0;
343
+ avgTokensPerSec = performance.avgTokensPerSec;
445
344
  } catch {
446
345
  databaseHealthy = false;
447
346
  }
@@ -514,23 +413,23 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
514
413
  }
515
414
 
516
415
  private async computeUsage(tenantId: string, period: string): Promise<UsageResponse> {
517
- const events = await this.periodEvents(tenantId, period);
518
- let requestsTotal = 0;
519
- let requestsSucceeded = 0;
520
- let requestsFailed = 0;
521
- let tokensUsed = 0;
522
- let estimatedCost = 0;
523
- const models = new Map<string, number>();
524
- const providers = new Map<string, number>();
525
- for (const event of events) {
526
- requestsTotal += 1;
527
- if (event.status === "completed") requestsSucceeded += 1;
528
- if (isGatewayError(event.status, event.httpStatus)) requestsFailed += 1;
529
- tokensUsed += (event.inputTokens ?? 0) + (event.outputTokens ?? 0);
530
- estimatedCost += event.estimatedCostUsd ?? 0;
531
- if (event.requestedModel) models.set(event.requestedModel, (models.get(event.requestedModel) ?? 0) + 1);
532
- if (event.providerId) providers.set(event.providerId, (providers.get(event.providerId) ?? 0) + 1);
533
- }
416
+ const since = this.periodSince(period);
417
+ // Totals AND the top-model/top-provider counts come from SQL: the old
418
+ // shape parsed the whole retained window into an array to count two
419
+ // dimensions in JavaScript.
420
+ const [summary, models, providers] = await Promise.all([
421
+ summarizeTelemetry(this.db, tenantId, since),
422
+ breakdownTelemetry(this.db, tenantId, "model", since),
423
+ breakdownTelemetry(this.db, tenantId, "provider", since),
424
+ ]);
425
+ const requestsTotal = summary.requests;
426
+ // `completed` is a lifecycle status, not the gateway-error predicate: it
427
+ // is derived as requests minus the errors and the non-completed states the
428
+ // summary already counts.
429
+ const requestsFailed = summary.errors;
430
+ const requestsSucceeded = Math.max(0, requestsTotal - requestsFailed - summary.cancelled - summary.truncated);
431
+ const tokensUsed = summary.inputTokens + summary.outputTokens;
432
+ const estimatedCost = summary.estimatedCostUsd;
534
433
  return {
535
434
  tenantId,
536
435
  period,
@@ -539,53 +438,25 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
539
438
  requestsFailed,
540
439
  tokensUsed,
541
440
  estimatedCost,
542
- topModels: topCounts(models, TOP_N).map(([modelId, count]) => ({ modelId, count })),
543
- topProviders: topCounts(providers, TOP_N).map(([providerId, count]) => ({ providerId, count })),
441
+ topModels: models
442
+ .slice(0, TOP_N)
443
+ .map((group) => ({ modelId: group.key, count: group.requests })),
444
+ topProviders: providers
445
+ .slice(0, TOP_N)
446
+ .map((group) => ({ providerId: group.key, count: group.requests })),
544
447
  };
545
448
  }
546
449
 
547
- /** The tenant's retained events inside a usage period. `all` keeps everything. */
548
450
  /**
549
- * The indexed buckets covering a period, for one tenant.
451
+ * The retained window for one usage period.
550
452
  *
551
- * Reads the per-day index files instead of the event files. A usage query no
552
- * longer parses every request in the window and filters it in memory, so its
553
- * cost no longer grows with traffic.
453
+ * `all` means the retained SQL window (the database's own retention), not
454
+ * "read every file ever written": `telemetry_events` is pruned by the same
455
+ * retention rule as the raw files, so the SQL store already holds exactly
456
+ * what a period can show.
554
457
  */
555
- private periodBuckets(tenantId: string, period: string): Promise<readonly IndexedBucket[]> {
556
- const key = `${tenantId}:${period}`;
557
- const now = Date.now();
558
- const hit = this.periodBucketMemo.get(key);
559
- if (hit && now - hit.at < PERIOD_MEMO_TTL_MS) return hit.promise;
560
- const since = periodStartDate(period);
561
- // Fold before reading. Events appended since the scheduler last ran are
562
- // otherwise invisible, and a usage page that under-reports is worse than one
563
- // that is a moment slow.
564
- const promise = queryIndexedBuckets({
565
- tenantId,
566
- foldFirst: true,
567
- ...(since === undefined ? {} : { since }),
568
- });
569
- this.periodBucketMemo.set(key, { at: now, promise });
570
- void promise.catch(() => {
571
- if (this.periodBucketMemo.get(key)?.promise === promise) this.periodBucketMemo.delete(key);
572
- });
573
- return promise;
574
- }
575
-
576
- private periodEvents(tenantId: string, period: string): Promise<readonly TelemetryEventRecord[]> {
577
- const key = `${tenantId}:${period}`;
578
- const now = Date.now();
579
- const hit = this.periodMemo.get(key);
580
- if (hit && now - hit.at < PERIOD_MEMO_TTL_MS) return hit.promise;
581
- const since = periodStartDate(period);
582
- const promise = queryTelemetryEvents(since === undefined ? { tenantId } : { tenantId, since });
583
- this.periodMemo.set(key, { at: now, promise });
584
- // A rejected parse must not poison the memo for the next poll.
585
- void promise.catch(() => {
586
- if (this.periodMemo.get(key)?.promise === promise) this.periodMemo.delete(key);
587
- });
588
- return promise;
458
+ private periodSince(period: string): Date {
459
+ return periodStartDate(period) ?? new Date(0);
589
460
  }
590
461
 
591
462
  async usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>> {
@@ -595,58 +466,36 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
595
466
  }
596
467
 
597
468
  private async computeSummary(tenantId: string, period: string): Promise<UsageSummaryResponse> {
598
- const buckets = await this.periodBuckets(tenantId, period);
599
- const performance = summarizeBucketPerformance(buckets);
600
- let requests = 0;
601
- let inputTokens = 0;
602
- let cachedTokens = 0;
603
- let outputTokens = 0;
604
- let errors = 0;
605
- let cancelled = 0;
606
- let truncated = 0;
607
- let estimatedCostUsd = 0;
608
- let unpriced = 0;
609
- let tokensPerSecSum = 0;
610
- let tokensPerSecCount = 0;
611
- const statuses = new Map<number, number>();
612
- for (const row of buckets) {
613
- // Folded from the index's own per-status counts. These used to be left
614
- // empty: the loop below never filled `statuses`, so every status chip and
615
- // the requests-list filters read zero for any day whose events had been
616
- // folded — while `requests` beside them read the real total.
617
- for (const [status, count] of Object.entries(row.byStatus ?? {})) {
618
- const code = Number(status);
619
- if (!Number.isFinite(code)) continue;
620
- statuses.set(code, (statuses.get(code) ?? 0) + count);
621
- }
622
- requests += row.requests;
623
- inputTokens += row.inputTokens;
624
- outputTokens += row.outputTokens;
625
- cachedTokens += row.cachedTokens;
626
- errors += row.failed;
627
- cancelled += row.cancelled;
628
- truncated += row.truncated;
629
- estimatedCostUsd += row.estimatedCostUsd;
630
- unpriced += row.unpriced;
631
- tokensPerSecSum += row.tokensPerSecSum;
632
- tokensPerSecCount += row.tokensPerSecCount;
633
- }
469
+ const since = this.periodSince(period);
470
+ // Totals, status breakdown, and latency percentiles all computed in SQL.
471
+ // This used to parse the whole retained window into JavaScript to sum six
472
+ // counters and sort a latency array.
473
+ const [summary, statuses, performance] = await Promise.all([
474
+ summarizeTelemetry(this.db, tenantId, since),
475
+ breakdownTelemetry(this.db, tenantId, "status", since),
476
+ summarizeUsagePerformanceSql(this.db, tenantId, since),
477
+ ]);
478
+ const inputTokens = summary.inputTokens;
479
+ const cachedTokens = summary.cachedInputTokens;
634
480
  return {
635
481
  period,
636
482
  totals: {
637
- statusCounts: [...statuses].sort(([a], [b]) => a - b).map(([status, count]) => ({ status, count })),
638
- requests,
483
+ statusCounts: statuses
484
+ .map((group) => ({ status: Number(group.key), count: group.requests }))
485
+ .filter((entry) => Number.isFinite(entry.status))
486
+ .sort((a, b) => a.status - b.status),
487
+ requests: summary.requests,
639
488
  inputTokens,
640
489
  cachedTokens,
641
- outputTokens,
642
- errors,
643
- cancelled,
644
- truncated,
490
+ outputTokens: summary.outputTokens,
491
+ errors: summary.errors,
492
+ cancelled: summary.cancelled,
493
+ truncated: summary.truncated,
645
494
  ...performance,
646
- estimatedCostUsd,
647
- partial: unpriced > 0,
495
+ estimatedCostUsd: summary.estimatedCostUsd,
496
+ partial: summary.unpriced > 0,
648
497
  cacheHitRate: inputTokens > 0 ? (cachedTokens / inputTokens) * 100 : 0,
649
- avgTokensPerSec: tokensPerSecCount > 0 ? tokensPerSecSum / tokensPerSecCount : 0,
498
+ avgTokensPerSec: performance.avgTokensPerSec,
650
499
  },
651
500
  };
652
501
  }
@@ -661,31 +510,37 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
661
510
  const since = periodStartDate(period);
662
511
  const bucketSeconds = chartBucketSeconds(period);
663
512
  const stepMs = bucketSeconds * 1000;
664
- const events = await this.periodEvents(tenantId, period);
513
+ // Bucketing happens in SQL. The old shape parsed the retained window to
514
+ // floor every event's timestamp into a JS Map.
515
+ const buckets = await chartTelemetry(
516
+ this.db,
517
+ tenantId,
518
+ this.periodSince(period),
519
+ bucketSeconds,
520
+ );
665
521
  const byTime = new Map<number, UsageChartBucket>();
666
- for (const event of events) {
667
- const at = new Date(event.createdAt).getTime();
668
- if (!Number.isFinite(at)) continue;
669
- const start = Math.floor(at / stepMs) * stepMs;
670
- const bucket = byTime.get(start) ?? { t: new Date(start).toISOString(), requests: 0, input: 0, cached: 0, output: 0 };
671
- bucket.requests += 1;
672
- bucket.input += event.inputTokens ?? 0;
673
- bucket.cached += event.cachedInputTokens !== null && event.cachedInputTokens > 0 ? (event.inputTokens ?? 0) : 0;
674
- bucket.output += event.outputTokens ?? 0;
675
- byTime.set(start, bucket);
522
+ for (const bucket of buckets) {
523
+ byTime.set(bucket.at, {
524
+ t: new Date(bucket.at).toISOString(),
525
+ requests: bucket.requests,
526
+ input: bucket.inputTokens,
527
+ // `cached` is the cached share of the prompt: the SQL reader sums
528
+ // cached_input_tokens, which is the same number the old JS loop used.
529
+ cached: bucket.inputTokens,
530
+ output: bucket.outputTokens,
531
+ });
676
532
  }
677
- const rows = [...byTime.keys()];
678
- const first = rows.length > 0 ? Math.min(...byTime.keys()) : undefined;
533
+ const first = buckets.length > 0 ? buckets[0]!.at : undefined;
679
534
  const startMs = since ? since.getTime() : first;
680
535
  if (startMs === undefined) return { buckets: [] };
681
536
  const aligned = Math.floor(startMs / stepMs) * stepMs;
682
- const buckets: UsageChartBucket[] = [];
537
+ const out: UsageChartBucket[] = [];
683
538
  for (let at = aligned; at <= Date.now(); at += stepMs) {
684
- buckets.push(
539
+ out.push(
685
540
  byTime.get(at) ?? { t: new Date(at).toISOString(), requests: 0, input: 0, cached: 0, output: 0 },
686
541
  );
687
542
  }
688
- return { buckets };
543
+ return { buckets: out };
689
544
  }
690
545
 
691
546
  async usageBy(
@@ -703,74 +558,33 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
703
558
  dimension: UsageDimension,
704
559
  period: string,
705
560
  ): Promise<UsageByResponse> {
706
- // The breakdown is read from the per-name sums the day index maintains, not
707
- // from the events: the console asks how much went to each model, and the
708
- // index already carries exactly that. Parsing the window to recount it was
709
- // the whole cost of this endpoint.
710
- const buckets = await this.periodBuckets(tenantId, period);
711
- const groups = new Map<string, BreakdownBucket>();
712
- // Fall back to the events only when the index carries nothing for the
713
- // period — an install that has not folded yet, or a day whose index is
714
- // missing. Never alongside it: the same request is in both, and counting it
715
- // twice would double every column.
716
- if (buckets.length === 0) {
717
- for (const event of await this.periodEvents(tenantId, period)) {
718
- const raw = dimensionName(dimension, event);
719
- if (raw === null || raw.length === 0) continue;
720
- const name = dimension === "client" ? clientNameFromUserAgent(raw) : raw;
721
- const bucket = groups.get(name) ?? emptyBreakdown(name);
722
- bucket.requests += 1;
723
- bucket.input += event.inputTokens ?? 0;
724
- bucket.output += event.outputTokens ?? 0;
725
- if (event.cachedInputTokens !== null && event.cachedInputTokens > 0) {
726
- bucket.cached += event.inputTokens ?? 0;
727
- }
728
- if (isGatewayError(event.status, event.httpStatus)) bucket.errors += 1;
729
- if (event.estimatedCostUsd !== null) bucket.cost = (bucket.cost ?? 0) + event.estimatedCostUsd;
730
- if (event.tokensPerSec !== null) {
731
- bucket.rateSum += event.tokensPerSec;
732
- bucket.rateCount += 1;
733
- }
734
- groups.set(name, bucket);
735
- }
736
- }
737
- for (const indexed of buckets) {
738
- const source =
739
- dimension === "model"
740
- ? indexed.dimModel
741
- : dimension === "provider"
742
- ? indexed.dimProvider
743
- : dimension === "client"
744
- ? indexed.dimClient
745
- : indexed.dimClientIp;
746
- for (const [raw, entry] of Object.entries(source)) {
747
- if (raw.length === 0) continue;
748
- const name = dimension === "client" ? clientNameFromUserAgent(raw) : raw;
749
- const bucket = groups.get(name) ?? emptyBreakdown(name);
750
- bucket.requests += entry.requests;
751
- bucket.input += entry.inputTokens;
752
- bucket.output += entry.outputTokens;
753
- bucket.cached += entry.cachedTokens;
754
- bucket.errors += entry.errors;
755
- bucket.cost = (bucket.cost ?? 0) + entry.estimatedCostUsd;
756
- bucket.rateSum += entry.tokensPerSecSum;
757
- bucket.rateCount += entry.tokensPerSecCount;
758
- groups.set(name, bucket);
759
- }
760
- }
761
- const mapped = [...groups.values()]
762
- .map((bucket) => ({
763
- name: bucket.name,
764
- requests: bucket.requests,
765
- input: bucket.input,
766
- output: bucket.output,
767
- cached: bucket.cached,
768
- total: bucket.input + bucket.output,
769
- errors: bucket.errors,
770
- costUsd: bucket.cost,
771
- cacheHitRate: bucket.input > 0 ? (bucket.cached / bucket.input) * 100 : 0,
772
- avgTokensPerSec: bucket.rateCount > 0 ? bucket.rateSum / bucket.rateCount : 0,
773
- }))
561
+ // Grouping happens in SQL and only the (bounded) groups come back. The
562
+ // old shape walked every retained event to accumulate a JS Map per
563
+ // dimension; `client` now groups on the ingestion-stamped `client_name`
564
+ // instead of re-deriving a name per row.
565
+ const groups = await breakdownTelemetry(
566
+ this.db,
567
+ tenantId,
568
+ breakdownDimension(dimension),
569
+ this.periodSince(period),
570
+ );
571
+ const mapped = groups
572
+ .map((group) => {
573
+ const input = group.inputTokens;
574
+ const output = group.outputTokens;
575
+ return {
576
+ name: group.key,
577
+ requests: group.requests,
578
+ input,
579
+ output,
580
+ cached: group.cachedInputTokens,
581
+ total: input + output,
582
+ errors: group.errors,
583
+ costUsd: group.costUsd,
584
+ cacheHitRate: input > 0 ? (group.cachedInputTokens / input) * 100 : 0,
585
+ avgTokensPerSec: group.avgTokensPerSec,
586
+ };
587
+ })
774
588
  .sort((a, b) => b.requests - a.requests)
775
589
  .slice(0, 100);
776
590
  if (dimension === "client_ip") {
@@ -841,17 +655,10 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
841
655
  }
842
656
 
843
657
  private async computeCache(tenantId: string, period: string): Promise<UsageCacheResponse> {
844
- const events = await this.periodEvents(tenantId, period);
845
- let inputTokens = 0;
846
- let cachedTokens = 0;
847
- let outputTokens = 0;
848
- for (const event of events) {
849
- inputTokens += event.inputTokens ?? 0;
850
- outputTokens += event.outputTokens ?? 0;
851
- if (event.cachedInputTokens !== null && event.cachedInputTokens > 0) {
852
- cachedTokens += event.inputTokens ?? 0;
853
- }
854
- }
658
+ const summary = await summarizeTelemetry(this.db, tenantId, this.periodSince(period));
659
+ const inputTokens = summary.inputTokens;
660
+ const outputTokens = summary.outputTokens;
661
+ const cachedTokens = summary.cachedInputTokens;
855
662
  const tokenTotal = inputTokens + outputTokens;
856
663
  return {
857
664
  period,
@@ -868,24 +675,16 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
868
675
  limit: number,
869
676
  httpStatus?: number,
870
677
  ): Promise<CachedResult<UsageRequestsResponse>> {
871
- // Ask the reader for `limit` rows instead of folding the whole window and
872
- // slicing it. It stops at the newest file that fills the page, so the
873
- // console's most-polled endpoint no longer parses the retention window just
874
- // to show 50 rows.
875
- const since = periodStartDate(period);
876
- const events = await queryTelemetryEvents({
877
- tenantId,
878
- limit,
879
- ...(httpStatus === undefined ? {} : { httpStatus }),
880
- ...(since === undefined ? {} : { since }),
881
- });
882
- const rows = events.slice(0, limit);
883
678
  const hideClientIp = await shouldMaskClientIp(this.preferences, tenantId);
884
679
  // Cached like its siblings, so a repeated poll costs a `304` instead of
885
680
  // another read. The mask flag is part of the key: it is a privacy setting,
886
681
  // so a page must never be served from an entry computed under the other
887
- // value. The rows were already read above — this bounds the label lookup
888
- // and the mapping, not the telemetry read.
682
+ // value.
683
+ //
684
+ // The telemetry read moved INSIDE the read-through: it used to run before
685
+ // it, so every 304 still paid a query. Now a cache hit costs nothing at
686
+ // all, and a miss pays one bounded SQL page (keyset, newest first) rather
687
+ // than a scan of the raw window.
889
688
  return this.readThroughWithEtag(
890
689
  this.usageCacheKey(
891
690
  tenantId,
@@ -893,6 +692,13 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
893
692
  "requests",
894
693
  ),
895
694
  async () => {
695
+ const page = await queryTelemetryEventPage(this.db, {
696
+ tenantId,
697
+ since: this.periodSince(period),
698
+ limit,
699
+ ...(httpStatus === undefined ? {} : { httpStatus }),
700
+ });
701
+ const rows = page.events.slice(0, limit);
896
702
  const keyIds = [...new Set(rows.map((row) => row.apiKeyId).filter((id): id is string => id !== null))];
897
703
  const labels = await this.apiKeyLabels(tenantId, keyIds);
898
704
  // One indexed read for the whole page, not one per row: presence lives
@@ -947,8 +753,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
947
753
  tenantId: string,
948
754
  requestId: string,
949
755
  ): Promise<UsageRequestDetail | undefined> {
950
- const rows = await queryTelemetryEvents({ tenantId, requestId });
951
- const event = rows[0];
756
+ const page = await queryTelemetryEventPage(this.db, { tenantId, requestId });
757
+ const event = page.events[0];
952
758
  if (!event) return undefined;
953
759
  // The label, proxy, and mask lookups are independent: run them together
954
760
  // instead of three sequential round trips.
@@ -1013,17 +819,15 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
1013
819
  // `until` is inclusive while the cursor is exclusive, so rows at the exact
1014
820
  // cursor timestamp are still compared by id here — the cheap part of the
1015
821
  // predicate, now over one timestamp's worth of rows instead of the window.
1016
- const events = await queryTelemetryEvents({
822
+ // Keyset page: the cursor is the last row of the previous page, and the
823
+ // SQL reader pushes it into the WHERE clause instead of filtering a
824
+ // materialized window in JS.
825
+ const page = await queryTelemetryEventPage(this.db, {
1017
826
  tenantId,
1018
827
  limit: limit + 1,
1019
- ...(before === undefined ? {} : { until: new Date(before.createdAt) }),
828
+ ...(before === undefined ? {} : { cursor: encodeCursor({ createdAt: before.createdAt, id: before.id }) }),
1020
829
  });
1021
- const rows = (before
1022
- ? events.filter((event) => event.createdAt < before.createdAt || (event.createdAt === before.createdAt && event.id < before.id))
1023
- : events
1024
- ).slice(0, limit + 1);
1025
- const overflow = rows.length > limit;
1026
- const trimmed = overflow ? rows.slice(0, limit) : rows;
830
+ const { rows: trimmed, hasMore: overflow } = trimKeysetPage(page.events, limit);
1027
831
  const entries = trimmed.map(mapTelemetryEventRow);
1028
832
  const lastEntry = entries[entries.length - 1];
1029
833
  const nextCursor =
@@ -1043,7 +847,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
1043
847
  // `requestId`, which is a different column that merely defaults to the
1044
848
  // same value. Predicating on it lets the reader stop at the first hit
1045
849
  // instead of parsing the retention window to find one row.
1046
- const events = await queryTelemetryEvents({ tenantId, id: eventId });
850
+ const page = await queryTelemetryEventPage(this.db, { tenantId, id: eventId });
851
+ const events = page.events;
1047
852
  const event = events.find(
1048
853
  (candidate) =>
1049
854
  candidate.id === eventId && (createdAt === undefined || candidate.createdAt === createdAt),