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
@@ -1,485 +1,524 @@
1
- import type { ConsoleAccessResolver } from "../auth/access";
2
- // Telemetry domain: system health, usage, and telemetry-event read API.
3
-
4
- import { ConsoleDomainError, requireTenantScope } from "../shared/errors";
5
- import { conditionalResponse, type CachedResult } from "../shared/read-through-cache";
6
- import { parseQueryLimit } from "../shared/query";
7
- import type { AccessDecision } from "../../security/access-control";
8
- import { USAGE_DIMENSIONS, type UsageDimension } from "./usage-dimensions";
9
-
10
- /**
11
- * Whitelisted usage periods. Numeric tokens bound the aggregation window;
12
- * `"all"` is the single explicit unbounded token. Anything else is rejected
13
- * before any database query so a typo cannot trigger a full-table scan.
14
- */
15
- export function isSupportedUsagePeriod(period: string): boolean {
16
- if (period === "all") return true;
17
- const match = /^(\d+)([hd])$/.exec(period);
18
- if (!match) return false;
19
- const amount = Number(match[1]);
20
- const cap = match[2] === "h" ? 8760 : 365;
21
- return Number.isSafeInteger(amount) && amount >= 1 && amount <= cap;
22
- }
23
-
24
- export { USAGE_PERIODS, type UsagePeriod } from "./usage-periods";
25
-
26
- export interface SystemHealthResponse {
27
- version: string;
28
- status: "healthy" | "degraded" | "unhealthy";
29
- uptime_seconds: number;
30
- database_healthy: boolean;
31
- redis_healthy: boolean;
32
- /** False when no Redis client is configured and coordination stays in memory. */
33
- redis_configured: boolean;
34
- /** JavaScript heap used by this process. */
35
- memory_heap_bytes: number;
36
- /** Native buffers and malloc outside the JavaScript heap. */
37
- memory_external_bytes: number;
38
- memory_bytes: number;
39
- /** RSS as a percentage of system memory, not of the JS heap. */
40
- memory_percent: number;
41
- /**
42
- * True when the database runs inside this process (SQLite). Then RSS covers
43
- * the gateway *and* a whole PostgreSQL build, so it is not comparable with a
44
- * driver-only process and the console must say so rather than imply the
45
- * gateway itself grew.
46
- */
47
- database_in_process: boolean;
48
- /**
49
- * `null` when the platform does not report it — there is no `loadavg` on
50
- * Windows and Node hands back a zeroed tuple rather than failing, so on
51
- * Windows this is always `null` rather than a confident-looking `0`.
52
- *
53
- * Nullable rather than `NaN`: `NaN` is not a JSON value and serializes to
54
- * `null` anyway, which means a client validating the field as a number
55
- * rejects the *whole* health payload. One unavailable metric then reads as
56
- * every metric being unavailable.
57
- */
58
- cpu_percent: number | null;
59
- cpu_cores: number;
60
- pid: number;
61
- platform: string;
62
- request_count: number;
63
- error_count: number;
64
- latency_avg_ms: number;
65
- latency_p95_ms: number;
66
- latency_p99_ms: number;
67
- cache_hit_rate_percent: number;
68
- avg_tokens_per_sec: number;
69
- }
70
- export interface UsageResponse {
71
- tenantId: string;
72
- period: string;
73
- requestsTotal: number;
74
- requestsSucceeded: number;
75
- requestsFailed: number;
76
- tokensUsed: number;
77
- estimatedCost: number;
78
- topModels: Array<{ modelId: string; count: number }>;
79
- topProviders: Array<{ providerId: string; count: number }>;
80
- }
81
-
82
- /** Period-bound usage summary mirroring the 20-beta analytics surface. */
83
- export interface UsageSummaryTotals {
84
- requests: number;
85
- inputTokens: number;
86
- cachedTokens: number;
87
- outputTokens: number;
88
- /**
89
- * Failed and truncated requests. Client aborts (`cancelled`, rendered 499)
90
- * are expected client behavior, never an error, and are tracked separately.
91
- */
92
- errors: number;
93
- /** Client-cancelled requests (rendered `499`) — a sub-count, never an error. */
94
- cancelled: number;
95
- /** Streams that ended early (`502`); also included in `errors`. */
96
- truncated: number;
97
- /** Counts by actual HTTP status for the clickable Requests filters. */
98
- statusCounts: Array<{ status: number; count: number }>;
99
- /** Average gateway routing and preparation delay before upstream dispatch. */
100
- avgResolveMs: number;
101
- avgDurationMs: number;
102
- /** Nearest-rank latency percentiles over the selected usage period. */
103
- latencyP50Ms: number;
104
- latencyP95Ms: number;
105
- latencyP99Ms: number;
106
- /**
107
- * How many events each latency series was computed from. A percentile over
108
- * twenty requests and one over twenty thousand do not deserve the same
109
- * confidence, and the count is the only thing that says which it is.
110
- */
111
- latencySampleCount: number;
112
- /** Events carrying a resolve measurement; differs when a row never dispatched. */
113
- resolveSampleCount: number;
114
- estimatedCostUsd: number;
115
- /** True when completed rows without persisted cost exist in the window. */
116
- partial: boolean;
117
- /** Cache hit rate percentage (0-100). */
118
- cacheHitRate: number;
119
- /** Average tokens per second across the period. */
120
- avgTokensPerSec: number;
121
- }
122
- export interface UsageSummaryResponse {
123
- period: string;
124
- totals: UsageSummaryTotals;
125
- }
126
- export interface UsageChartBucket {
127
- /** Bucket start as ISO timestamp. */
128
- t: string;
129
- requests: number;
130
- input: number;
131
- cached: number;
132
- output: number;
133
- }
134
- export interface UsageChartResponse {
135
- buckets: UsageChartBucket[];
136
- }
137
- export interface UsageByRow {
138
- name: string;
139
- requests: number;
140
- input: number;
141
- output: number;
142
- cached: number;
143
- total: number;
144
- errors: number;
145
- /** Null when no row in the group carries persisted cost. */
146
- costUsd: number | null;
147
- /** Resolved API key label for the `key` dimension; absent otherwise. */
148
- label?: string;
149
- /** Cache hit rate percentage (0-100). */
150
- cacheHitRate: number;
151
- /** Average tokens per second for this dimension. */
152
- avgTokensPerSec: number;
153
- }
154
- /**
155
- * Breakdown dimensions the usage endpoint accepts. Declared in `./usage-dimensions`
156
- * and re-exported here, because three layers read the same binding: the route
157
- * table registers one `by-<dimension>` path per member, the operations validator
158
- * rejects anything outside it, and the dashboard's `Dimension` union mirrors it
159
- * (kept in sync by hand).
160
- */
161
- export { USAGE_DIMENSIONS, type UsageDimension };
162
-
163
- export interface UsageByResponse {
164
- /** The dimension these rows were grouped by, so a stale response can never be read as another tab's data. */
165
- dimension: UsageDimension;
166
- rows: UsageByRow[];
167
- }
168
- export interface UsageCacheResponse {
169
- period: string;
170
- inputTokens: number;
171
- cachedTokens: number;
172
- /** Always zero: cache-write tokens are not tracked by the telemetry schema. */
173
- cacheWriteTokens: number;
174
- hitRate: number;
175
- }
176
- export type UsageRequestMode = "stream" | "non_stream";
177
- export interface UsageRequestItem {
178
- requestId: string;
179
- surface?: string;
180
- providerId?: string;
181
- /** Provider account (credential) that served this request, if any. */
182
- accountId?: string;
183
- model?: string;
184
- /**
185
- * The model the upstream named in its own response, verbatim — not the one
186
- * the client asked for. Absent when the response carried no `model`; never
187
- * backfilled from `model`, because that would report the ask as the answer.
188
- */
189
- reportedModel?: string;
190
- endpoint?: string;
191
- apiKeyId?: string;
192
- userAgent?: string;
193
- clientName?: string;
194
- estimatedCost?: number;
195
- /** Provider-billed credits; absent when the upstream reported no credit field. */
196
- creditUsed?: number;
197
- /** Gateway-side time from request start to upstream dispatch, in ms. */
198
- resolveMs?: number;
199
- /** Resolved API key label; absent when the key is gone or unknown. */
200
- apiKeyLabel?: string;
201
- /** HTTP status the client actually received; absent on historical rows. */
202
- httpStatus?: number;
203
- status: string;
204
- errorKind?: string;
205
- /**
206
- * Which layer failed: `cartethyia` (the gateway), `upstream` (the provider),
207
- * or `network`. Absent for rows written before the column existed.
208
- */
209
- errorOrigin?: string;
210
- mode: UsageRequestMode;
211
- startedAt: string;
212
- clientIp?: string;
213
- durationMs?: number;
214
- inputTokens?: number;
215
- outputTokens?: number;
216
- cachedTokens?: number;
217
- reasoningTokens?: number;
218
- totalTokens?: number;
219
- ttfbMs?: number;
220
- tokensPerSec?: number;
221
- firstContentDeltaAtMs?: number;
222
- lastEventAtMs?: number;
223
- /**
224
- * Why this request's drawer payloads are empty, or `captured` when they are
225
- * not. Remote-diagnosis field: the four panels read blank for every reason
226
- * alike, so without this an operator cannot tell "capture is off" from
227
- * "capture is broken" without access to the server's logs. Absent on rows
228
- * written before the column existed.
229
- */
230
- payloadCaptureState?: string;
231
- /**
232
- * Bitmask of the payload stages captured for this request: 1 request,
233
- * 2 provider request, 4 provider response, 8 client response. Read from the
234
- * payload row so the list can mark each drawer panel without inflating a
235
- * frame per row. Absent when nothing was captured for the request — the same
236
- * state a pre-migration row reports, rendered as every stage uncaptured
237
- * rather than as a measured zero.
238
- */
239
- payloadStages?: number;
240
- }
241
- export interface UsageRequestsResponse {
242
- items: UsageRequestItem[];
243
- }
244
- export interface UsageRequestDetail extends UsageRequestItem {
245
- proxy?: string;
246
- payloads?: {
247
- request?: unknown;
248
- response?: unknown;
249
- /** Body Cartethyia returned to the client (post-translation). */
250
- clientResponse?: unknown;
251
- providerRequest?: unknown;
252
- providerResponse?: unknown;
253
- } | null;
254
- payloadSignals?: {
255
- toolCalls: number;
256
- images: number;
257
- attachments: number;
258
- };
259
- }
260
- export interface TelemetryEventView {
261
- id: string;
262
- createdAt: string;
263
- tenantId: string;
264
- requestId: string;
265
- sourceSurface?: string;
266
- requestedModel?: string;
267
- providerId?: string;
268
- latencyMs?: number;
269
- status?: string;
270
- inputTokens?: number;
271
- outputTokens?: number;
272
- estimatedCost?: number;
273
- firstContentDeltaAtMs?: number;
274
- lastEventAtMs?: number;
275
- }
276
- export interface TelemetryEventDetail extends TelemetryEventView {
277
- payload?: { requestBody?: unknown; responseBody?: unknown };
278
- }
279
- export interface TelemetryEventListPage {
280
- entries: readonly TelemetryEventView[];
281
- /** Opaque cursor for the next page; `undefined` when the tail is reached. */
282
- nextCursor?: string;
283
- }
284
- export interface ObservabilityStore {
285
- /**
286
- * Every member below that is served from the read-through cache returns a
287
- * {@link CachedResult}, carrying the strong validator for the bytes the
288
- * client would otherwise receive. A repeated poll is then a `304` instead of
289
- * a full payload. The reads that are per-row and already bounded — the event
290
- * list and the detail views — have no cache entry and no validator.
291
- */
292
- health(tenantId: string): Promise<CachedResult<SystemHealthResponse>>;
293
- /**
294
- * Process memory as it is right now, bypassing the read-through cache.
295
- *
296
- * `health` is cached for 10 s, which is correct for a dashboard tile and
297
- * wrong for anything sampling a trend: a memory watcher polling on that same
298
- * 10 s cadence reads the cached bytes often enough that consecutive samples
299
- * can be identical, and a monotonic-growth test over identical samples is
300
- * meaningless. The values are read in-process and cost nothing, so there is
301
- * no reason to serve them stale.
302
- */
303
- liveMemory(): Promise<{ memory_bytes: number; memory_percent: number }>;
304
- usage(tenantId: string, period: string): Promise<CachedResult<UsageResponse>>;
305
- listEvents(tenantId: string, limit: number, cursor?: string): Promise<TelemetryEventListPage>;
306
- getEvent(
307
- tenantId: string,
308
- eventId: string,
309
- includePayload: boolean,
310
- createdAt?: string,
311
- ): Promise<TelemetryEventDetail | undefined>;
312
- usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>>;
313
- usageChart(tenantId: string, period: string): Promise<CachedResult<UsageChartResponse>>;
314
- usageBy(
315
- tenantId: string,
316
- dimension: UsageDimension,
317
- period: string,
318
- ): Promise<CachedResult<UsageByResponse>>;
319
- usageCache(tenantId: string, period: string): Promise<CachedResult<UsageCacheResponse>>;
320
- usageRequests(
321
- tenantId: string,
322
- period: string,
323
- limit: number,
324
- httpStatus?: number,
325
- ): Promise<CachedResult<UsageRequestsResponse>>;
326
- usageRequestDetail(tenantId: string, requestId: string): Promise<UsageRequestDetail | undefined>;
327
- }
328
- export interface ObservabilityConfig {
329
- readonly store: ObservabilityStore;
330
- readonly accessResolver: ConsoleAccessResolver;
331
- }
332
-
333
- export function createObservabilityOperations(config: ObservabilityConfig) {
334
- const operations = {
335
- /**
336
- * The cached reads below answer with a `Response` rather than a plain
337
- * object, because a repeated poll is a `304` and a `304` has no body to
338
- * serialize. `ifNoneMatch` is the client's `If-None-Match`; a match costs
339
- * ~200 bytes instead of the whole payload.
340
- */
341
- async getSystemHealth(
342
- access: AccessDecision | undefined,
343
- ifNoneMatch: string | null,
344
- ): Promise<Response> {
345
- const a = requireTenantScope(access, "dashboard:read");
346
- const result = await config.store.health(a.tenantId);
347
- return conditionalResponse(ifNoneMatch, result, result.value);
348
- },
349
- async getTenantUsage(
350
- access: AccessDecision | undefined,
351
- period = "7d",
352
- ifNoneMatch: string | null = null,
353
- ): Promise<Response> {
354
- const a = requireTenantScope(access, "dashboard:read");
355
- if (!isSupportedUsagePeriod(period)) {
356
- throw new ConsoleDomainError(
357
- "invalid_period",
358
- 400,
359
- `Unsupported usage period: ${period}. Use "24h", "7d", "30d", or "all".`,
360
- );
361
- }
362
- const result = await config.store.usage(a.tenantId, period);
363
- return conditionalResponse(ifNoneMatch, result, result.value);
364
- },
365
- async listEvents(
366
- access: AccessDecision | undefined,
367
- limit?: string,
368
- cursor?: string,
369
- ): Promise<TelemetryEventListPage> {
370
- const a = requireTenantScope(access, "dashboard:read");
371
- const lim = parseQueryLimit(limit, 50, 100);
372
- const page = await config.store.listEvents(a.tenantId, lim, cursor);
373
- const entries = page.entries;
374
- return page.nextCursor ? { entries, nextCursor: page.nextCursor } : { entries };
375
- },
376
- async getEventDetail(
377
- access: AccessDecision | undefined,
378
- eventId: string,
379
- includePayload = false,
380
- createdAt?: string,
381
- ): Promise<TelemetryEventDetail> {
382
- const a = requireTenantScope(access, "dashboard:read");
383
- const detail = await config.store.getEvent(
384
- a.tenantId,
385
- eventId,
386
- includePayload,
387
- createdAt,
388
- );
389
- if (!detail) throw new ConsoleDomainError("event_not_found", 404, `Event ${eventId} not found`);
390
- return detail;
391
- },
392
- requirePeriod(period: string): string {
393
- if (!isSupportedUsagePeriod(period)) {
394
- throw new ConsoleDomainError(
395
- "invalid_period",
396
- 400,
397
- `Unsupported usage period: ${period}. Use "1h", "24h", "7d", "30d", or "all".`,
398
- );
399
- }
400
- return period;
401
- },
402
- requireTenant(access: AccessDecision | undefined): string {
403
- const a = requireTenantScope(access, "dashboard:read");
404
- return a.tenantId;
405
- },
406
- async getUsageSummary(
407
- access: AccessDecision | undefined,
408
- period = "24h",
409
- ifNoneMatch: string | null = null,
410
- ): Promise<Response> {
411
- const tenantId = operations.requireTenant(access);
412
- const result = await config.store.usageSummary(tenantId, operations.requirePeriod(period));
413
- return conditionalResponse(ifNoneMatch, result, result.value);
414
- },
415
- async getUsageChart(
416
- access: AccessDecision | undefined,
417
- period = "24h",
418
- ifNoneMatch: string | null = null,
419
- ): Promise<Response> {
420
- const tenantId = operations.requireTenant(access);
421
- const result = await config.store.usageChart(tenantId, operations.requirePeriod(period));
422
- return conditionalResponse(ifNoneMatch, result, result.value);
423
- },
424
- async getUsageBy(
425
- access: AccessDecision | undefined,
426
- dimension: UsageDimension,
427
- period = "24h",
428
- ifNoneMatch: string | null = null,
429
- ): Promise<Response> {
430
- const tenantId = operations.requireTenant(access);
431
- if (!(USAGE_DIMENSIONS as readonly string[]).includes(dimension)) {
432
- throw new ConsoleDomainError(
433
- "invalid_dimension",
434
- 400,
435
- `Unsupported usage dimension: ${dimension}. Use ${USAGE_DIMENSIONS.map((d) => `"${d}"`).join(", ")}.`,
436
- );
437
- }
438
- const result = await config.store.usageBy(tenantId, dimension, operations.requirePeriod(period));
439
- return conditionalResponse(ifNoneMatch, result, result.value);
440
- },
441
- async getUsageCache(
442
- access: AccessDecision | undefined,
443
- period = "24h",
444
- ifNoneMatch: string | null = null,
445
- ): Promise<Response> {
446
- const tenantId = operations.requireTenant(access);
447
- const result = await config.store.usageCache(tenantId, operations.requirePeriod(period));
448
- return conditionalResponse(ifNoneMatch, result, result.value);
449
- },
450
- async listUsageRequests(
451
- access: AccessDecision | undefined,
452
- period = "24h",
453
- limit?: string,
454
- httpStatus?: string,
455
- ifNoneMatch: string | null = null,
456
- ): Promise<Response> {
457
- const tenantId = operations.requireTenant(access);
458
- let parsedStatus: number | undefined;
459
- if (httpStatus !== undefined) {
460
- parsedStatus = Number(httpStatus);
461
- if (!Number.isInteger(parsedStatus) || parsedStatus < 100 || parsedStatus > 599) {
462
- throw new ConsoleDomainError("invalid_request", 400, "Invalid HTTP status filter");
463
- }
464
- }
465
- const result = await config.store.usageRequests(
466
- tenantId,
467
- operations.requirePeriod(period),
468
- parseQueryLimit(limit, 50, 500),
469
- parsedStatus,
470
- );
471
- return conditionalResponse(ifNoneMatch, result, result.value);
472
- },
473
- async getUsageRequestDetail(
474
- access: AccessDecision | undefined,
475
- requestId: string,
476
- ): Promise<UsageRequestDetail> {
477
- const tenantId = operations.requireTenant(access);
478
- const detail = await config.store.usageRequestDetail(tenantId, requestId);
479
- if (!detail) throw new ConsoleDomainError("event_not_found", 404, `Request ${requestId} not found`);
480
- const payloads = detail.payloads ?? null;
481
- return { ...detail, payloads };
482
- },
483
- };
484
- return operations;
485
- }
1
+ import type { ConsoleAccessResolver } from "../auth/access";
2
+ // Telemetry domain: system health, usage, and telemetry-event read API.
3
+
4
+ import { readdir } from "node:fs/promises";
5
+ import { ConsoleDomainError, requireTenantScope } from "../shared/errors";
6
+ import { conditionalResponse, type CachedResult } from "../shared/read-through-cache";
7
+ import { parseQueryLimit } from "../shared/query";
8
+ import type { AccessDecision } from "../../security/access-control";
9
+ import { resolveTelemetryCompactAfterDays } from "../../config/telemetry";
10
+ import { USAGE_DIMENSIONS, type UsageDimension } from "./usage-dimensions";
11
+
12
+ /**
13
+ * Whitelisted usage periods. Numeric tokens bound the aggregation window;
14
+ * `"all"` is the single explicit unbounded token. Anything else is rejected
15
+ * before any database query so a typo cannot trigger a full-table scan.
16
+ */
17
+ export function isSupportedUsagePeriod(period: string): boolean {
18
+ if (period === "all") return true;
19
+ const match = /^(\d+)([hd])$/.exec(period);
20
+ if (!match) return false;
21
+ const amount = Number(match[1]);
22
+ const cap = match[2] === "h" ? 8760 : 365;
23
+ return Number.isSafeInteger(amount) && amount >= 1 && amount <= cap;
24
+ }
25
+
26
+ /**
27
+ * Why a request detail cannot be shown.
28
+ *
29
+ * A compacted request is genuinely gone: past the compaction age its row was
30
+ * folded into a per-day summary, and the individual request no longer exists
31
+ * anywhere. That is by design, not data loss — but "Request not found" reads
32
+ * like the latter, and an operator who thinks history was lost will go looking
33
+ * for a bug that is not there.
34
+ *
35
+ * The age is checked against the summary files rather than assumed: a request
36
+ * from before retention is aged out, not compacted, and the two deserve
37
+ * different words.
38
+ */
39
+ async function detailNotFoundMessage(requestId: string, noun = "Request"): Promise<string> {
40
+ const compacted = await readdir("./data/telemetry-events").catch(() => [] as string[]);
41
+ const hasSummaries = compacted.some((file) => file.endsWith(".index.json"));
42
+ if (!hasSummaries) return `${noun} ${requestId} not found`;
43
+ return (
44
+ `${noun} ${requestId} is no longer stored individually: requests older than ` +
45
+ `${resolveTelemetryCompactAfterDays()} day(s) are compacted into daily summaries. ` +
46
+ `Its totals still count in Usage — raise CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS ` +
47
+ `to keep individual requests longer.`
48
+ );
49
+ }
50
+
51
+ export { USAGE_PERIODS, type UsagePeriod } from "./usage-periods";
52
+
53
+ export interface SystemHealthResponse {
54
+ version: string;
55
+ status: "healthy" | "degraded" | "unhealthy";
56
+ uptime_seconds: number;
57
+ database_healthy: boolean;
58
+ redis_healthy: boolean;
59
+ /** False when no Redis client is configured and coordination stays in memory. */
60
+ redis_configured: boolean;
61
+ /** JavaScript heap used by this process. */
62
+ memory_heap_bytes: number;
63
+ /** Native buffers and malloc outside the JavaScript heap. */
64
+ memory_external_bytes: number;
65
+ memory_bytes: number;
66
+ /** RSS as a percentage of system memory, not of the JS heap. */
67
+ memory_percent: number;
68
+ /**
69
+ * True when the database runs inside this process (SQLite). Then RSS covers
70
+ * the gateway *and* a whole PostgreSQL build, so it is not comparable with a
71
+ * driver-only process and the console must say so rather than imply the
72
+ * gateway itself grew.
73
+ */
74
+ database_in_process: boolean;
75
+ /**
76
+ * `null` when the platform does not report it — there is no `loadavg` on
77
+ * Windows and Node hands back a zeroed tuple rather than failing, so on
78
+ * Windows this is always `null` rather than a confident-looking `0`.
79
+ *
80
+ * Nullable rather than `NaN`: `NaN` is not a JSON value and serializes to
81
+ * `null` anyway, which means a client validating the field as a number
82
+ * rejects the *whole* health payload. One unavailable metric then reads as
83
+ * every metric being unavailable.
84
+ */
85
+ cpu_percent: number | null;
86
+ cpu_cores: number;
87
+ pid: number;
88
+ platform: string;
89
+ request_count: number;
90
+ error_count: number;
91
+ latency_avg_ms: number;
92
+ latency_p95_ms: number;
93
+ latency_p99_ms: number;
94
+ cache_hit_rate_percent: number;
95
+ avg_tokens_per_sec: number;
96
+ }
97
+ export interface UsageResponse {
98
+ tenantId: string;
99
+ period: string;
100
+ requestsTotal: number;
101
+ requestsSucceeded: number;
102
+ requestsFailed: number;
103
+ tokensUsed: number;
104
+ estimatedCost: number;
105
+ topModels: Array<{ modelId: string; count: number }>;
106
+ topProviders: Array<{ providerId: string; count: number }>;
107
+ }
108
+
109
+ /** Period-bound usage summary mirroring the 20-beta analytics surface. */
110
+ export interface UsageSummaryTotals {
111
+ requests: number;
112
+ inputTokens: number;
113
+ cachedTokens: number;
114
+ outputTokens: number;
115
+ /**
116
+ * Failed and truncated requests. Client aborts (`cancelled`, rendered 499)
117
+ * are expected client behavior, never an error, and are tracked separately.
118
+ */
119
+ errors: number;
120
+ /** Client-cancelled requests (rendered `499`) — a sub-count, never an error. */
121
+ cancelled: number;
122
+ /** Streams that ended early (`502`); also included in `errors`. */
123
+ truncated: number;
124
+ /** Counts by actual HTTP status for the clickable Requests filters. */
125
+ statusCounts: Array<{ status: number; count: number }>;
126
+ /** Average gateway routing and preparation delay before upstream dispatch. */
127
+ avgResolveMs: number;
128
+ avgDurationMs: number;
129
+ /** Nearest-rank latency percentiles over the selected usage period. */
130
+ latencyP50Ms: number;
131
+ latencyP95Ms: number;
132
+ latencyP99Ms: number;
133
+ /**
134
+ * How many events each latency series was computed from. A percentile over
135
+ * twenty requests and one over twenty thousand do not deserve the same
136
+ * confidence, and the count is the only thing that says which it is.
137
+ */
138
+ latencySampleCount: number;
139
+ /** Events carrying a resolve measurement; differs when a row never dispatched. */
140
+ resolveSampleCount: number;
141
+ estimatedCostUsd: number;
142
+ /** True when completed rows without persisted cost exist in the window. */
143
+ partial: boolean;
144
+ /** Cache hit rate percentage (0-100). */
145
+ cacheHitRate: number;
146
+ /** Average tokens per second across the period. */
147
+ avgTokensPerSec: number;
148
+ }
149
+ export interface UsageSummaryResponse {
150
+ period: string;
151
+ totals: UsageSummaryTotals;
152
+ }
153
+ export interface UsageChartBucket {
154
+ /** Bucket start as ISO timestamp. */
155
+ t: string;
156
+ requests: number;
157
+ input: number;
158
+ cached: number;
159
+ output: number;
160
+ }
161
+ export interface UsageChartResponse {
162
+ buckets: UsageChartBucket[];
163
+ }
164
+ export interface UsageByRow {
165
+ name: string;
166
+ requests: number;
167
+ input: number;
168
+ output: number;
169
+ cached: number;
170
+ total: number;
171
+ errors: number;
172
+ /** Null when no row in the group carries persisted cost. */
173
+ costUsd: number | null;
174
+ /** Resolved API key label for the `key` dimension; absent otherwise. */
175
+ label?: string;
176
+ /** Cache hit rate percentage (0-100). */
177
+ cacheHitRate: number;
178
+ /** Average tokens per second for this dimension. */
179
+ avgTokensPerSec: number;
180
+ }
181
+ /**
182
+ * Breakdown dimensions the usage endpoint accepts. Declared in `./usage-dimensions`
183
+ * and re-exported here, because three layers read the same binding: the route
184
+ * table registers one `by-<dimension>` path per member, the operations validator
185
+ * rejects anything outside it, and the dashboard's `Dimension` union mirrors it
186
+ * (kept in sync by hand).
187
+ */
188
+ export { USAGE_DIMENSIONS, type UsageDimension };
189
+
190
+ export interface UsageByResponse {
191
+ /** The dimension these rows were grouped by, so a stale response can never be read as another tab's data. */
192
+ dimension: UsageDimension;
193
+ rows: UsageByRow[];
194
+ }
195
+ export interface UsageCacheResponse {
196
+ period: string;
197
+ inputTokens: number;
198
+ cachedTokens: number;
199
+ /** Always zero: cache-write tokens are not tracked by the telemetry schema. */
200
+ cacheWriteTokens: number;
201
+ hitRate: number;
202
+ }
203
+ export type UsageRequestMode = "stream" | "non_stream";
204
+ export interface UsageRequestItem {
205
+ requestId: string;
206
+ surface?: string;
207
+ providerId?: string;
208
+ /** Provider account (credential) that served this request, if any. */
209
+ accountId?: string;
210
+ model?: string;
211
+ /**
212
+ * The model the upstream named in its own response, verbatim — not the one
213
+ * the client asked for. Absent when the response carried no `model`; never
214
+ * backfilled from `model`, because that would report the ask as the answer.
215
+ */
216
+ reportedModel?: string;
217
+ endpoint?: string;
218
+ apiKeyId?: string;
219
+ userAgent?: string;
220
+ clientName?: string;
221
+ estimatedCost?: number;
222
+ /** Provider-billed credits; absent when the upstream reported no credit field. */
223
+ creditUsed?: number;
224
+ /** Gateway-side time from request start to upstream dispatch, in ms. */
225
+ resolveMs?: number;
226
+ /** Resolved API key label; absent when the key is gone or unknown. */
227
+ apiKeyLabel?: string;
228
+ /** HTTP status the client actually received; absent on historical rows. */
229
+ httpStatus?: number;
230
+ status: string;
231
+ errorKind?: string;
232
+ /**
233
+ * Which layer failed: `cartethyia` (the gateway), `upstream` (the provider),
234
+ * or `network`. Absent for rows written before the column existed.
235
+ */
236
+ errorOrigin?: string;
237
+ mode: UsageRequestMode;
238
+ startedAt: string;
239
+ clientIp?: string;
240
+ durationMs?: number;
241
+ inputTokens?: number;
242
+ outputTokens?: number;
243
+ cachedTokens?: number;
244
+ reasoningTokens?: number;
245
+ totalTokens?: number;
246
+ ttfbMs?: number;
247
+ tokensPerSec?: number;
248
+ firstContentDeltaAtMs?: number;
249
+ lastEventAtMs?: number;
250
+ /**
251
+ * Why this request's drawer payloads are empty, or `captured` when they are
252
+ * not. Remote-diagnosis field: the four panels read blank for every reason
253
+ * alike, so without this an operator cannot tell "capture is off" from
254
+ * "capture is broken" without access to the server's logs. Absent on rows
255
+ * written before the column existed.
256
+ */
257
+ payloadCaptureState?: string;
258
+ /**
259
+ * Bitmask of the payload stages captured for this request: 1 request,
260
+ * 2 provider request, 4 provider response, 8 client response. Read from the
261
+ * payload row so the list can mark each drawer panel without inflating a
262
+ * frame per row. Absent when nothing was captured for the request — the same
263
+ * state a pre-migration row reports, rendered as every stage uncaptured
264
+ * rather than as a measured zero.
265
+ */
266
+ payloadStages?: number;
267
+ }
268
+ export interface UsageRequestsResponse {
269
+ items: UsageRequestItem[];
270
+ }
271
+ export interface UsageRequestDetail extends UsageRequestItem {
272
+ proxy?: string;
273
+ payloads?: {
274
+ request?: unknown;
275
+ response?: unknown;
276
+ /** Body Cartethyia returned to the client (post-translation). */
277
+ clientResponse?: unknown;
278
+ providerRequest?: unknown;
279
+ providerResponse?: unknown;
280
+ } | null;
281
+ payloadSignals?: {
282
+ toolCalls: number;
283
+ images: number;
284
+ attachments: number;
285
+ };
286
+ }
287
+ export interface TelemetryEventView {
288
+ id: string;
289
+ createdAt: string;
290
+ tenantId: string;
291
+ requestId: string;
292
+ sourceSurface?: string;
293
+ requestedModel?: string;
294
+ providerId?: string;
295
+ latencyMs?: number;
296
+ status?: string;
297
+ inputTokens?: number;
298
+ outputTokens?: number;
299
+ estimatedCost?: number;
300
+ firstContentDeltaAtMs?: number;
301
+ lastEventAtMs?: number;
302
+ }
303
+ export interface TelemetryEventDetail extends TelemetryEventView {
304
+ payload?: { requestBody?: unknown; responseBody?: unknown };
305
+ }
306
+ export interface TelemetryEventListPage {
307
+ entries: readonly TelemetryEventView[];
308
+ /** Opaque cursor for the next page; `undefined` when the tail is reached. */
309
+ nextCursor?: string;
310
+ }
311
+ export interface ObservabilityStore {
312
+ /**
313
+ * Every member below that is served from the read-through cache returns a
314
+ * {@link CachedResult}, carrying the strong validator for the bytes the
315
+ * client would otherwise receive. A repeated poll is then a `304` instead of
316
+ * a full payload. The reads that are per-row and already bounded — the event
317
+ * list and the detail views — have no cache entry and no validator.
318
+ */
319
+ health(tenantId: string): Promise<CachedResult<SystemHealthResponse>>;
320
+ /**
321
+ * Process memory as it is right now, bypassing the read-through cache.
322
+ *
323
+ * `health` is cached for 10 s, which is correct for a dashboard tile and
324
+ * wrong for anything sampling a trend: a memory watcher polling on that same
325
+ * 10 s cadence reads the cached bytes often enough that consecutive samples
326
+ * can be identical, and a monotonic-growth test over identical samples is
327
+ * meaningless. The values are read in-process and cost nothing, so there is
328
+ * no reason to serve them stale.
329
+ */
330
+ liveMemory(): Promise<{ memory_bytes: number; memory_percent: number }>;
331
+ usage(tenantId: string, period: string): Promise<CachedResult<UsageResponse>>;
332
+ listEvents(tenantId: string, limit: number, cursor?: string): Promise<TelemetryEventListPage>;
333
+ getEvent(
334
+ tenantId: string,
335
+ eventId: string,
336
+ includePayload: boolean,
337
+ createdAt?: string,
338
+ ): Promise<TelemetryEventDetail | undefined>;
339
+ usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>>;
340
+ usageChart(tenantId: string, period: string): Promise<CachedResult<UsageChartResponse>>;
341
+ usageBy(
342
+ tenantId: string,
343
+ dimension: UsageDimension,
344
+ period: string,
345
+ ): Promise<CachedResult<UsageByResponse>>;
346
+ usageCache(tenantId: string, period: string): Promise<CachedResult<UsageCacheResponse>>;
347
+ usageRequests(
348
+ tenantId: string,
349
+ period: string,
350
+ limit: number,
351
+ httpStatus?: number,
352
+ ): Promise<CachedResult<UsageRequestsResponse>>;
353
+ usageRequestDetail(tenantId: string, requestId: string): Promise<UsageRequestDetail | undefined>;
354
+ }
355
+ export interface ObservabilityConfig {
356
+ readonly store: ObservabilityStore;
357
+ readonly accessResolver: ConsoleAccessResolver;
358
+ }
359
+
360
+ export function createObservabilityOperations(config: ObservabilityConfig) {
361
+ const operations = {
362
+ /**
363
+ * The cached reads below answer with a `Response` rather than a plain
364
+ * object, because a repeated poll is a `304` and a `304` has no body to
365
+ * serialize. `ifNoneMatch` is the client's `If-None-Match`; a match costs
366
+ * ~200 bytes instead of the whole payload.
367
+ */
368
+ async getSystemHealth(
369
+ access: AccessDecision | undefined,
370
+ ifNoneMatch: string | null,
371
+ ): Promise<Response> {
372
+ const a = requireTenantScope(access, "dashboard:read");
373
+ const result = await config.store.health(a.tenantId);
374
+ return conditionalResponse(ifNoneMatch, result, result.value);
375
+ },
376
+ async getTenantUsage(
377
+ access: AccessDecision | undefined,
378
+ period = "7d",
379
+ ifNoneMatch: string | null = null,
380
+ ): Promise<Response> {
381
+ const a = requireTenantScope(access, "dashboard:read");
382
+ if (!isSupportedUsagePeriod(period)) {
383
+ throw new ConsoleDomainError(
384
+ "invalid_period",
385
+ 400,
386
+ `Unsupported usage period: ${period}. Use "24h", "7d", "30d", or "all".`,
387
+ );
388
+ }
389
+ const result = await config.store.usage(a.tenantId, period);
390
+ return conditionalResponse(ifNoneMatch, result, result.value);
391
+ },
392
+ async listEvents(
393
+ access: AccessDecision | undefined,
394
+ limit?: string,
395
+ cursor?: string,
396
+ ): Promise<TelemetryEventListPage> {
397
+ const a = requireTenantScope(access, "dashboard:read");
398
+ const lim = parseQueryLimit(limit, 50, 100);
399
+ const page = await config.store.listEvents(a.tenantId, lim, cursor);
400
+ const entries = page.entries;
401
+ return page.nextCursor ? { entries, nextCursor: page.nextCursor } : { entries };
402
+ },
403
+ async getEventDetail(
404
+ access: AccessDecision | undefined,
405
+ eventId: string,
406
+ includePayload = false,
407
+ createdAt?: string,
408
+ ): Promise<TelemetryEventDetail> {
409
+ const a = requireTenantScope(access, "dashboard:read");
410
+ const detail = await config.store.getEvent(
411
+ a.tenantId,
412
+ eventId,
413
+ includePayload,
414
+ createdAt,
415
+ );
416
+ if (!detail) {
417
+ throw new ConsoleDomainError(
418
+ "event_not_found",
419
+ 404,
420
+ await detailNotFoundMessage(eventId, "Event"),
421
+ );
422
+ }
423
+ return detail;
424
+ },
425
+ requirePeriod(period: string): string {
426
+ if (!isSupportedUsagePeriod(period)) {
427
+ throw new ConsoleDomainError(
428
+ "invalid_period",
429
+ 400,
430
+ `Unsupported usage period: ${period}. Use "1h", "24h", "7d", "30d", or "all".`,
431
+ );
432
+ }
433
+ return period;
434
+ },
435
+ requireTenant(access: AccessDecision | undefined): string {
436
+ const a = requireTenantScope(access, "dashboard:read");
437
+ return a.tenantId;
438
+ },
439
+ async getUsageSummary(
440
+ access: AccessDecision | undefined,
441
+ period = "24h",
442
+ ifNoneMatch: string | null = null,
443
+ ): Promise<Response> {
444
+ const tenantId = operations.requireTenant(access);
445
+ const result = await config.store.usageSummary(tenantId, operations.requirePeriod(period));
446
+ return conditionalResponse(ifNoneMatch, result, result.value);
447
+ },
448
+ async getUsageChart(
449
+ access: AccessDecision | undefined,
450
+ period = "24h",
451
+ ifNoneMatch: string | null = null,
452
+ ): Promise<Response> {
453
+ const tenantId = operations.requireTenant(access);
454
+ const result = await config.store.usageChart(tenantId, operations.requirePeriod(period));
455
+ return conditionalResponse(ifNoneMatch, result, result.value);
456
+ },
457
+ async getUsageBy(
458
+ access: AccessDecision | undefined,
459
+ dimension: UsageDimension,
460
+ period = "24h",
461
+ ifNoneMatch: string | null = null,
462
+ ): Promise<Response> {
463
+ const tenantId = operations.requireTenant(access);
464
+ if (!(USAGE_DIMENSIONS as readonly string[]).includes(dimension)) {
465
+ throw new ConsoleDomainError(
466
+ "invalid_dimension",
467
+ 400,
468
+ `Unsupported usage dimension: ${dimension}. Use ${USAGE_DIMENSIONS.map((d) => `"${d}"`).join(", ")}.`,
469
+ );
470
+ }
471
+ const result = await config.store.usageBy(tenantId, dimension, operations.requirePeriod(period));
472
+ return conditionalResponse(ifNoneMatch, result, result.value);
473
+ },
474
+ async getUsageCache(
475
+ access: AccessDecision | undefined,
476
+ period = "24h",
477
+ ifNoneMatch: string | null = null,
478
+ ): Promise<Response> {
479
+ const tenantId = operations.requireTenant(access);
480
+ const result = await config.store.usageCache(tenantId, operations.requirePeriod(period));
481
+ return conditionalResponse(ifNoneMatch, result, result.value);
482
+ },
483
+ async listUsageRequests(
484
+ access: AccessDecision | undefined,
485
+ period = "24h",
486
+ limit?: string,
487
+ httpStatus?: string,
488
+ ifNoneMatch: string | null = null,
489
+ ): Promise<Response> {
490
+ const tenantId = operations.requireTenant(access);
491
+ let parsedStatus: number | undefined;
492
+ if (httpStatus !== undefined) {
493
+ parsedStatus = Number(httpStatus);
494
+ if (!Number.isInteger(parsedStatus) || parsedStatus < 100 || parsedStatus > 599) {
495
+ throw new ConsoleDomainError("invalid_request", 400, "Invalid HTTP status filter");
496
+ }
497
+ }
498
+ const result = await config.store.usageRequests(
499
+ tenantId,
500
+ operations.requirePeriod(period),
501
+ parseQueryLimit(limit, 50, 500),
502
+ parsedStatus,
503
+ );
504
+ return conditionalResponse(ifNoneMatch, result, result.value);
505
+ },
506
+ async getUsageRequestDetail(
507
+ access: AccessDecision | undefined,
508
+ requestId: string,
509
+ ): Promise<UsageRequestDetail> {
510
+ const tenantId = operations.requireTenant(access);
511
+ const detail = await config.store.usageRequestDetail(tenantId, requestId);
512
+ if (!detail) {
513
+ throw new ConsoleDomainError(
514
+ "event_not_found",
515
+ 404,
516
+ await detailNotFoundMessage(requestId),
517
+ );
518
+ }
519
+ const payloads = detail.payloads ?? null;
520
+ return { ...detail, payloads };
521
+ },
522
+ };
523
+ return operations;
524
+ }