cartethyia 2.5.3 → 2.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  60. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  61. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  62. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  63. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  64. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
  66. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  70. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
  74. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
  76. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
  78. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  79. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  80. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
  82. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
  84. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  85. package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
  86. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
  88. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  89. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  90. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
  94. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
  96. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
  98. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
  100. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
  102. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  103. package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
  104. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  105. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  106. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  107. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  108. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  109. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  110. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  111. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
  112. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  113. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  114. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  115. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  116. package/dist/dashboard/index.html +1 -1
  117. package/dist/dashboard/index.html.gz +0 -0
  118. package/dist/dashboard/precompressed-assets.json +57 -57
  119. package/migrations/README.md +52 -52
  120. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  121. package/package.json +96 -95
  122. package/scripts/cli/admin.ts +125 -0
  123. package/scripts/cli/autostart.ts +237 -0
  124. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  125. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
  126. package/scripts/cli/doctor.ts +37 -0
  127. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  128. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  129. package/scripts/cli/instance.ts +224 -0
  130. package/scripts/cli/main.ts +88 -18
  131. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  132. package/scripts/cli/port.ts +100 -0
  133. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  134. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  135. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  136. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  137. package/scripts/cli/tray/index.ts +62 -12
  138. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  139. package/scripts/cli/update.ts +40 -0
  140. package/scripts/cli/version.ts +67 -0
  141. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  142. package/src/boot/app.ts +11 -7
  143. package/src/boot/dependencies.ts +50 -19
  144. package/src/boot/timeout.ts +2 -13
  145. package/src/config/cooldowns.ts +186 -138
  146. package/src/config/network.ts +8 -14
  147. package/src/config/providers.ts +7 -10
  148. package/src/config/runtime.ts +76 -76
  149. package/src/config/security.ts +15 -0
  150. package/src/config/spec.ts +52 -7
  151. package/src/config/telemetry.ts +26 -0
  152. package/src/config/timeouts.ts +0 -6
  153. package/src/console/admin/model-abuse/contracts.ts +28 -6
  154. package/src/console/admin/model-abuse/routes.ts +56 -3
  155. package/src/console/api-keys/bucket-spend.ts +15 -4
  156. package/src/console/api-keys/contracts.ts +410 -414
  157. package/src/console/api-keys/routes.ts +881 -880
  158. package/src/console/auth/credentials.ts +3 -3
  159. package/src/console/auth/setup-service.ts +2 -2
  160. package/src/console/backup/contracts.ts +15 -2
  161. package/src/console/backup/export-endpoint.ts +6 -2
  162. package/src/console/backup/nine-router.ts +2 -5
  163. package/src/console/backup/service.ts +4 -0
  164. package/src/console/backup/store.ts +192 -29
  165. package/src/console/backup/validate.ts +3 -45
  166. package/src/console/cli-tools/fs-ops.ts +487 -487
  167. package/src/console/console-router-guards.ts +1 -1
  168. package/src/console/console-router.ts +8 -0
  169. package/src/console/data-management/contracts.ts +100 -0
  170. package/src/console/data-management/operations.ts +234 -0
  171. package/src/console/data-management/routes.ts +103 -0
  172. package/src/console/domain-registration.ts +45 -1
  173. package/src/console/observability/contracts.ts +524 -485
  174. package/src/console/observability/live.ts +22 -0
  175. package/src/console/observability/store.ts +1138 -1018
  176. package/src/console/observability/usage-dimensions.ts +4 -4
  177. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  178. package/src/console/providers/catalog/contracts.ts +6 -0
  179. package/src/console/providers/catalog/model-operations.ts +5 -2
  180. package/src/console/providers/catalog/provider-operations.ts +24 -4
  181. package/src/console/providers/catalog/public-model-store.ts +8 -11
  182. package/src/console/providers/catalog/routes.ts +7 -0
  183. package/src/console/providers/catalog/store.ts +1553 -1532
  184. package/src/console/providers/oauth/routes.ts +8 -12
  185. package/src/console/quota/account-quota-shared.ts +52 -39
  186. package/src/console/quota/account-quota-tenant.ts +6 -6
  187. package/src/console/quota/quota-view.ts +1 -1
  188. package/src/console/quota/refresh.ts +441 -429
  189. package/src/console/remote-support/contracts.ts +61 -0
  190. package/src/console/remote-support/routes.ts +65 -0
  191. package/src/console/remote-support/service.ts +207 -0
  192. package/src/console/response-compression.ts +0 -3
  193. package/src/console/routing/pools/import-parse.ts +206 -0
  194. package/src/console/routing/pools/operations.ts +543 -501
  195. package/src/console/routing/pools/probe-result.ts +3 -2
  196. package/src/console/routing/pools/probe-verdict.ts +132 -132
  197. package/src/console/routing/pools/routes.ts +181 -164
  198. package/src/console/routing/pools/store.ts +8 -8
  199. package/src/console/settings/contracts.ts +95 -2
  200. package/src/console/settings/store.ts +81 -3
  201. package/src/console/shared/cursor.ts +8 -8
  202. package/src/console/shared/errors.ts +1 -3
  203. package/src/console/updates/routes.ts +16 -0
  204. package/src/main.ts +10 -0
  205. package/src/network/http2-bidi.ts +2 -4
  206. package/src/network/pool/agent.ts +478 -478
  207. package/src/network/pool/selector.ts +5 -4
  208. package/src/network/retry.ts +1 -2
  209. package/src/network/ssrf.ts +5 -4
  210. package/src/observability/event-compaction.ts +317 -0
  211. package/src/observability/event-day-index.ts +440 -0
  212. package/src/observability/event-log.ts +155 -6
  213. package/src/observability/logger.ts +6 -6
  214. package/src/observability/payload-capture.ts +158 -33
  215. package/src/observability/payload-store.ts +82 -61
  216. package/src/observability/redaction.ts +5 -6
  217. package/src/observability/request-notify.ts +43 -0
  218. package/src/observability/telemetry-status.ts +8 -9
  219. package/src/observability/token-speed.ts +5 -4
  220. package/src/persistence/db-mode.ts +7 -7
  221. package/src/persistence/health-event-flush.ts +36 -0
  222. package/src/persistence/readiness.ts +1 -1
  223. package/src/persistence/schema.ts +75 -29
  224. package/src/persistence/shared-cache.ts +1 -2
  225. package/src/persistence/stores/api-key-store.ts +18 -6
  226. package/src/persistence/stores/share-store.ts +4 -3
  227. package/src/persistence/stores/telemetry-store.ts +33 -11
  228. package/src/protocol/canonical-model.ts +11 -3
  229. package/src/protocol/codecs.ts +139 -6
  230. package/src/protocol/gateway-error.ts +6 -4
  231. package/src/protocol/image-clamp.ts +19 -1
  232. package/src/protocol/registry.ts +8 -0
  233. package/src/protocol/request/chat.ts +21 -1
  234. package/src/protocol/request/codex.ts +10 -1
  235. package/src/protocol/request/gemini.ts +9 -2
  236. package/src/protocol/request/messages.ts +18 -7
  237. package/src/protocol/request/responses.ts +9 -1
  238. package/src/protocol/response/chat.ts +34 -4
  239. package/src/protocol/response/codex.ts +7 -4
  240. package/src/protocol/response/dsml.ts +140 -28
  241. package/src/protocol/response/responses.ts +11 -5
  242. package/src/protocol/stream-error-frames.ts +4 -5
  243. package/src/providers/adapters/compatible-adapter.ts +3 -0
  244. package/src/providers/authentication/jwt-validator.ts +9 -10
  245. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  246. package/src/providers/bundled-discovery-providers.ts +122 -119
  247. package/src/providers/configured-openai-providers.ts +4 -4
  248. package/src/providers/default-registry.ts +5 -4
  249. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  250. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  251. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  252. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  253. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  254. package/src/providers/discovery/probe-phases.ts +4 -4
  255. package/src/providers/discovery/probe-wire.ts +6 -13
  256. package/src/providers/discovery/probing-service.ts +1117 -1045
  257. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  258. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  259. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  260. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  261. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  262. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  263. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  264. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  265. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  266. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  267. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  268. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  269. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  270. package/src/providers/integrations/claude/claude.ts +608 -608
  271. package/src/providers/integrations/cline/cline-quota.ts +27 -20
  272. package/src/providers/integrations/cline/cline.ts +537 -539
  273. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  274. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  275. package/src/providers/integrations/codex/codex.ts +2 -29
  276. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  277. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  278. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  279. package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
  280. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  281. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  282. package/src/providers/integrations/cursor/cursor.ts +1333 -960
  283. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  284. package/src/providers/integrations/factory/factory-models.ts +3 -5
  285. package/src/providers/integrations/factory/factory.ts +8 -1
  286. package/src/providers/integrations/gemini.ts +6 -1
  287. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  288. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  289. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  290. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  291. package/src/providers/integrations/grok/grok.ts +6 -7
  292. package/src/providers/integrations/hermes.ts +14 -18
  293. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  294. package/src/providers/integrations/kiro/kiro.ts +1 -1
  295. package/src/providers/integrations/mistral.ts +6 -6
  296. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  297. package/src/providers/integrations/opencode/provider.ts +19 -20
  298. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  299. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  300. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  301. package/src/providers/integrations/qoder/qoder.ts +884 -865
  302. package/src/providers/integrations/shared/connect.ts +6 -6
  303. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  304. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  305. package/src/providers/integrations/trae/trae.ts +2 -2
  306. package/src/providers/integrations/zcode/oauth.ts +6 -1
  307. package/src/providers/operations/account-health-service.ts +75 -27
  308. package/src/providers/operations/account-reset-service.ts +1 -1
  309. package/src/providers/operations/client-versions.ts +6 -6
  310. package/src/providers/operations/custom-cli-headers.ts +3 -4
  311. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  312. package/src/providers/operations/provider-catalog-service.ts +3 -4
  313. package/src/providers/provider-contracts.ts +7 -0
  314. package/src/providers/provider-metadata.ts +10 -8
  315. package/src/providers/provider-registry.ts +6 -7
  316. package/src/providers/provider-usage.ts +1 -1
  317. package/src/providers/quota/quota-support.ts +8 -0
  318. package/src/security/abuse.ts +15 -10
  319. package/src/security/access-control.ts +144 -146
  320. package/src/security/admission/contracts.ts +11 -0
  321. package/src/security/admission/in-memory-store.ts +4 -3
  322. package/src/security/admission/lease-sweep.ts +6 -6
  323. package/src/security/admission/reasons.ts +5 -1
  324. package/src/security/admission/redis-store.ts +211 -210
  325. package/src/security/admission/service.ts +4 -0
  326. package/src/security/api-key-auth.ts +27 -17
  327. package/src/security/ip-boundary.ts +155 -15
  328. package/src/security/model-abuse.ts +366 -94
  329. package/src/security/outbound-headers.ts +3 -4
  330. package/src/shared/sleep.ts +4 -0
  331. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  332. package/src/transport/dispatch/attempt-loop.ts +531 -530
  333. package/src/transport/dispatch/generation.ts +6 -0
  334. package/src/transport/dispatch/proxy-request.ts +24 -1
  335. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  336. package/src/transport/dispatch/stream-bridge.ts +1 -3
  337. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  338. package/src/transport/dispatch/streaming/stream.ts +19 -6
  339. package/src/transport/dispatch/upstream.ts +9 -0
  340. package/src/transport/failure-policy.ts +162 -8
  341. package/src/transport/guards/auth-guard.ts +7 -4
  342. package/src/transport/guards/concurrency-guard.ts +0 -5
  343. package/src/transport/guards/error-lifecycle.ts +13 -6
  344. package/src/transport/guards/pipeline.ts +15 -3
  345. package/src/transport/guards/request-context.ts +8 -2
  346. package/src/transport/request/inflight.ts +5 -6
  347. package/src/transport/request/preparer.ts +9 -8
  348. package/src/transport/request/state.ts +21 -8
  349. package/src/transport/request/tenant-preferences.ts +60 -0
  350. package/src/transport/routing/admission.ts +4 -0
  351. package/src/transport/routing/route-catalog.ts +65 -10
  352. package/src/transport/routing/route-model.ts +46 -14
  353. package/src/transport/routing/router.ts +21 -14
  354. package/src/transport/surface/completion.ts +2 -3
  355. package/src/transport/surface/dialects.ts +5 -6
  356. package/src/transport/surface/responses/encode.ts +38 -0
  357. package/src/transport/surface/responses/parse.ts +4 -0
  358. package/src/transport/translation/capabilities.ts +2 -3
  359. package/src/transport/translation/quirks.ts +3 -3
  360. package/src/workers/buddy-growth-tasks.ts +5 -10
  361. package/src/workers/daily-checkin.ts +17 -23
  362. package/src/workers/quota-refresh-sweep.ts +26 -26
  363. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  364. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
  365. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  366. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  367. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
  368. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  369. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  370. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  371. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
  372. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  373. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  374. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
  375. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  376. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
  377. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  378. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
  379. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  380. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
  381. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  382. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
  383. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  384. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
  385. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  386. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
  387. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  388. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  389. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  390. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  391. package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
  392. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  393. package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
  394. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  395. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  396. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  397. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  398. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  399. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  400. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  401. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  402. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  403. package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
  404. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  405. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  406. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  407. package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
  408. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  409. package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
  410. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  411. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  412. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  413. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  414. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  415. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  416. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  417. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  418. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  419. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  420. package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
  421. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  422. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  423. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  424. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  425. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  426. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  427. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  428. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  429. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  430. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  431. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  432. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  433. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  434. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  435. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  436. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  437. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  438. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  439. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  440. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  441. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  442. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  443. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  444. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  445. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  446. package/scripts/cli/command/admin.ts +0 -65
  447. package/scripts/cli/command/doctor.ts +0 -257
  448. package/scripts/cli/command/update.ts +0 -23
  449. package/scripts/cli/gateway/drain-token.ts +0 -78
  450. package/scripts/cli/gateway/instance-lock.ts +0 -105
  451. package/scripts/cli/gateway/stop.ts +0 -51
  452. package/scripts/cli/support/bun.ts +0 -27
  453. package/scripts/cli/support/version.ts +0 -37
  454. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
  455. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
  456. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -0,0 +1,440 @@
