cartethyia 2.4.6 → 2.5.1

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 (303) hide show
  1. package/README.md +45 -13
  2. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js +1 -0
  3. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-Bb9CNuoh.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-BXUaRCQQ.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
  8. package/dist/dashboard/assets/CombosPage-CisNZOUt.js +1 -0
  9. package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-BOpI_LOH.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-DAkdvJ4k.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-DeT-VXHd.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-CytxSeGP.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  25. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  26. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
  32. package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-8-eVadhc.js} +1 -1
  33. package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
  34. package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-uEIXPyGO.js} +1 -1
  35. package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +1 -0
  37. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-DqK5E26W.js} +4 -4
  39. package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  44. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  45. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  47. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
  49. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
  51. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
  53. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
  61. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  62. package/dist/dashboard/assets/entry--yj84zk3.js +4 -0
  63. package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
  65. package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
  66. package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
  67. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-D70pChfn.js → index-CcD1eSJ6.js} +2 -2
  71. package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
  73. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
  75. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
  77. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
  81. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  84. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  85. package/dist/dashboard/assets/providers/trae.webp +0 -0
  86. package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
  87. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  88. package/dist/dashboard/assets/select-XPJ-dIw3.js +41 -0
  89. package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
  90. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  91. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  92. package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
  93. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  94. package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
  95. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  96. package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
  97. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  98. package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
  99. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -52
  105. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  106. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  107. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  108. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  109. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  110. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  111. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  112. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  113. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  114. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  115. package/package.json +3 -2
  116. package/scripts/cli/command/console-menu.ts +30 -11
  117. package/scripts/cli/command/start.ts +77 -13
  118. package/scripts/cli/command/tray.ts +47 -8
  119. package/scripts/cli/command/update.ts +1 -1
  120. package/scripts/cli/gateway/instance-lock.ts +105 -0
  121. package/scripts/cli/gateway/spawn.ts +173 -101
  122. package/scripts/cli/ui/background.ts +110 -49
  123. package/src/boot/app.ts +20 -6
  124. package/src/boot/dependencies.ts +19 -0
  125. package/src/boot/elysia-typebox-bridge.ts +37 -0
  126. package/src/boot/lifecycle.ts +2 -0
  127. package/src/config/cooldowns.ts +138 -138
  128. package/src/config/providers.ts +5 -5
  129. package/src/config/runtime.ts +19 -3
  130. package/src/config/spec.ts +9 -0
  131. package/src/console/api-keys/contracts.ts +414 -391
  132. package/src/console/api-keys/routes.ts +880 -870
  133. package/src/console/auth/account-reset-store.ts +14 -0
  134. package/src/console/auth/account-reset.ts +3 -2
  135. package/src/console/auth/setup-service.ts +6 -3
  136. package/src/console/domain-registration.ts +9 -2
  137. package/src/console/observability/contracts.ts +475 -455
  138. package/src/console/observability/store.ts +1015 -952
  139. package/src/console/providers/catalog/contracts.ts +31 -0
  140. package/src/console/providers/catalog/provider-operations.ts +8 -0
  141. package/src/console/providers/catalog/store.ts +1532 -1525
  142. package/src/console/providers/oauth/callback-listener.ts +18 -3
  143. package/src/console/providers/oauth/routes.ts +38 -15
  144. package/src/console/quota/refresh.ts +19 -0
  145. package/src/console/updates/routes.ts +162 -4
  146. package/src/main.ts +3 -0
  147. package/src/network/http2-bidi.ts +85 -0
  148. package/src/network/outbound-fetch.ts +1 -26
  149. package/src/network/pool/agent.ts +478 -460
  150. package/src/network/pool/resolver.ts +1104 -544
  151. package/src/network/pool-health-machine.ts +12 -5
  152. package/src/observability/payload-capture.ts +344 -324
  153. package/src/persistence/db-pg.ts +9 -3
  154. package/src/persistence/db-sqlite.ts +497 -495
  155. package/src/persistence/health-event-flush.ts +137 -0
  156. package/src/persistence/migrate.ts +181 -129
  157. package/src/persistence/schema.ts +1360 -1295
  158. package/src/persistence/settings-revision.ts +25 -0
  159. package/src/persistence/shared-cache.ts +217 -0
  160. package/src/persistence/stores/share-store.ts +556 -528
  161. package/src/persistence/usage-flush.ts +61 -1
  162. package/src/protocol/canonical-model.ts +23 -1
  163. package/src/protocol/response/chat.ts +143 -10
  164. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  165. package/src/providers/default-registry.ts +173 -1
  166. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  167. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  168. package/src/providers/discovery/probing-service.ts +1022 -1001
  169. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
  170. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  171. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
  172. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  173. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  174. package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
  175. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  176. package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
  177. package/src/providers/integrations/claude/claude.ts +606 -582
  178. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  179. package/src/providers/integrations/cline/cline.ts +539 -468
  180. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  181. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  182. package/src/providers/integrations/commandcode/quota.ts +160 -0
  183. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  184. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  185. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  186. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  187. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  188. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  189. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  190. package/src/providers/integrations/cursor/cursor.ts +960 -0
  191. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  192. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  193. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  194. package/src/providers/integrations/github/github.ts +4 -4
  195. package/src/providers/integrations/hermes.ts +79 -8
  196. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  197. package/src/providers/integrations/kilo/kilo.ts +153 -146
  198. package/src/providers/integrations/opencode/provider.ts +532 -538
  199. package/src/providers/integrations/openrouter/provider.ts +139 -281
  200. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  201. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
  202. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  203. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  204. package/src/providers/integrations/qoder/qoder.ts +865 -730
  205. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  206. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  207. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  208. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  209. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  210. package/src/providers/integrations/trae/trae.ts +458 -0
  211. package/src/providers/operations/account-health-service.ts +107 -10
  212. package/src/providers/operations/client-versions.ts +133 -6
  213. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  214. package/src/providers/operations/provider-credential-service.ts +46 -7
  215. package/src/providers/provider-contracts.ts +76 -0
  216. package/src/providers/provider-metadata.ts +78 -6
  217. package/src/providers/provider-model-definition.ts +373 -349
  218. package/src/security/api-key-auth.ts +126 -8
  219. package/src/transport/contracts.ts +3 -0
  220. package/src/transport/dispatch/attempt-finalize.ts +32 -1
  221. package/src/transport/dispatch/attempt-loop.ts +530 -515
  222. package/src/transport/dispatch/generation.ts +88 -21
  223. package/src/transport/dispatch/leases.ts +99 -2
  224. package/src/transport/dispatch/proxy-request.ts +60 -3
  225. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  226. package/src/transport/dispatch/streaming/stream.ts +22 -2
  227. package/src/transport/dispatch/upstream.ts +19 -1
  228. package/src/transport/guards/auth-guard.ts +26 -1
  229. package/src/transport/guards/body-policy.ts +5 -4
  230. package/src/transport/request/preparer.ts +103 -0
  231. package/src/transport/request/state.ts +14 -0
  232. package/src/transport/routing/plan-gate.ts +45 -0
  233. package/src/transport/routing/route-catalog.ts +799 -777
  234. package/src/transport/routing/route-model.ts +52 -1
  235. package/src/transport/routing/router.ts +51 -0
  236. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
  237. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  238. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  239. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  240. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
  241. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  242. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  243. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  244. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  245. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  246. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  247. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
  248. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  249. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
  250. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  251. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  252. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  253. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
  254. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  255. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
  256. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  257. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
  258. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  259. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  260. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  261. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
  262. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  263. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  264. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
  265. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  266. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  267. package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
  268. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  269. package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
  270. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  271. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  272. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  273. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  274. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  275. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  276. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  277. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  278. package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
  279. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  280. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  281. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  282. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  283. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  284. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  285. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  286. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  287. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  288. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  289. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  290. package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
  291. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  292. package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
  293. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  294. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  295. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  296. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  297. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  298. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  299. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  300. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  301. package/src/network/tls/bun-client-hello.ts +0 -112
  302. /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
  303. /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