1
+ import { mkdir, open, readFile, rename, stat, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import {
4
+ accumulate,
5
+ emptyBucket,
6
+ mergeBucketsInternal,
7
+ type CompactedBucket,
8
+ } from "./event-compaction";
9
+ import { TELEMETRY_EVENT_DIR, type TelemetryEventRecord } from "./event-log";
10
+
11
+ /**
12
+ * One sidecar index per day, beside that day's append-only event file.
13
+ *
14
+ * The event file grows all day and is never rewritten while it is open. The
15
+ * index is the thing the dashboard reads: it holds the totals and the breakdown
16
+ * counts, maintained incrementally from a byte watermark rather than rebuilt by
17
+ * folding and deleting source files.
18
+ *
19
+ * The watermark is what makes that possible. Deciding which file was still
20
+ * being written by comparing file *names* lets the open file be folded and
21
+ * unlinked mid-write, so its requests never reach the summary. A byte offset
22
+ * has no such ambiguity: everything below it is counted, everything above it is
23
+ * not, and re-reading the tail is idempotent.
24
+ */
25
+
26
+ /**
27
+ * An event whose tenant is known to be present.
28
+ *
29
+ * `tenantId` is optional on the record because a row can arrive without one,
30
+ * but the index is keyed by tenant, so only rows carrying one are folded.
31
+ */
32
+ type FoldedEvent = TelemetryEventRecord & { readonly tenantId: string };
33
+
34
+ /** Marker so a reader can tell an index from anything else in the directory. */
35
+ const INDEX_KIND = "telemetry-day-index" as const;
36
+
37
+ /** Version bumped whenever the shape changes; an unreadable version is ignored. */
38
+ const INDEX_VERSION = 1 as const;
39
+
40
+ /** The event file for one UTC day. */
41
+ export function dayEventFileName(day: string): string {
42
+ return `${day}.events.jsonl`;
43
+ }
44
+
45
+ /** The sidecar index for one UTC day. */
46
+ export function dayIndexFileName(day: string): string {
47
+ return `${day}.index.json`;
48
+ }
49
+
50
+ /** The UTC day (`2026-10-22`) a moment belongs to. */
51
+ export function utcDay(at: Date): string {
52
+ return at.toISOString().slice(0, 10);
53
+ }
54
+
55
+ /**
56
+ * One hour's share of a bucket.
57
+ *
58
+ * The Traffic chart asks how a day was spent, which the totals alone cannot
59
+ * answer. Twenty-four slots per bucket is small — four numbers each — so it is
60
+ * held *inside* the index rather than as separate per-hour files.
61
+ */
62
+ /**
63
+ * One named dimension's share of a bucket.
64
+ *
65
+ * The breakdown asks for tokens, errors and cost per model/provider/client/IP,
66
+ * not merely how many requests each took. Counts alone would leave every
67
+ * breakdown column reading zero, so each named entry carries the same running
68
+ * sums the bucket does.
69
+ */
70
+ export interface IndexedDimension {
71
+ readonly requests: number;
72
+ readonly inputTokens: number;
73
+ readonly cachedTokens: number;
74
+ readonly outputTokens: number;
75
+ readonly errors: number;
76
+ readonly estimatedCostUsd: number;
77
+ readonly tokensPerSecSum: number;
78
+ readonly tokensPerSecCount: number;
79
+ }
80
+
81
+ export interface IndexedHour {
82
+ readonly requests: number;
83
+ readonly inputTokens: number;
84
+ readonly cachedTokens: number;
85
+ readonly outputTokens: number;
86
+ }
87
+
88
+ /** One tenant/key pair's running totals for the day. */
89
+ export interface IndexedBucket extends CompactedBucket {
90
+ readonly tenantId: string;
91
+ readonly apiKeyId: string | null;
92
+ /** Index 0-23, the UTC hour an event fell in. */
93
+ readonly hours: readonly IndexedHour[];
94
+ /** Per-name sums for each breakdown dimension. */
95
+ readonly dimModel: Readonly<Record<string, IndexedDimension>>;
96
+ readonly dimProvider: Readonly<Record<string, IndexedDimension>>;
97
+ readonly dimClient: Readonly<Record<string, IndexedDimension>>;
98
+ readonly dimClientIp: Readonly<Record<string, IndexedDimension>>;
99
+ }
100
+
101
+ /** The on-disk shape of a day index. */
102
+ export interface DayIndex {
103
+ readonly kind: typeof INDEX_KIND;
104
+ readonly version: typeof INDEX_VERSION;
105
+ readonly day: string;
106
+ /**
107
+ * Bytes of the event file already folded in.
108
+ *
109
+ * Everything past this offset is uncounted; a crash between append and index
110
+ * write only ever leaves it behind, never ahead, so the tail is re-read.
111
+ */
112
+ readonly watermarkBytes: number;
113
+ /** Totals per tenant/key. */
114
+ readonly buckets: readonly IndexedBucket[];
115
+ /** Wall-clock of the last fold, for operators reading the file by hand. */
116
+ readonly updatedAt: string;
117
+ }
118
+
119
+ function bucketKey(tenantId: string, apiKeyId: string | null): string {
120
+ return `${tenantId}|${apiKeyId ?? ""}`;
121
+ }
122
+
123
+ /** Twenty-four empty hour slots. */
124
+ function emptyHours(): IndexedHour[] {
125
+ return Array.from({ length: 24 }, () => ({
126
+ requests: 0,
127
+ inputTokens: 0,
128
+ cachedTokens: 0,
129
+ outputTokens: 0,
130
+ }));
131
+ }
132
+
133
+ /** The UTC hour (0-23) of an ISO timestamp. */
134
+ function hourOf(createdAt: string): number {
135
+ const at = Date.parse(createdAt);
136
+ if (!Number.isFinite(at)) return 0;
137
+ return Number.parseInt(createdAt.slice(11, 13), 10) || 0;
138
+ }
139
+
140
+ /** An empty per-name entry. */
141
+ function emptyDimension(): IndexedDimension {
142
+ return {
143
+ requests: 0,
144
+ inputTokens: 0,
145
+ cachedTokens: 0,
146
+ outputTokens: 0,
147
+ errors: 0,
148
+ estimatedCostUsd: 0,
149
+ tokensPerSecSum: 0,
150
+ tokensPerSecCount: 0,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * A cost as a number.
156
+ *
157
+ * Costs arrive as strings from `numeric` columns and from restored rows, and
158
+ * `0 + "0.000966"` concatenates rather than sums. Coerced once, here, so the
159
+ * per-name breakdown cannot accumulate a string the way it did.
160
+ */
161
+ function numericCost(value: unknown): number {
162
+ const asNumber = typeof value === "number" ? value : Number(value);
163
+ return Number.isFinite(asNumber) ? asNumber : 0;
164
+ }
165
+
166
+ /** Folds one event into one named dimension entry. */
167
+ function foldDimension(
168
+ into: Readonly<Record<string, IndexedDimension>> | undefined,
169
+ name: string | null | undefined,
170
+ event: FoldedEvent,
171
+ ): Record<string, IndexedDimension> {
172
+ const out: Record<string, IndexedDimension> = { ...(into ?? {}) };
173
+ const key = typeof name === "string" ? name.trim() : "";
174
+ if (key === "") return out;
175
+ const prior = out[key] ?? emptyDimension();
176
+ const failed = event.status === "failed" ? 1 : 0;
177
+ out[key] = {
178
+ requests: prior.requests + 1,
179
+ inputTokens: prior.inputTokens + (event.inputTokens ?? 0),
180
+ cachedTokens: prior.cachedTokens + (event.cachedInputTokens ?? 0),
181
+ outputTokens: prior.outputTokens + (event.outputTokens ?? 0),
182
+ errors: prior.errors + failed,
183
+ estimatedCostUsd: prior.estimatedCostUsd + numericCost(event.estimatedCostUsd),
184
+ tokensPerSecSum: prior.tokensPerSecSum + (event.tokensPerSec ?? 0),
185
+ tokensPerSecCount: prior.tokensPerSecCount + (event.tokensPerSec === null ? 0 : 1),
186
+ };
187
+ return out;
188
+ }
189
+
190
+ /** Adds one event's tokens into its hour slot. */
191
+ function withHour(
192
+ hours: readonly IndexedHour[],
193
+ event: FoldedEvent,
194
+ ): IndexedHour[] {
195
+ const base = hours.length === 24 ? [...hours] : emptyHours();
196
+ const index = hourOf(event.createdAt);
197
+ const prior = base[index] ?? { requests: 0, inputTokens: 0, cachedTokens: 0, outputTokens: 0 };
198
+ base[index] = {
199
+ requests: prior.requests + 1,
200
+ inputTokens: prior.inputTokens + (event.inputTokens ?? 0),
201
+ cachedTokens: prior.cachedTokens + (event.cachedInputTokens ?? 0),
202
+ outputTokens: prior.outputTokens + (event.outputTokens ?? 0),
203
+ };
204
+ return base;
205
+ }
206
+
207
+ /** An empty index for a day with nothing counted yet. */
208
+ export function emptyDayIndex(day: string): DayIndex {
209
+ return {
210
+ kind: INDEX_KIND,
211
+ version: INDEX_VERSION,
212
+ day,
213
+ watermarkBytes: 0,
214
+ buckets: [],
215
+ updatedAt: new Date().toISOString(),
216
+ };
217
+ }
218
+
219
+ /** True when the index is one this version can read. */
220
+ function isDayIndex(value: unknown): value is DayIndex {
221
+ if (typeof value !== "object" || value === null) return false;
222
+ const candidate = value as Partial<DayIndex>;
223
+ return (
224
+ candidate.kind === INDEX_KIND &&
225
+ candidate.version === INDEX_VERSION &&
226
+ typeof candidate.day === "string" &&
227
+ typeof candidate.watermarkBytes === "number" &&
228
+ Array.isArray(candidate.buckets)
229
+ );
230
+ }
231
+
232
+ /** Reads a day index, or an empty one when it is absent or unreadable. */
233
+ export async function readDayIndex(day: string): Promise<DayIndex> {
234
+ const text = await readFile(join(TELEMETRY_EVENT_DIR, dayIndexFileName(day)), "utf8").catch(
235
+ () => undefined,
236
+ );
237
+ if (text === undefined) return emptyDayIndex(day);
238
+ try {
239
+ const parsed: unknown = JSON.parse(text);
240
+ return isDayIndex(parsed) ? parsed : emptyDayIndex(day);
241
+ } catch {
242
+ // A torn index costs nothing to rebuild — the event file is the record — so
243
+ // a parse failure starts over rather than failing the read.
244
+ return emptyDayIndex(day);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Writes an index atomically.
250
+ *
251
+ * Written to a pid-suffixed temp file and renamed, so a reader never sees a
252
+ * half-written index and a crash mid-write leaves the previous one in place.
253
+ */
254
+ export async function writeDayIndex(index: DayIndex): Promise<void> {
255
+ await mkdir(TELEMETRY_EVENT_DIR, { recursive: true });
256
+ const target = join(TELEMETRY_EVENT_DIR, dayIndexFileName(index.day));
257
+ const temp = `${target}.${process.pid}.tmp`;
258
+ await writeFile(temp, `${JSON.stringify(index)}\n`, "utf8");
259
+ await rename(temp, target);
260
+ }
261
+
262
+ /** Byte size of a day's event file; 0 when it does not exist. */
263
+ export async function dayEventFileBytes(day: string): Promise<number> {
264
+ const size = await stat(join(TELEMETRY_EVENT_DIR, dayEventFileName(day)))
265
+ .then((value) => value.size)
266
+ .catch(() => undefined);
267
+ return size ?? 0;
268
+ }
269
+
270
+ /**
271
+ * Reads the uncounted tail of a day's event file.
272
+ *
273
+ * Only bytes past `fromBytes` are read, and a partial line at the very end is
274
+ * dropped rather than counted: the writer may still be appending to it, and a
275
+ * line that is not yet terminated is not yet an event.
276
+ */
277
+ async function* readTail(
278
+ day: string,
279
+ fromBytes: number,
280
+ ): AsyncGenerator<{ readonly event: FoldedEvent; readonly endBytes: number }> {
281
+ const handle = await open(join(TELEMETRY_EVENT_DIR, dayEventFileName(day)), "r").catch(
282
+ () => undefined,
283
+ );
284
+ if (handle === undefined) return;
285
+ try {
286
+ const size = (await handle.stat().catch(() => undefined))?.size ?? 0;
287
+ if (size <= fromBytes) return;
288
+ const length = size - fromBytes;
289
+ const buffer = Buffer.alloc(length);
290
+ await handle.read(buffer, 0, length, fromBytes);
291
+ const text = buffer.toString("utf8");
292
+ const lines = text.split("\n");
293
+ // The final element is either empty or a line the writer has not finished;
294
+ // a unterminated line is not an event yet, so it is left for the next fold.
295
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
296
+ else lines.pop();
297
+ let consumed = 0;
298
+ for (const line of lines) {
299
+ consumed += Buffer.byteLength(line, "utf8") + 1;
300
+ if (line.length === 0) continue;
301
+ const event = parseEventLine(line);
302
+ if (event !== undefined) yield { event, endBytes: fromBytes + consumed };
303
+ }
304
+ } finally {
305
+ await handle.close().catch(() => undefined);
306
+ }
307
+ }
308
+
309
+ /** Parses one event line, or `undefined` when it is not a usable event. */
310
+ function parseEventLine(line: string): FoldedEvent | undefined {
311
+ try {
312
+ const parsed: unknown = JSON.parse(line);
313
+ if (typeof parsed !== "object" || parsed === null) return undefined;
314
+ const candidate = parsed as Record<string, unknown>;
315
+ if (typeof candidate["tenantId"] !== "string") return undefined;
316
+ if (typeof candidate["requestId"] !== "string") return undefined;
317
+ if (typeof candidate["createdAt"] !== "string") return undefined;
318
+ // `tenantId` is checked above but declared optional on the record, so the
319
+ // narrow shape is asserted here rather than at every use site.
320
+ return candidate as unknown as FoldedEvent;
321
+ } catch {
322
+ return undefined;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Folds every event written since the watermark into the day index.
328
+ *
329
+ * Safe to call as often as you like: the watermark advances only after the
330
+ * index is on disk, so a crash means some events are folded twice at worst —
331
+ * and folding is a sum, so that is corrected by running again with the tail,
332
+ * not by any compensating logic.
333
+ */
334
+ export async function foldDayIndex(day: string): Promise<{ readonly folded: number }> {
335
+ const index = await readDayIndex(day);
336
+ const byKey = new Map<string, IndexedBucket>();
337
+ for (const bucket of index.buckets) {
338
+ byKey.set(bucketKey(bucket.tenantId, bucket.apiKeyId), bucket);
339
+ }
340
+
341
+ let watermark = index.watermarkBytes;
342
+ let folded = 0;
343
+ for await (const { event, endBytes } of readTail(day, index.watermarkBytes)) {
344
+ const key = bucketKey(event.tenantId, event.apiKeyId ?? null);
345
+ const prior = byKey.get(key);
346
+ const base: IndexedBucket =
347
+ prior ?? {
348
+ ...emptyBucket(),
349
+ tenantId: event.tenantId,
350
+ apiKeyId: event.apiKeyId ?? null,
351
+ hours: emptyHours(),
352
+ dimModel: {},
353
+ dimProvider: {},
354
+ dimClient: {},
355
+ dimClientIp: {},
356
+ };
357
+ byKey.set(key, {
358
+ ...accumulate(base, event),
359
+ tenantId: base.tenantId,
360
+ apiKeyId: base.apiKeyId,
361
+ hours: withHour(base.hours, event),
362
+ dimModel: foldDimension(base.dimModel, event.requestedModel ?? event.reportedModel, event),
363
+ dimProvider: foldDimension(base.dimProvider, event.providerId, event),
364
+ dimClient: foldDimension(base.dimClient, event.sourceSurface, event),
365
+ dimClientIp: foldDimension(base.dimClientIp, event.clientIp, event),
366
+ });
367
+ watermark = Math.max(watermark, endBytes);
368
+ folded += 1;
369
+ }
370
+
371
+ if (folded === 0) return { folded: 0 };
372
+ await writeDayIndex({
373
+ kind: INDEX_KIND,
374
+ version: INDEX_VERSION,
375
+ day,
376
+ watermarkBytes: watermark,
377
+ buckets: [...byKey.values()],
378
+ updatedAt: new Date().toISOString(),
379
+ });
380
+ return { folded };
381
+ }
382
+
383
+ /** The days whose index should be folded, newest last. */
384
+ export async function listEventDays(): Promise<readonly string[]> {
385
+ const { readdir } = await import("node:fs/promises");
386
+ const files = await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]);
387
+ const days = new Set<string>();
388
+ for (const file of files) {
389
+ if (file.endsWith(".events.jsonl")) days.add(file.slice(0, 10));
390
+ }
391
+ return [...days].sort();
392
+ }
393
+
394
+ /**
395
+ * Every bucket for one tenant, across the days a period covers.
396
+ *
397
+ * Reads only the index files for those days — a few small JSON reads — rather
398
+ * than parsing the event files, which is what made a usage query cost grow with
399
+ * traffic.
400
+ */
401
+ export async function queryIndexedBuckets(options: {
402
+ /** Omit to read every tenant — a caller that filters by key, not by tenant. */
403
+ readonly tenantId?: string | undefined;
404
+ readonly since?: Date | undefined;
405
+ /** Fold each day's tail first, so a read never reports a stale total. */
406
+ readonly foldFirst?: boolean;
407
+ }): Promise<readonly IndexedBucket[]> {
408
+ const days = await listEventDays();
409
+ const sinceDay = options.since === undefined ? undefined : utcDay(options.since);
410
+ const out: IndexedBucket[] = [];
411
+ for (const day of days) {
412
+ if (sinceDay !== undefined && day < sinceDay) continue;
413
+ if (options.foldFirst === true) await foldDayIndex(day).catch(() => ({ folded: 0 }));
414
+ const index = await readDayIndex(day);
415
+ for (const bucket of index.buckets) {
416
+ if (options.tenantId !== undefined && bucket.tenantId !== options.tenantId) continue;
417
+ out.push(bucket);
418
+ }
419
+ }
420
+ return out;
421
+ }
422
+
423
+ /** The days a period spans, oldest first. */
424
+ export function daysSince(since: Date | undefined, now = new Date()): readonly string[] {
425
+ const start = since === undefined ? now : since;
426
+ const out: string[] = [];
427
+ const cursor = new Date(Date.UTC(
428
+ start.getUTCFullYear(),
429
+ start.getUTCMonth(),
430
+ start.getUTCDate(),
431
+ ));
432
+ const end = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
433
+ while (cursor.getTime() <= end && out.length < 400) {
434
+ out.push(utcDay(cursor));
435
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
436
+ }
437
+ return out;
438
+ }
439
+
440
+ export { mergeBucketsInternal as mergeBuckets };
@@ -1,13 +1,44 @@
1
1
  import { open, readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { resolveTelemetryRetentionDays } from "../config/telemetry";
4
+ import { resolveDataDir } from "../persistence/db-mode";
3
5
 
4
6
  /**
5
7
  * Per-request telemetry lives in hourly JSONL files, not in Postgres. The
6
- * database keeps only the durable aggregate. Every reader that used to query
7
- * `telemetry_events` reads through here, so the file is the single source for
8
- * recent requests and the table is no longer written.
8
+ * database keeps only the durable aggregate; the files are the single source
9
+ * for recent requests and readers must go through this module, not a
10
+ * `telemetry_events` table.
9
11
  */
10
- export const TELEMETRY_EVENT_DIR = "./data/telemetry-events";
12
+ /**
13
+ * Where hourly event files and their compacted summaries live.
14
+ *
15
+ * Resolved from the configured data directory, never relative to the working
16
+ * directory: the gateway can be started from anywhere, and a relative path
17
+ * makes the written history depend on where the operator happened to be —
18
+ * start it from another folder and every past request reads as zero, because
19
+ * the files were written somewhere else entirely.
20
+ */
21
+ export const TELEMETRY_EVENT_DIR = join(resolveDataDir(), "telemetry-events");
22
+
23
+ /**
24
+ * The source file an event lands in: one per closed `windowMinutes` bucket.
25
+ *
26
+ * The name is the bucket's own start (`YYYY-MM-DDTHH-MM`), so both the writer
27
+ * and the compactor derive it from a timestamp and cannot disagree. It is also
28
+ * what makes "is this window closed?" a name comparison rather than a read.
29
+ */
30
+ export function windowFileName(at: Date, windowMinutes: number): string {
31
+ // `2026-10-22T05-15.jsonl`: ISO to the minute with the colon replaced, so the
32
+ // name sorts chronologically and needs no separate timestamp column.
33
+ return `${bucketStart(at, windowMinutes).toISOString().slice(0, 16).replace(":", "-")}.jsonl`;
34
+ }
35
+
36
+ /** The start of the window containing `at`, floored to `windowMinutes`. */
37
+ export function bucketStart(at: Date, windowMinutes: number): Date {
38
+ const ms = Math.max(1, Math.floor(windowMinutes)) * 60_000;
39
+ return new Date(Math.floor(at.getTime() / ms) * ms);
40
+ }
41
+
11
42
 
12
43
  export interface TelemetryEventRecord {
13
44
  readonly id: string;
@@ -100,8 +131,17 @@ function normalize(raw: Record<string, unknown>): TelemetryEventRecord | undefin
100
131
  };
101
132
  }
102
133
 
134
+ /**
135
+ * The comparable hour of an event file's name.
136
+ *
137
+ * A name is either `2026-10-22T05-15.jsonl` (an older window file) or
138
+ * `2026-10-22.events.jsonl` (a day file). Both sort against `hourStamp`: only
139
+ * the leading date and hour are taken, so the day file's `.events` suffix cannot
140
+ * make it compare as later or earlier than the window files beside it.
141
+ */
103
142
  function eventFileHour(file: string): string {
104
- return file.slice(0, 13);
143
+ const stamp = file.endsWith(".events.jsonl") ? `${file.slice(0, 10)}T00` : file.slice(0, 13);
144
+ return stamp;
105
145
  }
106
146
 
107
147
  function hourStamp(date: Date): string {
@@ -111,6 +151,71 @@ function hourStamp(date: Date): string {
111
151
  /** Chunk size for the streaming scan: one line can be long, a file cannot. */
112
152
  const READ_CHUNK_BYTES = 256 * 1024;
113
153
 
154
+ /**
155
+ * The newest matching events in one file, read backwards from the end.
156
+ *
157
+ * Reads fixed blocks from the tail until the page is full, so a day file of any
158
+ * size costs one block per page rather than a full parse. A partial line at the
159
+ * very end is dropped: the writer may still be appending to it.
160
+ */
161
+ async function* readEventFileTail(
162
+ file: string,
163
+ tenantId: string | undefined,
164
+ want: number,
165
+ since: number | undefined,
166
+ until: number | undefined,
167
+ ): AsyncGenerator<TelemetryEventRecord> {
168
+ if (want <= 0) return;
169
+ const handle = await open(join(TELEMETRY_EVENT_DIR, file), "r").catch(() => undefined);
170
+ if (handle === undefined) return;
171
+ try {
172
+ const size = (await handle.stat().catch(() => undefined))?.size ?? 0;
173
+ if (size === 0) return;
174
+ const BLOCK = 64 * 1024;
175
+ let end = size;
176
+ let carry = "";
177
+ const found: TelemetryEventRecord[] = [];
178
+ while (end > 0 && found.length < want) {
179
+ const start = Math.max(0, end - BLOCK);
180
+ const length = end - start;
181
+ const buffer = Buffer.alloc(length);
182
+ await handle.read(buffer, 0, length, start);
183
+ const text = buffer.toString("utf8") + carry;
184
+ const lines = text.split("\n");
185
+ carry = lines.shift() ?? "";
186
+ // Newest first: the block was read from the end, so its lines run
187
+ // backwards through the file.
188
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
189
+ const line = lines[index] ?? "";
190
+ if (line.length === 0) continue;
191
+ if (tenantId !== undefined && !line.includes(tenantId)) continue;
192
+ let parsed: TelemetryEventRecord | undefined;
193
+ try {
194
+ const value: unknown = JSON.parse(line);
195
+ if (typeof value === "object" && value !== null) {
196
+ parsed = value as TelemetryEventRecord;
197
+ }
198
+ } catch {
199
+ continue;
200
+ }
201
+ if (parsed === undefined) continue;
202
+ if (parsed.tenantId !== tenantId) continue;
203
+ const at = Date.parse(parsed.createdAt);
204
+ if (Number.isFinite(at)) {
205
+ if (since !== undefined && at < since) continue;
206
+ if (until !== undefined && at > until) continue;
207
+ }
208
+ found.push(parsed);
209
+ if (found.length >= want) break;
210
+ }
211
+ end = start;
212
+ }
213
+ for (const event of found) yield event;
214
+ } finally {
215
+ await handle.close().catch(() => undefined);
216
+ }
217
+ }
218
+
114
219
  async function* readEventFiles(
115
220
  files: readonly string[],
116
221
  allow?: (line: string) => boolean,
@@ -213,8 +318,29 @@ function matches(event: TelemetryEventRecord, query: EventQuery, since: number |
213
318
 
214
319
  /** The events matching a query, already newest first. */
215
320
  export async function queryTelemetryEvents(query: EventQuery): Promise<TelemetryEventRecord[]> {
321
+ // Hourly request files only. A `.compacted.jsonl` also ends in `.jsonl`, and
322
+ // letting one through meant every summary row was parsed as if it were a
323
+ // request — a row with no `tenantId`/`createdAt` that then failed `matches`,
324
+ // but only after paying for the parse.
325
+ //
326
+ // The lower bound is the retention window, not just `since`: a caller that
327
+ // asks for all time still must not scan files that have already been aged
328
+ // out, and one that passes no `since` at all must not read every file ever
329
+ // written. Files older than retention are gone or compacted; either way they
330
+ // are not here.
331
+ const retentionFloor = hourStamp(
332
+ new Date(Date.now() - resolveTelemetryRetentionDays() * 24 * 60 * 60_000),
333
+ );
334
+ const sinceFloor =
335
+ query.since === undefined ? retentionFloor : hourStamp(query.since);
336
+ const floor = sinceFloor > retentionFloor ? sinceFloor : retentionFloor;
216
337
  const files = (await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]))
217
- .filter((file) => file.endsWith(".jsonl") && (query.since === undefined || eventFileHour(file) >= hourStamp(query.since)))
338
+ .filter(
339
+ (file) =>
340
+ file.endsWith(".jsonl") &&
341
+ !file.endsWith(".compacted.jsonl") &&
342
+ eventFileHour(file) >= floor,
343
+ )
218
344
  .sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
219
345
  const since = query.since?.getTime();
220
346
  const until = query.until?.getTime();
@@ -235,6 +361,27 @@ export async function queryTelemetryEvents(query: EventQuery): Promise<Telemetry
235
361
  // A single-row lookup: stop at the first (newest) match instead of parsing
236
362
  // the whole retention window.
237
363
  const oneRow = query.requestId !== undefined || query.id !== undefined;
364
+ // With one file per day, a plain page of the newest rows would otherwise
365
+ // parse the whole day to return fifty lines. When nothing filters on
366
+ // content — no status, no key, no account — the newest rows are the last
367
+ // lines, so they are read backwards from the end and the scan stops once the
368
+ // page is full.
369
+ const tailOnly =
370
+ query.limit !== undefined &&
371
+ !oneRow &&
372
+ query.httpStatus === undefined &&
373
+ query.apiKeyIds === undefined &&
374
+ query.accountIds === undefined;
375
+ if (tailOnly) {
376
+ const newest: TelemetryEventRecord[] = [];
377
+ for (const file of files) {
378
+ for await (const event of readEventFileTail(file, query.tenantId, query.limit - newest.length, since, until)) {
379
+ newest.push(event);
380
+ }
381
+ if (newest.length >= query.limit) break;
382
+ }
383
+ return newest.slice(0, query.limit);
384
+ }
238
385
  for (const file of files) {
239
386
  const matched: TelemetryEventRecord[] = [];
240
387
  for await (const event of readEventFiles([file], allow)) {
@@ -270,6 +417,8 @@ export function emptyTotals(): EventTotals {
270
417
  return { requests: 0, succeeded: 0, failed: 0, cancelled: 0, truncated: 0, inputTokens: 0, cachedTokens: 0, outputTokens: 0, estimatedCost: 0, unpriced: 0, latencySum: 0, latencyCount: 0, tokensPerSecSum: 0, tokensPerSecCount: 0 };
271
418
  }
272
419
 
420
+ /**
421
+
273
422
  /** Folds one event into a totals bucket. `failed` counts any non-completed terminal status. */
274
423
  export function accumulate(totals: EventTotals, event: TelemetryEventRecord): void {
275
424
  totals.requests += 1;
@@ -115,12 +115,12 @@ function logArgs(
115
115
  msg: string,
116
116
  args: readonly unknown[],
117
117
  ): void {
118
- // The level is consulted first. Building the safe copy used to happen
119
- // unconditionally, so a `debug` call at the production `info` default still
120
- // paid a depth-capped deep copy plus a JSON.stringify of every argument, on
121
- // every request that reached it — the surface-detection debug line fires on
122
- // almost every chat request. Pino's own level check happens inside `write`,
123
- // which is already too late to skip the work.
118
+ // The level is consulted first. Building the safe copy unconditionally would
119
+ // make a `debug` call at the production `info` default still pay a
120
+ // depth-capped deep copy plus a JSON.stringify of every argument, on every
121
+ // request that reaches it — the surface-detection debug line fires on almost
122
+ // every chat request. Pino's own level check happens inside `write`, which is
123
+ // already too late to skip the work.
124
124
  if (!logger.isLevelEnabled(level)) return;
125
125
  const safeArgs = safeLogArgs(args);
126
126
  ring(level, msg, safeArgs);