@@ -1,455 +1,475 @@
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
- cpu_percent: number;
49
- cpu_cores: number;
50
- pid: number;
51
- platform: string;
52
- request_count: number;
53
- error_count: number;
54
- latency_avg_ms: number;
55
- latency_p95_ms: number;
56
- latency_p99_ms: number;
57
- cache_hit_rate_percent: number;
58
- avg_tokens_per_sec: number;
59
- }
60
- export interface UsageResponse {
61
- tenantId: string;
62
- period: string;
63
- requestsTotal: number;
64
- requestsSucceeded: number;
65
- requestsFailed: number;
66
- tokensUsed: number;
67
- estimatedCost: number;
68
- topModels: Array<{ modelId: string; count: number }>;
69
- topProviders: Array<{ providerId: string; count: number }>;
70
- }
71
-
72
- /** Period-bound usage summary mirroring the 20-beta analytics surface. */
73
- export interface UsageSummaryTotals {
74
- requests: number;
75
- inputTokens: number;
76
- cachedTokens: number;
77
- outputTokens: number;
78
- /**
79
- * Failed and truncated requests. Client aborts (`cancelled`, rendered 499)
80
- * are expected client behavior, never an error, and are tracked separately.
81
- */
82
- errors: number;
83
- /** Client-cancelled requests (rendered `499`) — a sub-count, never an error. */
84
- cancelled: number;
85
- /** Streams that ended early (`502`); also included in `errors`. */
86
- truncated: number;
87
- /** Counts by actual HTTP status for the clickable Requests filters. */
88
- statusCounts: Array<{ status: number; count: number }>;
89
- /** Average gateway routing and preparation delay before upstream dispatch. */
90
- avgResolveMs: number;
91
- avgDurationMs: number;
92
- /** Nearest-rank latency percentiles over the selected usage period. */
93
- latencyP50Ms: number;
94
- latencyP95Ms: number;
95
- latencyP99Ms: number;
96
- /**
97
- * How many events each latency series was computed from. A percentile over
98
- * twenty requests and one over twenty thousand do not deserve the same
99
- * confidence, and the count is the only thing that says which it is.
100
- */
101
- latencySampleCount: number;
102
- /** Events carrying a resolve measurement; differs when a row never dispatched. */
103
- resolveSampleCount: number;
104
- estimatedCostUsd: number;
105
- /** True when completed rows without persisted cost exist in the window. */
106
- partial: boolean;
107
- /** Cache hit rate percentage (0-100). */
108
- cacheHitRate: number;
109
- /** Average tokens per second across the period. */
110
- avgTokensPerSec: number;
111
- }
112
- export interface UsageSummaryResponse {
113
- period: string;
114
- totals: UsageSummaryTotals;
115
- }
116
- export interface UsageChartBucket {
117
- /** Bucket start as ISO timestamp. */
118
- t: string;
119
- requests: number;
120
- input: number;
121
- cached: number;
122
- output: number;
123
- }
124
- export interface UsageChartResponse {
125
- buckets: UsageChartBucket[];
126
- }
127
- export interface UsageByRow {
128
- name: string;
129
- requests: number;
130
- input: number;
131
- output: number;
132
- cached: number;
133
- total: number;
134
- errors: number;
135
- /** Null when no row in the group carries persisted cost. */
136
- costUsd: number | null;
137
- /** Resolved API key label for the `key` dimension; absent otherwise. */
138
- label?: string;
139
- /** Cache hit rate percentage (0-100). */
140
- cacheHitRate: number;
141
- /** Average tokens per second for this dimension. */
142
- avgTokensPerSec: number;
143
- }
144
- /**
145
- * Breakdown dimensions the usage endpoint accepts. Declared in `./usage-dimensions`
146
- * and re-exported here, because three layers read the same binding: the route
147
- * table registers one `by-<dimension>` path per member, the operations validator
148
- * rejects anything outside it, and the dashboard's `Dimension` union mirrors it
149
- * (kept in sync by hand).
150
- */
151
- export { USAGE_DIMENSIONS, type UsageDimension };
152
-
153
- export interface UsageByResponse {
154
- /** The dimension these rows were grouped by, so a stale response can never be read as another tab's data. */
155
- dimension: UsageDimension;
156
- rows: UsageByRow[];
157
- }
158
- export interface UsageCacheResponse {
159
- period: string;
160
- inputTokens: number;
161
- cachedTokens: number;
162
- /** Always zero: cache-write tokens are not tracked by the telemetry schema. */
163
- cacheWriteTokens: number;
164
- hitRate: number;
165
- }
166
- export type UsageRequestMode = "stream" | "non_stream";
167
- export interface UsageRequestItem {
168
- requestId: string;
169
- surface?: string;
170
- providerId?: string;
171
- /** Provider account (credential) that served this request, if any. */
172
- accountId?: string;
173
- model?: string;
174
- /**
175
- * The model the upstream named in its own response, verbatim — not the one
176
- * the client asked for. Absent when the response carried no `model`; never
177
- * backfilled from `model`, because that would report the ask as the answer.
178
- */
179
- reportedModel?: string;
180
- endpoint?: string;
181
- apiKeyId?: string;
182
- userAgent?: string;
183
- clientName?: string;
184
- estimatedCost?: number;
185
- /** Provider-billed credits; absent when the upstream reported no credit field. */
186
- creditUsed?: number;
187
- /** Gateway-side time from request start to upstream dispatch, in ms. */
188
- resolveMs?: number;
189
- /** Resolved API key label; absent when the key is gone or unknown. */
190
- apiKeyLabel?: string;
191
- /** HTTP status the client actually received; absent on historical rows. */
192
- httpStatus?: number;
193
- status: string;
194
- errorKind?: string;
195
- /**
196
- * Which layer failed: `cartethyia` (the gateway), `upstream` (the provider),
197
- * or `network`. Absent for rows written before the column existed.
198
- */
199
- errorOrigin?: string;
200
- mode: UsageRequestMode;
201
- startedAt: string;
202
- clientIp?: string;
203
- durationMs?: number;
204
- inputTokens?: number;
205
- outputTokens?: number;
206
- cachedTokens?: number;
207
- reasoningTokens?: number;
208
- totalTokens?: number;
209
- ttfbMs?: number;
210
- tokensPerSec?: number;
211
- firstContentDeltaAtMs?: number;
212
- lastEventAtMs?: number;
213
- /**
214
- * Why this request's drawer payloads are empty, or `captured` when they are
215
- * not. Remote-diagnosis field: the four panels read blank for every reason
216
- * alike, so without this an operator cannot tell "capture is off" from
217
- * "capture is broken" without access to the server's logs. Absent on rows
218
- * written before the column existed.
219
- */
220
- payloadCaptureState?: string;
221
- }
222
- export interface UsageRequestsResponse {
223
- items: UsageRequestItem[];
224
- }
225
- export interface UsageRequestDetail extends UsageRequestItem {
226
- proxy?: string;
227
- payloads?: {
228
- request?: unknown;
229
- response?: unknown;
230
- /** Body Cartethyia returned to the client (post-translation). */
231
- clientResponse?: unknown;
232
- providerRequest?: unknown;
233
- providerResponse?: unknown;
234
- } | null;
235
- payloadSignals?: {
236
- toolCalls: number;
237
- images: number;
238
- attachments: number;
239
- };
240
- }
241
- export interface TelemetryEventView {
242
- id: string;
243
- createdAt: string;
244
- tenantId: string;
245
- requestId: string;
246
- sourceSurface?: string;
247
- requestedModel?: string;
248
- providerId?: string;
249
- latencyMs?: number;
250
- status?: string;
251
- inputTokens?: number;
252
- outputTokens?: number;
253
- estimatedCost?: number;
254
- firstContentDeltaAtMs?: number;
255
- lastEventAtMs?: number;
256
- }
257
- export interface TelemetryEventDetail extends TelemetryEventView {
258
- payload?: { requestBody?: unknown; responseBody?: unknown };
259
- }
260
- export interface TelemetryEventListPage {
261
- entries: readonly TelemetryEventView[];
262
- /** Opaque cursor for the next page; `undefined` when the tail is reached. */
263
- nextCursor?: string;
264
- }
265
- export interface ObservabilityStore {
266
- /**
267
- * Every member below that is served from the read-through cache returns a
268
- * {@link CachedResult}, carrying the strong validator for the bytes the
269
- * client would otherwise receive. A repeated poll is then a `304` instead of
270
- * a full payload. The reads that are per-row and already bounded — the event
271
- * list and the detail views — have no cache entry and no validator.
272
- */
273
- health(tenantId: string): Promise<CachedResult<SystemHealthResponse>>;
274
- usage(tenantId: string, period: string): Promise<CachedResult<UsageResponse>>;
275
- listEvents(tenantId: string, limit: number, cursor?: string): Promise<TelemetryEventListPage>;
276
- getEvent(
277
- tenantId: string,
278
- eventId: string,
279
- includePayload: boolean,
280
- createdAt?: string,
281
- ): Promise<TelemetryEventDetail | undefined>;
282
- usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>>;
283
- usageChart(tenantId: string, period: string): Promise<CachedResult<UsageChartResponse>>;
284
- usageBy(
285
- tenantId: string,
286
- dimension: UsageDimension,
287
- period: string,
288
- ): Promise<CachedResult<UsageByResponse>>;
289
- usageCache(tenantId: string, period: string): Promise<CachedResult<UsageCacheResponse>>;
290
- usageRequests(
291
- tenantId: string,
292
- period: string,
293
- limit: number,
294
- httpStatus?: number,
295
- ): Promise<CachedResult<UsageRequestsResponse>>;
296
- usageRequestDetail(tenantId: string, requestId: string): Promise<UsageRequestDetail | undefined>;
297
- }
298
- export interface ObservabilityConfig {
299
- readonly store: ObservabilityStore;
300
- readonly accessResolver: ConsoleAccessResolver;
301
- }
302
-
303
- export function createObservabilityOperations(config: ObservabilityConfig) {
304
- const operations = {
305
- /**
306
- * The cached reads below answer with a `Response` rather than a plain
307
- * object, because a repeated poll is a `304` and a `304` has no body to
308
- * serialize. `ifNoneMatch` is the client's `If-None-Match`; a match costs
309
- * ~200 bytes instead of the whole payload.
310
- */
311
- async getSystemHealth(
312
- access: AccessDecision | undefined,
313
- ifNoneMatch: string | null,
314
- ): Promise<Response> {
315
- const a = requireTenantScope(access, "dashboard:read");
316
- const result = await config.store.health(a.tenantId);
317
- return conditionalResponse(ifNoneMatch, result, result.value);
318
- },
319
- async getTenantUsage(
320
- access: AccessDecision | undefined,
321
- period = "7d",
322
- ifNoneMatch: string | null = null,
323
- ): Promise<Response> {
324
- const a = requireTenantScope(access, "dashboard:read");
325
- if (!isSupportedUsagePeriod(period)) {
326
- throw new ConsoleDomainError(
327
- "invalid_period",
328
- 400,
329
- `Unsupported usage period: ${period}. Use "24h", "7d", "30d", or "all".`,
330
- );
331
- }
332
- const result = await config.store.usage(a.tenantId, period);
333
- return conditionalResponse(ifNoneMatch, result, result.value);
334
- },
335
- async listEvents(
336
- access: AccessDecision | undefined,
337
- limit?: string,
338
- cursor?: string,
339
- ): Promise<TelemetryEventListPage> {
340
- const a = requireTenantScope(access, "dashboard:read");
341
- const lim = parseQueryLimit(limit, 50, 100);
342
- const page = await config.store.listEvents(a.tenantId, lim, cursor);
343
- const entries = page.entries;
344
- return page.nextCursor ? { entries, nextCursor: page.nextCursor } : { entries };
345
- },
346
- async getEventDetail(
347
- access: AccessDecision | undefined,
348
- eventId: string,
349
- includePayload = false,
350
- createdAt?: string,
351
- ): Promise<TelemetryEventDetail> {
352
- const a = requireTenantScope(access, "dashboard:read");
353
- const detail = await config.store.getEvent(
354
- a.tenantId,
355
- eventId,
356
- includePayload,
357
- createdAt,
358
- );
359
- if (!detail) throw new ConsoleDomainError("event_not_found", 404, `Event ${eventId} not found`);
360
- return detail;
361
- },
362
- requirePeriod(period: string): string {
363
- if (!isSupportedUsagePeriod(period)) {
364
- throw new ConsoleDomainError(
365
- "invalid_period",
366
- 400,
367
- `Unsupported usage period: ${period}. Use "1h", "24h", "7d", "30d", or "all".`,
368
- );
369
- }
370
- return period;
371
- },
372
- requireTenant(access: AccessDecision | undefined): string {
373
- const a = requireTenantScope(access, "dashboard:read");
374
- return a.tenantId;
375
- },
376
- async getUsageSummary(
377
- access: AccessDecision | undefined,
378
- period = "24h",
379
- ifNoneMatch: string | null = null,
380
- ): Promise<Response> {
381
- const tenantId = operations.requireTenant(access);
382
- const result = await config.store.usageSummary(tenantId, operations.requirePeriod(period));
383
- return conditionalResponse(ifNoneMatch, result, result.value);
384
- },
385
- async getUsageChart(
386
- access: AccessDecision | undefined,
387
- period = "24h",
388
- ifNoneMatch: string | null = null,
389
- ): Promise<Response> {
390
- const tenantId = operations.requireTenant(access);
391
- const result = await config.store.usageChart(tenantId, operations.requirePeriod(period));
392
- return conditionalResponse(ifNoneMatch, result, result.value);
393
- },
394
- async getUsageBy(
395
- access: AccessDecision | undefined,
396
- dimension: UsageDimension,
397
- period = "24h",
398
- ifNoneMatch: string | null = null,
399
- ): Promise<Response> {
400
- const tenantId = operations.requireTenant(access);
401
- if (!(USAGE_DIMENSIONS as readonly string[]).includes(dimension)) {
402
- throw new ConsoleDomainError(
403
- "invalid_dimension",
404
- 400,
405
- `Unsupported usage dimension: ${dimension}. Use ${USAGE_DIMENSIONS.map((d) => `"${d}"`).join(", ")}.`,
406
- );
407
- }
408
- const result = await config.store.usageBy(tenantId, dimension, operations.requirePeriod(period));
409
- return conditionalResponse(ifNoneMatch, result, result.value);
410
- },
411
- async getUsageCache(
412
- access: AccessDecision | undefined,
413
- period = "24h",
414
- ifNoneMatch: string | null = null,
415
- ): Promise<Response> {
416
- const tenantId = operations.requireTenant(access);
417
- const result = await config.store.usageCache(tenantId, operations.requirePeriod(period));
418
- return conditionalResponse(ifNoneMatch, result, result.value);
419
- },
420
- async listUsageRequests(
421
- access: AccessDecision | undefined,
422
- period = "24h",
423
- limit?: string,
424
- httpStatus?: string,
425
- ifNoneMatch: string | null = null,
426
- ): Promise<Response> {
427
- const tenantId = operations.requireTenant(access);
428
- let parsedStatus: number | undefined;
429
- if (httpStatus !== undefined) {
430
- parsedStatus = Number(httpStatus);
431
- if (!Number.isInteger(parsedStatus) || parsedStatus < 100 || parsedStatus > 599) {
432
- throw new ConsoleDomainError("invalid_request", 400, "Invalid HTTP status filter");
433
- }
434
- }
435
- const result = await config.store.usageRequests(
436
- tenantId,
437
- operations.requirePeriod(period),
438
- parseQueryLimit(limit, 50, 500),
439
- parsedStatus,
440
- );
441
- return conditionalResponse(ifNoneMatch, result, result.value);
442
- },
443
- async getUsageRequestDetail(
444
- access: AccessDecision | undefined,
445
- requestId: string,
446
- ): Promise<UsageRequestDetail> {
447
- const tenantId = operations.requireTenant(access);
448
- const detail = await config.store.usageRequestDetail(tenantId, requestId);
449
- if (!detail) throw new ConsoleDomainError("event_not_found", 404, `Request ${requestId} not found`);
450
- const payloads = detail.payloads ?? null;
451
- return { ...detail, payloads };
452
- },
453
- };
454
- return operations;
455
- }
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
+ cpu_percent: number;
49
+ cpu_cores: number;
50
+ pid: number;
51
+ platform: string;
52
+ request_count: number;
53
+ error_count: number;
54
+ latency_avg_ms: number;
55
+ latency_p95_ms: number;
56
+ latency_p99_ms: number;
57
+ cache_hit_rate_percent: number;
58
+ avg_tokens_per_sec: number;
59
+ }
60
+ export interface UsageResponse {
61
+ tenantId: string;
62
+ period: string;
63
+ requestsTotal: number;
64
+ requestsSucceeded: number;
65
+ requestsFailed: number;
66
+ tokensUsed: number;
67
+ estimatedCost: number;
68
+ topModels: Array<{ modelId: string; count: number }>;
69
+ topProviders: Array<{ providerId: string; count: number }>;
70
+ }
71
+
72
+ /** Period-bound usage summary mirroring the 20-beta analytics surface. */
73
+ export interface UsageSummaryTotals {
74
+ requests: number;
75
+ inputTokens: number;
76
+ cachedTokens: number;
77
+ outputTokens: number;
78
+ /**
79
+ * Failed and truncated requests. Client aborts (`cancelled`, rendered 499)
80
+ * are expected client behavior, never an error, and are tracked separately.
81
+ */
82
+ errors: number;
83
+ /** Client-cancelled requests (rendered `499`) — a sub-count, never an error. */
84
+ cancelled: number;
85
+ /** Streams that ended early (`502`); also included in `errors`. */
86
+ truncated: number;
87
+ /** Counts by actual HTTP status for the clickable Requests filters. */
88
+ statusCounts: Array<{ status: number; count: number }>;
89
+ /** Average gateway routing and preparation delay before upstream dispatch. */
90
+ avgResolveMs: number;
91
+ avgDurationMs: number;
92
+ /** Nearest-rank latency percentiles over the selected usage period. */
93
+ latencyP50Ms: number;
94
+ latencyP95Ms: number;
95
+ latencyP99Ms: number;
96
+ /**
97
+ * How many events each latency series was computed from. A percentile over
98
+ * twenty requests and one over twenty thousand do not deserve the same
99
+ * confidence, and the count is the only thing that says which it is.
100
+ */
101
+ latencySampleCount: number;
102
+ /** Events carrying a resolve measurement; differs when a row never dispatched. */
103
+ resolveSampleCount: number;
104
+ estimatedCostUsd: number;
105
+ /** True when completed rows without persisted cost exist in the window. */
106
+ partial: boolean;
107
+ /** Cache hit rate percentage (0-100). */
108
+ cacheHitRate: number;
109
+ /** Average tokens per second across the period. */
110
+ avgTokensPerSec: number;
111
+ }
112
+ export interface UsageSummaryResponse {
113
+ period: string;
114
+ totals: UsageSummaryTotals;
115
+ }
116
+ export interface UsageChartBucket {
117
+ /** Bucket start as ISO timestamp. */
118
+ t: string;
119
+ requests: number;
120
+ input: number;
121
+ cached: number;
122
+ output: number;
123
+ }
124
+ export interface UsageChartResponse {
125
+ buckets: UsageChartBucket[];
126
+ }
127
+ export interface UsageByRow {
128
+ name: string;
129
+ requests: number;
130
+ input: number;
131
+ output: number;
132
+ cached: number;
133
+ total: number;
134
+ errors: number;
135
+ /** Null when no row in the group carries persisted cost. */
136
+ costUsd: number | null;
137
+ /** Resolved API key label for the `key` dimension; absent otherwise. */
138
+ label?: string;
139
+ /** Cache hit rate percentage (0-100). */
140
+ cacheHitRate: number;
141
+ /** Average tokens per second for this dimension. */
142
+ avgTokensPerSec: number;
143
+ }
144
+ /**
145
+ * Breakdown dimensions the usage endpoint accepts. Declared in `./usage-dimensions`
146
+ * and re-exported here, because three layers read the same binding: the route
147
+ * table registers one `by-<dimension>` path per member, the operations validator
148
+ * rejects anything outside it, and the dashboard's `Dimension` union mirrors it
149
+ * (kept in sync by hand).
150
+ */
151
+ export { USAGE_DIMENSIONS, type UsageDimension };
152
+
153
+ export interface UsageByResponse {
154
+ /** The dimension these rows were grouped by, so a stale response can never be read as another tab's data. */
155
+ dimension: UsageDimension;
156
+ rows: UsageByRow[];
157
+ }
158
+ export interface UsageCacheResponse {
159
+ period: string;
160
+ inputTokens: number;
161
+ cachedTokens: number;
162
+ /** Always zero: cache-write tokens are not tracked by the telemetry schema. */
163
+ cacheWriteTokens: number;
164
+ hitRate: number;
165
+ }
166
+ export type UsageRequestMode = "stream" | "non_stream";
167
+ export interface UsageRequestItem {
168
+ requestId: string;
169
+ surface?: string;
170
+ providerId?: string;
171
+ /** Provider account (credential) that served this request, if any. */
172
+ accountId?: string;
173
+ model?: string;
174
+ /**
175
+ * The model the upstream named in its own response, verbatim — not the one
176
+ * the client asked for. Absent when the response carried no `model`; never
177
+ * backfilled from `model`, because that would report the ask as the answer.
178
+ */
179
+ reportedModel?: string;
180
+ endpoint?: string;
181
+ apiKeyId?: string;
182
+ userAgent?: string;
183
+ clientName?: string;
184
+ estimatedCost?: number;
185
+ /** Provider-billed credits; absent when the upstream reported no credit field. */
186
+ creditUsed?: number;
187
+ /** Gateway-side time from request start to upstream dispatch, in ms. */
188
+ resolveMs?: number;
189
+ /** Resolved API key label; absent when the key is gone or unknown. */
190
+ apiKeyLabel?: string;
191
+ /** HTTP status the client actually received; absent on historical rows. */
192
+ httpStatus?: number;
193
+ status: string;
194
+ errorKind?: string;
195
+ /**
196
+ * Which layer failed: `cartethyia` (the gateway), `upstream` (the provider),
197
+ * or `network`. Absent for rows written before the column existed.
198
+ */
199
+ errorOrigin?: string;
200
+ mode: UsageRequestMode;
201
+ startedAt: string;
202
+ clientIp?: string;
203
+ durationMs?: number;
204
+ inputTokens?: number;
205
+ outputTokens?: number;
206
+ cachedTokens?: number;
207
+ reasoningTokens?: number;
208
+ totalTokens?: number;
209
+ ttfbMs?: number;
210
+ tokensPerSec?: number;
211
+ firstContentDeltaAtMs?: number;
212
+ lastEventAtMs?: number;
213
+ /**
214
+ * Why this request's drawer payloads are empty, or `captured` when they are
215
+ * not. Remote-diagnosis field: the four panels read blank for every reason
216
+ * alike, so without this an operator cannot tell "capture is off" from
217
+ * "capture is broken" without access to the server's logs. Absent on rows
218
+ * written before the column existed.
219
+ */
220
+ payloadCaptureState?: string;
221
+ /**
222
+ * Bitmask of the payload stages captured for this request: 1 request,
223
+ * 2 provider request, 4 provider response, 8 client response. Read from the
224
+ * payload row so the list can mark each drawer panel without inflating a
225
+ * frame per row. Absent when nothing was captured for the request — the same
226
+ * state a pre-migration row reports, rendered as every stage uncaptured
227
+ * rather than as a measured zero.
228
+ */
229
+ payloadStages?: number;
230
+ }
231
+ export interface UsageRequestsResponse {
232
+ items: UsageRequestItem[];
233
+ }
234
+ export interface UsageRequestDetail extends UsageRequestItem {
235
+ proxy?: string;
236
+ payloads?: {
237
+ request?: unknown;
238
+ response?: unknown;
239
+ /** Body Cartethyia returned to the client (post-translation). */
240
+ clientResponse?: unknown;
241
+ providerRequest?: unknown;
242
+ providerResponse?: unknown;
243
+ } | null;
244
+ payloadSignals?: {
245
+ toolCalls: number;
246
+ images: number;
247
+ attachments: number;
248
+ };
249
+ }
250
+ export interface TelemetryEventView {
251
+ id: string;
252
+ createdAt: string;
253
+ tenantId: string;
254
+ requestId: string;
255
+ sourceSurface?: string;
256
+ requestedModel?: string;
257
+ providerId?: string;
258
+ latencyMs?: number;
259
+ status?: string;
260
+ inputTokens?: number;
261
+ outputTokens?: number;
262
+ estimatedCost?: number;
263
+ firstContentDeltaAtMs?: number;
264
+ lastEventAtMs?: number;
265
+ }
266
+ export interface TelemetryEventDetail extends TelemetryEventView {
267
+ payload?: { requestBody?: unknown; responseBody?: unknown };
268
+ }
269
+ export interface TelemetryEventListPage {
270
+ entries: readonly TelemetryEventView[];
271
+ /** Opaque cursor for the next page; `undefined` when the tail is reached. */
272
+ nextCursor?: string;
273
+ }
274
+ export interface ObservabilityStore {
275
+ /**
276
+ * Every member below that is served from the read-through cache returns a
277
+ * {@link CachedResult}, carrying the strong validator for the bytes the
278
+ * client would otherwise receive. A repeated poll is then a `304` instead of
279
+ * a full payload. The reads that are per-row and already bounded — the event
280
+ * list and the detail views — have no cache entry and no validator.
281
+ */
282
+ health(tenantId: string): Promise<CachedResult<SystemHealthResponse>>;
283
+ /**
284
+ * Process memory as it is right now, bypassing the read-through cache.
285
+ *
286
+ * `health` is cached for 10 s, which is correct for a dashboard tile and
287
+ * wrong for anything sampling a trend: a memory watcher polling on that same
288
+ * 10 s cadence reads the cached bytes often enough that consecutive samples
289
+ * can be identical, and a monotonic-growth test over identical samples is
290
+ * meaningless. The values are read in-process and cost nothing, so there is
291
+ * no reason to serve them stale.
292
+ */
293
+ liveMemory(): Promise<{ memory_bytes: number; memory_percent: number }>;
294
+ usage(tenantId: string, period: string): Promise<CachedResult<UsageResponse>>;
295
+ listEvents(tenantId: string, limit: number, cursor?: string): Promise<TelemetryEventListPage>;
296
+ getEvent(
297
+ tenantId: string,
298
+ eventId: string,
299
+ includePayload: boolean,
300
+ createdAt?: string,
301
+ ): Promise<TelemetryEventDetail | undefined>;
302
+ usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>>;
303
+ usageChart(tenantId: string, period: string): Promise<CachedResult<UsageChartResponse>>;
304
+ usageBy(
305
+ tenantId: string,
306
+ dimension: UsageDimension,
307
+ period: string,
308
+ ): Promise<CachedResult<UsageByResponse>>;
309
+ usageCache(tenantId: string, period: string): Promise<CachedResult<UsageCacheResponse>>;
310
+ usageRequests(
311
+ tenantId: string,
312
+ period: string,
313
+ limit: number,
314
+ httpStatus?: number,
315
+ ): Promise<CachedResult<UsageRequestsResponse>>;
316
+ usageRequestDetail(tenantId: string, requestId: string): Promise<UsageRequestDetail | undefined>;
317
+ }
318
+ export interface ObservabilityConfig {
319
+ readonly store: ObservabilityStore;
320
+ readonly accessResolver: ConsoleAccessResolver;
321
+ }
322
+
323
+ export function createObservabilityOperations(config: ObservabilityConfig) {
324
+ const operations = {
325
+ /**
326
+ * The cached reads below answer with a `Response` rather than a plain
327
+ * object, because a repeated poll is a `304` and a `304` has no body to
328
+ * serialize. `ifNoneMatch` is the client's `If-None-Match`; a match costs
329
+ * ~200 bytes instead of the whole payload.
330
+ */
331
+ async getSystemHealth(
332
+ access: AccessDecision | undefined,
333
+ ifNoneMatch: string | null,
334
+ ): Promise<Response> {
335
+ const a = requireTenantScope(access, "dashboard:read");
336
+ const result = await config.store.health(a.tenantId);
337
+ return conditionalResponse(ifNoneMatch, result, result.value);
338
+ },
339
+ async getTenantUsage(
340
+ access: AccessDecision | undefined,
341
+ period = "7d",
342
+ ifNoneMatch: string | null = null,
343
+ ): Promise<Response> {
344
+ const a = requireTenantScope(access, "dashboard:read");
345
+ if (!isSupportedUsagePeriod(period)) {
346
+ throw new ConsoleDomainError(
347
+ "invalid_period",
348
+ 400,
349
+ `Unsupported usage period: ${period}. Use "24h", "7d", "30d", or "all".`,
350
+ );
351
+ }
352
+ const result = await config.store.usage(a.tenantId, period);
353
+ return conditionalResponse(ifNoneMatch, result, result.value);
354
+ },
355
+ async listEvents(
356
+ access: AccessDecision | undefined,
357
+ limit?: string,
358
+ cursor?: string,
359
+ ): Promise<TelemetryEventListPage> {
360
+ const a = requireTenantScope(access, "dashboard:read");
361
+ const lim = parseQueryLimit(limit, 50, 100);
362
+ const page = await config.store.listEvents(a.tenantId, lim, cursor);
363
+ const entries = page.entries;
364
+ return page.nextCursor ? { entries, nextCursor: page.nextCursor } : { entries };
365
+ },
366
+ async getEventDetail(
367
+ access: AccessDecision | undefined,
368
+ eventId: string,
369
+ includePayload = false,
370
+ createdAt?: string,
371
+ ): Promise<TelemetryEventDetail> {
372
+ const a = requireTenantScope(access, "dashboard:read");
373
+ const detail = await config.store.getEvent(
374
+ a.tenantId,
375
+ eventId,
376
+ includePayload,
377
+ createdAt,
378
+ );
379
+ if (!detail) throw new ConsoleDomainError("event_not_found", 404, `Event ${eventId} not found`);
380
+ return detail;
381
+ },
382
+ requirePeriod(period: string): string {
383
+ if (!isSupportedUsagePeriod(period)) {
384
+ throw new ConsoleDomainError(
385
+ "invalid_period",
386
+ 400,
387
+ `Unsupported usage period: ${period}. Use "1h", "24h", "7d", "30d", or "all".`,
388
+ );
389
+ }
390
+ return period;
391
+ },
392
+ requireTenant(access: AccessDecision | undefined): string {
393
+ const a = requireTenantScope(access, "dashboard:read");
394
+ return a.tenantId;
395
+ },
396
+ async getUsageSummary(
397
+ access: AccessDecision | undefined,
398
+ period = "24h",
399
+ ifNoneMatch: string | null = null,
400
+ ): Promise<Response> {
401
+ const tenantId = operations.requireTenant(access);
402
+ const result = await config.store.usageSummary(tenantId, operations.requirePeriod(period));
403
+ return conditionalResponse(ifNoneMatch, result, result.value);
404
+ },
405
+ async getUsageChart(
406
+ access: AccessDecision | undefined,
407
+ period = "24h",
408
+ ifNoneMatch: string | null = null,
409
+ ): Promise<Response> {
410
+ const tenantId = operations.requireTenant(access);
411
+ const result = await config.store.usageChart(tenantId, operations.requirePeriod(period));
412
+ return conditionalResponse(ifNoneMatch, result, result.value);
413
+ },
414
+ async getUsageBy(
415
+ access: AccessDecision | undefined,
416
+ dimension: UsageDimension,
417
+ period = "24h",
418
+ ifNoneMatch: string | null = null,
419
+ ): Promise<Response> {
420
+ const tenantId = operations.requireTenant(access);
421
+ if (!(USAGE_DIMENSIONS as readonly string[]).includes(dimension)) {
422
+ throw new ConsoleDomainError(
423
+ "invalid_dimension",
424
+ 400,
425
+ `Unsupported usage dimension: ${dimension}. Use ${USAGE_DIMENSIONS.map((d) => `"${d}"`).join(", ")}.`,
426
+ );
427
+ }
428
+ const result = await config.store.usageBy(tenantId, dimension, operations.requirePeriod(period));
429
+ return conditionalResponse(ifNoneMatch, result, result.value);
430
+ },
431
+ async getUsageCache(
432
+ access: AccessDecision | undefined,
433
+ period = "24h",
434
+ ifNoneMatch: string | null = null,
435
+ ): Promise<Response> {
436
+ const tenantId = operations.requireTenant(access);
437
+ const result = await config.store.usageCache(tenantId, operations.requirePeriod(period));
438
+ return conditionalResponse(ifNoneMatch, result, result.value);
439
+ },
440
+ async listUsageRequests(
441
+ access: AccessDecision | undefined,
442
+ period = "24h",
443
+ limit?: string,
444
+ httpStatus?: string,
445
+ ifNoneMatch: string | null = null,
446
+ ): Promise<Response> {
447
+ const tenantId = operations.requireTenant(access);
448
+ let parsedStatus: number | undefined;
449
+ if (httpStatus !== undefined) {
450
+ parsedStatus = Number(httpStatus);
451
+ if (!Number.isInteger(parsedStatus) || parsedStatus < 100 || parsedStatus > 599) {
452
+ throw new ConsoleDomainError("invalid_request", 400, "Invalid HTTP status filter");
453
+ }
454
+ }
455
+ const result = await config.store.usageRequests(
456
+ tenantId,
457
+ operations.requirePeriod(period),
458
+ parseQueryLimit(limit, 50, 500),
459
+ parsedStatus,
460
+ );
461
+ return conditionalResponse(ifNoneMatch, result, result.value);
462
+ },
463
+ async getUsageRequestDetail(
464
+ access: AccessDecision | undefined,
465
+ requestId: string,
466
+ ): Promise<UsageRequestDetail> {
467
+ const tenantId = operations.requireTenant(access);
468
+ const detail = await config.store.usageRequestDetail(tenantId, requestId);
469
+ if (!detail) throw new ConsoleDomainError("event_not_found", 404, `Request ${requestId} not found`);
470
+ const payloads = detail.payloads ?? null;
471
+ return { ...detail, payloads };
472
+ },
473
+ };
474
+ return operations;
475
+ }