cartethyia 2.7.9 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/.env.example +3 -2
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  30. package/dist/dashboard/assets/{ProxyPage-BJ87Pp0U.js → ProxyPage-BwNC_-Cx.js} +1 -1
  31. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  44. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-CqTLw5e7.js +1 -0
  47. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
  49. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
  51. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
  53. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-CEL5Sg_J.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-HvBW0AjI.js} +1 -1
  61. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  62. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  63. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
  65. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
  67. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
  69. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
  71. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
  73. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
  75. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
  77. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
  79. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
  81. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
  83. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
  85. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
  87. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/zro.svg +3 -0
  89. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  90. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  91. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  92. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-BFQX2VBc.js} +1 -1
  93. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  94. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
  95. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  96. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
  97. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  98. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
  99. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  100. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
  101. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  102. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
  103. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  104. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
  105. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/dashboard/index.html.gz +0 -0
  108. package/dist/dashboard/precompressed-assets.json +54 -53
  109. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  110. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  111. package/package.json +1 -1
  112. package/src/boot/app.ts +10 -13
  113. package/src/boot/dependencies.ts +1 -1
  114. package/src/boot/flush.ts +20 -0
  115. package/src/boot/lifecycle.ts +7 -5
  116. package/src/config/routing.ts +26 -0
  117. package/src/config/spec-metadata.ts +72 -0
  118. package/src/config/spec.ts +20 -0
  119. package/src/console/api-keys/contracts.ts +12 -1
  120. package/src/console/api-keys/routes.ts +40 -28
  121. package/src/console/auth/account-reset.ts +7 -9
  122. package/src/console/backup/contracts.ts +4 -0
  123. package/src/console/backup/export-endpoint.ts +7 -5
  124. package/src/console/cli-tools/service.ts +6 -12
  125. package/src/console/console-router-guards.ts +23 -16
  126. package/src/console/domain-registration.ts +0 -2
  127. package/src/console/inferhub/normalize.ts +24 -28
  128. package/src/console/inferhub/types.ts +3 -0
  129. package/src/console/ip-bans/routes.ts +7 -13
  130. package/src/console/observability/store.ts +0 -53
  131. package/src/console/providers/catalog/contracts.ts +2 -2
  132. package/src/console/providers/catalog/discover-constants.ts +6 -0
  133. package/src/console/providers/catalog/routes.ts +1 -0
  134. package/src/console/providers/catalog/store.ts +21 -16
  135. package/src/console/quota/account-quota-global.ts +14 -24
  136. package/src/console/quota/account-quota-shared.ts +49 -0
  137. package/src/console/quota/account-quota-tenant.ts +6 -18
  138. package/src/console/quota/quota-view.ts +19 -3
  139. package/src/console/routing/pools/operations.ts +31 -46
  140. package/src/console/shared/errors.ts +33 -6
  141. package/src/main.ts +2 -3
  142. package/src/network/pool/byte-accounting.ts +3 -2
  143. package/src/network/pool/selector.ts +182 -39
  144. package/src/network/pool-cooldown-store.ts +89 -14
  145. package/src/network/pool-health.ts +49 -61
  146. package/src/network/rate-limit-verdict.ts +1 -1
  147. package/src/network/ssrf.ts +6 -6
  148. package/src/observability/event-log.ts +7 -361
  149. package/src/observability/performance-metrics.ts +9 -0
  150. package/src/observability/runtime-metrics.ts +14 -9
  151. package/src/observability/telemetry-buffer.ts +15 -25
  152. package/src/observability/telemetry-reconciliation.ts +9 -37
  153. package/src/observability/telemetry-row.ts +125 -0
  154. package/src/persistence/db-mode.ts +4 -1
  155. package/src/persistence/dialect.ts +18 -0
  156. package/src/persistence/migrate.ts +6 -3
  157. package/src/persistence/schema.ts +8 -0
  158. package/src/persistence/ttl-cache.ts +20 -2
  159. package/src/protocol/codecs.ts +1 -1
  160. package/src/protocol/dsml.ts +34 -86
  161. package/src/protocol/markup-stream-parser.ts +195 -0
  162. package/src/protocol/reasoning-text.ts +19 -0
  163. package/src/protocol/request/chat.ts +10 -10
  164. package/src/protocol/request/gemini.ts +8 -2
  165. package/src/protocol/request/messages.ts +3 -4
  166. package/src/protocol/response/chat.ts +12 -20
  167. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  168. package/src/protocol/response/responses.ts +18 -2
  169. package/src/protocol/stop-reason.ts +23 -1
  170. package/src/protocol/think-split.ts +112 -0
  171. package/src/protocol/translation/dialect/qwen.ts +26 -58
  172. package/src/providers/adapters/compatible-adapter.ts +71 -12
  173. package/src/providers/authentication/oauth-client.ts +1 -1
  174. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  175. package/src/providers/authentication/oauth-refresh-service.ts +30 -49
  176. package/src/providers/configured-provider.ts +148 -0
  177. package/src/providers/default-registry.ts +61 -36
  178. package/src/providers/discovery/probe-phases.ts +14 -4
  179. package/src/providers/discovery/probing-service.ts +15 -2
  180. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  181. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  182. package/src/providers/integrations/antigravity/antigravity-protocol.ts +5 -1
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
  184. package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
  185. package/src/providers/integrations/buddy/capabilities.ts +2 -2
  186. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
  187. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
  188. package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
  189. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  190. package/src/providers/integrations/cline/cline.ts +2 -2
  191. package/src/providers/integrations/commandcode/quota.ts +22 -28
  192. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  193. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  194. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  195. package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
  196. package/src/providers/integrations/cursor/cursor.ts +89 -40
  197. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  198. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  199. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  200. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  201. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  202. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  203. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  204. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  205. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  206. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  207. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  208. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  209. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  210. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  211. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  212. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  213. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  214. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +9 -9
  215. package/src/providers/integrations/hermes/index.ts +2 -0
  216. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  217. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  218. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  219. package/src/providers/integrations/kiro/kiro.ts +15 -19
  220. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  221. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  222. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  223. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  224. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  225. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  226. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  227. package/src/providers/integrations/perch/perch.ts +3 -13
  228. package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
  229. package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
  230. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  231. package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
  232. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  233. package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
  234. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
  235. package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
  236. package/src/providers/integrations/qoder/qoder.ts +0 -1
  237. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  238. package/src/providers/integrations/trae/trae-quota.ts +6 -28
  239. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  240. package/src/providers/integrations/trae/trae.ts +7 -24
  241. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  242. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  243. package/src/providers/integrations/zro/zro.ts +204 -0
  244. package/src/providers/operations/account-error-classification.ts +199 -307
  245. package/src/providers/operations/account-health-service.ts +82 -1
  246. package/src/providers/operations/account-refresh-status.ts +22 -37
  247. package/src/providers/operations/account-tier.ts +15 -16
  248. package/src/providers/operations/client-version-resolver.ts +1 -1
  249. package/src/providers/operations/client-versions.ts +5 -51
  250. package/src/providers/operations/cooldown-rules.ts +11 -16
  251. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  252. package/src/providers/operations/provider-catalog-service.ts +1 -1
  253. package/src/providers/operations/provider-credential-service.ts +102 -32
  254. package/src/providers/operations/provider-rules/github.ts +48 -0
  255. package/src/providers/operations/provider-rules/grok.ts +33 -0
  256. package/src/providers/operations/provider-rules/inception.ts +49 -0
  257. package/src/providers/operations/provider-rules/index.ts +32 -0
  258. package/src/providers/operations/provider-rules/perch.ts +37 -0
  259. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  260. package/src/providers/operations/provider-rules/trae.ts +81 -0
  261. package/src/providers/provider-contracts.ts +4 -2
  262. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  263. package/src/providers/provider-metadata.ts +40 -10
  264. package/src/providers/provider-probe.ts +15 -1
  265. package/src/providers/provider-registry.ts +42 -33
  266. package/src/providers/quota/quota-limit-windows.ts +16 -5
  267. package/src/providers/quota/quota-support.ts +1 -1
  268. package/src/providers/quota/quota-window-parser.ts +90 -18
  269. package/src/providers/quota/refresh.ts +75 -18
  270. package/src/security/api-key-auth-cache.ts +1 -1
  271. package/src/security/api-key-auth.ts +6 -2
  272. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  273. package/src/transport/dispatch/attempt-loop.ts +71 -17
  274. package/src/transport/dispatch/generation.ts +2 -8
  275. package/src/transport/dispatch/native-shared.ts +25 -0
  276. package/src/transport/dispatch/proxy-request.ts +8 -46
  277. package/src/transport/dispatch/responses-compact.ts +2 -8
  278. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  279. package/src/transport/dispatch/streaming/stream.ts +8 -36
  280. package/src/transport/dispatch/systemone.ts +2 -8
  281. package/src/transport/dispatch/token-count.ts +2 -6
  282. package/src/transport/dispatch/upstream.ts +63 -0
  283. package/src/transport/drain-endpoint.ts +31 -4
  284. package/src/transport/failure-policy.ts +16 -30
  285. package/src/transport/guards/body-policy.ts +7 -1
  286. package/src/transport/guards/error-lifecycle.ts +13 -1
  287. package/src/transport/guards/request-context.ts +22 -14
  288. package/src/transport/request/preparer.ts +81 -71
  289. package/src/transport/routing/bounded-lru.ts +14 -0
  290. package/src/transport/routing/drain-reserve.ts +129 -0
  291. package/src/transport/routing/plan-gate.ts +7 -1
  292. package/src/transport/routing/round-robin.ts +60 -0
  293. package/src/transport/routing/route-catalog.ts +123 -111
  294. package/src/transport/routing/route-model.ts +51 -9
  295. package/src/transport/routing/router.ts +393 -32
  296. package/src/transport/surface/adapters.ts +11 -2
  297. package/src/transport/surface/chat/adapter.ts +2 -2
  298. package/src/transport/surface/chat/encode.ts +47 -4
  299. package/src/transport/surface/chat/parse.ts +2 -12
  300. package/src/transport/surface/completion.ts +44 -33
  301. package/src/transport/surface/content-parts.ts +21 -0
  302. package/src/transport/surface/messages/adapter.ts +7 -1
  303. package/src/transport/surface/messages/encode.ts +11 -3
  304. package/src/transport/surface/messages/parse.ts +2 -1
  305. package/src/transport/surface/messages/stream.ts +33 -17
  306. package/src/transport/surface/responses/encode.ts +22 -5
  307. package/src/transport/surface/responses/parse.ts +3 -11
  308. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  309. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  311. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  312. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  313. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  314. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  315. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  316. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  320. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  321. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  322. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  324. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  325. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  326. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  327. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  328. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  330. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  333. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  334. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  336. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  337. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  338. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  339. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  340. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  341. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  342. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  343. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  344. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  347. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  348. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  349. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  354. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  355. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  356. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  357. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  358. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  359. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  360. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  361. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  362. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  363. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  364. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  365. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  366. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  367. package/src/providers/bundled-discovery-providers.ts +0 -98
  368. package/src/providers/configured-openai-providers.ts +0 -120
  369. package/src/providers/operations/model-discovery-cache.ts +0 -34
  370. package/src/providers/operations/probe-fetch.ts +0 -15
  371. package/src/providers/operations/provider-version-cache.ts +0 -30
  372. package/src/providers/provider-capabilities.ts +0 -33
  373. package/src/providers/provider-credential-envelope.ts +0 -43
  374. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-DIXLjojg.js} +0 -0
  375. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -1,12 +1,11 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import type { CartethyiaDatabase } from "../persistence/db-handle";
3
- import { healthEvents } from "../persistence/schema";
4
2
  import type { NetworkPoolSelector } from "./pool/selector";
5
- import type { GatewayError } from "../protocol/gateway-error";
3
+ import { GatewayError } from "../protocol/gateway-error";
6
4
  import { metrics } from "../observability/metrics";
7
5
  import { log } from "../observability/logger";
8
- import { upsertProviderCooldownMarker } from "./pool-cooldown-store";
6
+ import { appendPoolProviderEgressAuditRow, upsertProviderCooldownMarker } from "./pool-cooldown-store";
9
7
  import { resolvePoolProviderRateLimitCooldownMs } from "../config/cooldowns";
8
+ import type { EgressVerdict } from "./rate-limit-verdict";
10
9
 
11
10
  /**
12
11
  * Durable pool rate-limit history.
@@ -29,9 +28,6 @@ import { resolvePoolProviderRateLimitCooldownMs } from "../config/cooldowns";
29
28
  * needs one row per cooldown episode — not one per throttled request.
30
29
  */
31
30
  const POOL_COOLDOWN_EVENT_WINDOW_MS = 15 * 60 * 1000;
32
- const MAX_TRACKED_PAIRS = 5_000;
33
-
34
- const lastRecordedByPoolProvider = new Map<string, number>();
35
31
 
36
32
  /** Durable half of `flagPoolCooldown`; not a second public entry point. */
37
33
  async function recordPoolCooldownEvent(
@@ -40,35 +36,13 @@ async function recordPoolCooldownEvent(
40
36
  providerId: string,
41
37
  reason: string,
42
38
  ): Promise<void> {
43
- const key = `${poolId}::${providerId.toLowerCase()}`;
44
- const now = Date.now();
45
- const last = lastRecordedByPoolProvider.get(key);
46
- if (last !== undefined && now - last < POOL_COOLDOWN_EVENT_WINDOW_MS) return;
47
- try {
48
- await db.insert(healthEvents).values({
49
- id: randomUUID(),
50
- entityKind: "pool",
51
- networkPoolId: poolId,
52
- fromStatus: null,
53
- toStatus: "cooldown",
54
- reason: `provider ${providerId}: ${reason}`,
55
- errorCategory: "rate_limit_transient",
56
- createdAt: new Date(),
57
- });
58
- } catch {
59
- // Non-fatal: observability writes must never fail dispatch. The
60
- // coalescing stamp is only written below, so a failed insert does not
61
- // suppress the next attempt for the whole window.
62
- return;
63
- }
64
- lastRecordedByPoolProvider.set(key, now);
65
- if (lastRecordedByPoolProvider.size > MAX_TRACKED_PAIRS) {
66
- for (const [trackedKey, recordedAt] of lastRecordedByPoolProvider) {
67
- if (now - recordedAt >= POOL_COOLDOWN_EVENT_WINDOW_MS) {
68
- lastRecordedByPoolProvider.delete(trackedKey);
69
- }
70
- }
71
- }
39
+ await appendPoolProviderEgressAuditRow(db, {
40
+ poolId,
41
+ providerId,
42
+ reason,
43
+ blocked: false,
44
+ coalesceWindowMs: POOL_COOLDOWN_EVENT_WINDOW_MS,
45
+ });
72
46
  }
73
47
 
74
48
  /**
@@ -106,14 +80,7 @@ export function flagPoolBlocked(
106
80
  error: String(cause),
107
81
  });
108
82
  });
109
- void recordPoolBlockEvent(db, poolId, providerId, reason, error).catch((cause: unknown) => {
110
- metrics.pool_cooldown_record_failed.inc(1);
111
- log.warn("pool block event failed", {
112
- networkPoolId: poolId,
113
- providerId,
114
- error: String(cause),
115
- });
116
- });
83
+ void recordPoolBlockEvent(db, poolId, providerId, reason, error);
117
84
  }
118
85
 
119
86
  /**
@@ -131,15 +98,12 @@ async function recordPoolBlockEvent(
131
98
  reason: string,
132
99
  error: GatewayError,
133
100
  ): Promise<void> {
134
- await db.insert(healthEvents).values({
135
- id: randomUUID(),
136
- entityKind: "pool",
137
- networkPoolId: poolId,
138
- fromStatus: null,
139
- toStatus: "cooldown",
140
- reason: `provider ${providerId}: ${reason}${providerCodeSuffix(error)}`,
141
- errorCategory: "egress_blocked",
142
- createdAt: new Date(),
101
+ await appendPoolProviderEgressAuditRow(db, {
102
+ poolId,
103
+ providerId,
104
+ reason,
105
+ blocked: true,
106
+ codeSuffix: providerCodeSuffix(error),
143
107
  });
144
108
  }
145
109
 
@@ -199,12 +163,36 @@ export function flagPoolCooldown(
199
163
  error: String(cause),
200
164
  });
201
165
  });
202
- void recordPoolCooldownEvent(db, poolId, providerId, reason).catch((cause: unknown) => {
203
- metrics.pool_cooldown_record_failed.inc(1);
204
- log.warn("pool cooldown event failed", {
205
- networkPoolId: poolId,
206
- providerId,
207
- error: String(cause),
208
- });
209
- });
166
+ void recordPoolCooldownEvent(db, poolId, providerId, reason);
167
+ }
168
+
169
+ /**
170
+ * One egress-failure verdict → pool-flag dispatch, shared by the attempt loop
171
+ * and the streaming finalizer.
172
+ *
173
+ * Both paths classify the same failure with `classifyEgressFailure`, then must
174
+ * retire the same pool the same way; a policy change — which verdict maps to a
175
+ * cooldown vs a block, or a new verdict — belongs here so the two response
176
+ * modes cannot drift in telemetry.
177
+ *
178
+ * The call sites compute the verdict (the attempt loop also needs it for its
179
+ * retry decision) and pass it in; the gating is identical to what both
180
+ * previously applied by hand: only a real pool id + selector pair, only a
181
+ * `GatewayError`, and never a `"none"` verdict. The flags themselves are
182
+ * fire-and-forget advisory writes that never fail the request.
183
+ */
184
+ export function flagPoolOnEgressFailure(
185
+ poolSelector: NetworkPoolSelector | undefined,
186
+ db: CartethyiaDatabase,
187
+ poolId: string | undefined,
188
+ providerId: string,
189
+ error: unknown,
190
+ verdict: EgressVerdict,
191
+ ): void {
192
+ if (poolId === undefined || poolSelector === undefined || !(error instanceof GatewayError)) return;
193
+ if (verdict.kind === "cooldown") {
194
+ flagPoolCooldown(poolSelector, db, poolId, providerId, error);
195
+ } else if (verdict.kind === "blocked") {
196
+ flagPoolBlocked(poolSelector, db, poolId, providerId, error);
197
+ }
210
198
  }
@@ -21,7 +21,7 @@ import { GatewayError } from "../protocol/gateway-error";
21
21
  import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
22
22
 
23
23
  /** What one upstream failure means for the egress that carried it. */
24
- type EgressVerdict =
24
+ export type EgressVerdict =
25
25
  | { kind: "none" }
26
26
  | { kind: "cooldown"; retryAfterMs?: number }
27
27
  | { kind: "blocked" };
@@ -3,6 +3,7 @@ import { lookup } from "node:dns/promises";
3
3
  import { isIP } from "node:net";
4
4
  import { GatewayError } from "../protocol/gateway-error";
5
5
  import type { SsrfPolicy } from "../config/network";
6
+ import { BoundedLruMap } from "../transport/routing/bounded-lru";
6
7
  import {
7
8
  cidrContainsAddress,
8
9
  ipv4ToNumberStrict,
@@ -12,17 +13,16 @@ import {
12
13
 
13
14
  const DNS_CACHE_TTL_MS = 60_000;
14
15
  const DNS_CACHE_MAX = 64;
15
- const dnsCache = new Map<string, { addrs: string[]; at: number }>();
16
+ // Capacity-bounded: `set` re-inserts the host (moving it to the tail) and
17
+ // evicts the least-recently-used entry when over the cap — the same
18
+ // insertion-order eviction the hand-rolled loop performed, since every write
19
+ // here re-inserts.
20
+ const dnsCache = new BoundedLruMap<string, { addrs: string[]; at: number }>(DNS_CACHE_MAX);
16
21
 
17
22
  /** Bounds cache memory: evicts oldest entries once size exceeds the cap, not just expired ones. */
18
23
  function cacheDnsResult(hostname: string, addrs: string[], now: number): void {
19
24
  dnsCache.delete(hostname);
20
25
  dnsCache.set(hostname, { addrs, at: now });
21
- while (dnsCache.size > DNS_CACHE_MAX) {
22
- const oldest = dnsCache.keys().next().value;
23
- if (oldest === undefined) break;
24
- dnsCache.delete(oldest);
25
- }
26
26
  }
27
27
 
28
28
 
@@ -1,15 +1,15 @@
1
- import { open, readdir } from "node:fs/promises";
2
1
  import { join } from "node:path";
3
- import { resolveTelemetryRetentionDays } from "../config/telemetry";
4
2
  import { resolveDataDir } from "../persistence/db-mode";
5
- import { hourStamp } from "../shared/time";
6
3
 
7
4
  /**
8
- * Per-request telemetry lives in hourly JSONL files, not in Postgres. The
9
- * database keeps only the durable aggregate; the files are the single source
10
- * for recent requests and readers must go through this module, not a
11
- * `telemetry_events` table.
5
+ * Per-request telemetry is mirrored to hourly JSONL files and the durable
6
+ * `telemetry_events` table. This module is the shared home for the raw-file
7
+ * directory constant and the raw record type; the canonical reader is
8
+ * `event-query.ts` (SQL), and the reconciliation job (`telemetry-reconciliation.ts`)
9
+ * replays the raw mirror. The file-based reader that once lived here is gone —
10
+ * it was used only by tests and by this module itself.
12
11
  */
12
+
13
13
  /**
14
14
  * Where raw hourly event files live.
15
15
  *
@@ -64,357 +64,3 @@ export interface TelemetryEventRecord {
64
64
  */
65
65
  readonly degenerateStop?: boolean;
66
66
  }
67
-
68
- function asString(value: unknown): string | null {
69
- return typeof value === "string" ? value : null;
70
- }
71
-
72
- function asNumber(value: unknown): number | null {
73
- if (typeof value === "number" && Number.isFinite(value)) return value;
74
- if (typeof value === "string" && value.length > 0 && Number.isFinite(Number(value))) return Number(value);
75
- return null;
76
- }
77
-
78
- function normalize(raw: Record<string, unknown>): TelemetryEventRecord | undefined {
79
- const tenantId = asString(raw["tenantId"]);
80
- const requestId = asString(raw["requestId"]);
81
- const createdAt = asString(raw["createdAt"]);
82
- // No time means the row cannot be ordered or aged out. Inventing one made
83
- // every undated row reappear as "just now" on each read.
84
- if (!tenantId || !requestId || !createdAt) return undefined;
85
- return {
86
- id: asString(raw["id"]) ?? requestId,
87
- createdAt,
88
- tenantId,
89
- requestId,
90
- sourceSurface: asString(raw["sourceSurface"]),
91
- requestedModel: asString(raw["requestedModel"]),
92
- reportedModel: asString(raw["reportedModel"]),
93
- requestedEffort: asString(raw["requestedEffort"]),
94
- providerId: asString(raw["providerId"]),
95
- accountId: asString(raw["accountId"]),
96
- networkPoolId: asString(raw["networkPoolId"]),
97
- endpoint: asString(raw["endpoint"]),
98
- apiKeyId: asString(raw["apiKeyId"]),
99
- userAgent: asString(raw["userAgent"]),
100
- clientIp: asString(raw["clientIp"]),
101
- latencyMs: asNumber(raw["latencyMs"]),
102
- ttfbMs: asNumber(raw["ttfbMs"]),
103
- stream: typeof raw["stream"] === "boolean" ? raw["stream"] : null,
104
- status: asString(raw["status"]),
105
- httpStatus: asNumber(raw["httpStatus"]),
106
- errorOrigin: asString(raw["errorOrigin"]),
107
- errorCategory: asString(raw["errorCategory"]),
108
- inputTokens: asNumber(raw["inputTokens"]),
109
- cachedInputTokens: asNumber(raw["cachedInputTokens"]),
110
- outputTokens: asNumber(raw["outputTokens"]),
111
- reasoningTokens: asNumber(raw["reasoningTokens"]),
112
- estimatedCostUsd: asNumber(raw["estimatedCostUsd"]),
113
- creditUsed: asNumber(raw["creditUsed"]),
114
- resolveMs: asNumber(raw["resolveMs"]),
115
- tokensPerSec: asNumber(raw["tokensPerSec"]),
116
- firstContentDeltaAtMs: asNumber(raw["firstContentDeltaAtMs"]),
117
- lastEventAtMs: asNumber(raw["lastEventAtMs"]),
118
- ...(raw["degenerateStop"] === true ? { degenerateStop: true } : {}),
119
- };
120
- }
121
-
122
- /**
123
- * The comparable hour of an event file's name.
124
- *
125
- * A name is either `2026-10-22T05-15.jsonl` (an older window file) or
126
- * `2026-10-22.events.jsonl` (a day file). Both sort against `hourStamp`: only
127
- * the leading date and hour are taken, so the day file's `.events` suffix cannot
128
- * make it compare as later or earlier than the window files beside it.
129
- */
130
- function eventFileHour(file: string): string {
131
- const stamp = file.endsWith(".events.jsonl") ? `${file.slice(0, 10)}T00` : file.slice(0, 13);
132
- return stamp;
133
- }
134
-
135
- /** Chunk size for the streaming scan: one line can be long, a file cannot. */
136
- const READ_CHUNK_BYTES = 256 * 1024;
137
-
138
- /**
139
- * The newest matching events in one file, read backwards from the end.
140
- *
141
- * Reads fixed blocks from the tail until the page is full, so a day file of any
142
- * size costs one block per page rather than a full parse. A partial line at the
143
- * very end is dropped: the writer may still be appending to it.
144
- */
145
- async function* readEventFileTail(
146
- file: string,
147
- tenantId: string | undefined,
148
- want: number,
149
- since: number | undefined,
150
- until: number | undefined,
151
- ): AsyncGenerator<TelemetryEventRecord> {
152
- if (want <= 0) return;
153
- const handle = await open(join(TELEMETRY_EVENT_DIR, file), "r").catch(() => undefined);
154
- if (handle === undefined) return;
155
- try {
156
- const size = (await handle.stat().catch(() => undefined))?.size ?? 0;
157
- if (size === 0) return;
158
- const BLOCK = 64 * 1024;
159
- let end = size;
160
- let carry = "";
161
- const found: TelemetryEventRecord[] = [];
162
- while (end > 0 && found.length < want) {
163
- const start = Math.max(0, end - BLOCK);
164
- const length = end - start;
165
- const buffer = Buffer.alloc(length);
166
- await handle.read(buffer, 0, length, start);
167
- const text = buffer.toString("utf8") + carry;
168
- const lines = text.split("\n");
169
- carry = lines.shift() ?? "";
170
- // Newest first: the block was read from the end, so its lines run
171
- // backwards through the file.
172
- for (let index = lines.length - 1; index >= 0; index -= 1) {
173
- const line = lines[index] ?? "";
174
- if (line.length === 0) continue;
175
- if (tenantId !== undefined && !line.includes(tenantId)) continue;
176
- let parsed: TelemetryEventRecord | undefined;
177
- try {
178
- const value: unknown = JSON.parse(line);
179
- if (typeof value === "object" && value !== null) {
180
- parsed = value as TelemetryEventRecord;
181
- }
182
- } catch {
183
- continue;
184
- }
185
- if (parsed === undefined) continue;
186
- if (parsed.tenantId !== tenantId) continue;
187
- const at = Date.parse(parsed.createdAt);
188
- if (Number.isFinite(at)) {
189
- if (since !== undefined && at < since) continue;
190
- if (until !== undefined && at > until) continue;
191
- }
192
- found.push(parsed);
193
- if (found.length >= want) break;
194
- }
195
- end = start;
196
- }
197
- for (const event of found) yield event;
198
- } finally {
199
- await handle.close().catch(() => undefined);
200
- }
201
- }
202
-
203
- async function* readEventFiles(
204
- files: readonly string[],
205
- allow?: (line: string) => boolean,
206
- ): AsyncGenerator<TelemetryEventRecord> {
207
- for (const file of files) {
208
- const handle = await open(join(TELEMETRY_EVENT_DIR, file), "r").catch(() => undefined);
209
- if (handle === undefined) continue;
210
- // Chunked, because `readFile` + `split` held the whole file as a string plus
211
- // an array of one string per line — several times the file's size, per
212
- // concurrent read, on the same process that serves every request. Peak
213
- // memory is now one chunk and one line.
214
- const decoder = new TextDecoder();
215
- const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
216
- let carry = "";
217
- try {
218
- for (;;) {
219
- const { bytesRead } = await handle.read(buffer, 0, READ_CHUNK_BYTES);
220
- if (bytesRead === 0) break;
221
- const text = carry + decoder.decode(buffer.subarray(0, bytesRead), { stream: true });
222
- const lines = text.split(String.fromCharCode(10));
223
- // The tail is a partial line unless the chunk ended exactly on a newline.
224
- carry = lines.pop() ?? "";
225
- for (const line of lines) {
226
- if (!line) continue;
227
- // A line that cannot match is skipped before `JSON.parse`. The log is
228
- // shared by every tenant, so most of its bytes belong to someone else —
229
- // parsing them was the entire cost of a console read.
230
- if (allow !== undefined && !allow(line)) continue;
231
- try {
232
- const event = normalize(JSON.parse(line) as Record<string, unknown>);
233
- if (event) yield event;
234
- } catch {
235
- // A torn tail from a crash is skipped, not fatal. The next line stands alone.
236
- }
237
- }
238
- }
239
- if (carry.length > 0) {
240
- const line = carry + decoder.decode();
241
- if (allow === undefined || allow(line)) {
242
- try {
243
- const event = normalize(JSON.parse(line) as Record<string, unknown>);
244
- if (event) yield event;
245
- } catch {
246
- // A torn tail from a crash is skipped, not fatal.
247
- }
248
- }
249
- }
250
- } finally {
251
- await handle.close();
252
- }
253
- }
254
- }
255
-
256
- /** Reads every retained event, newest first. A missing directory is an empty log. */
257
- export async function readTelemetryEvents(): Promise<TelemetryEventRecord[]> {
258
- const files = await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]);
259
- const events: TelemetryEventRecord[] = [];
260
- for await (const event of readEventFiles(files.filter((file) => file.endsWith(".jsonl")))) events.push(event);
261
- events.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
262
- return events;
263
- }
264
-
265
- interface EventQuery {
266
- readonly tenantId?: string;
267
- readonly since?: Date;
268
- /**
269
- * Exclusive upper bound on `createdAt`, used to push a pagination cursor into
270
- * the query instead of filtering after the whole window was parsed. Without
271
- * it a cursor page drops `limit` — the reader cannot know where the page
272
- * ends — so only the first page of a list is bounded.
273
- */
274
- readonly until?: Date;
275
- readonly apiKeyIds?: readonly string[];
276
- readonly accountIds?: readonly string[];
277
- readonly requestId?: string;
278
- /**
279
- * Distinct from {@link requestId}: `id` defaults to the request id when a row
280
- * does not carry one of its own (`event-log.ts:68`), so a row can match by
281
- * either. A lookup by `id` short-circuits like `requestId` does, which is the
282
- * whole point — without it a single-event detail read parses the retention
283
- * window and then scans it in JS.
284
- */
285
- readonly id?: string;
286
- readonly httpStatus?: number;
287
- readonly limit?: number;
288
- }
289
-
290
- function matches(event: TelemetryEventRecord, query: EventQuery, since: number | undefined, until: number | undefined, keys: ReadonlySet<string> | undefined, accounts: ReadonlySet<string> | undefined): boolean {
291
- if (query.tenantId !== undefined && event.tenantId !== query.tenantId) return false;
292
- const at = new Date(event.createdAt).getTime();
293
- if (since !== undefined && at < since) return false;
294
- if (until !== undefined && at > until) return false;
295
- if (keys && (event.apiKeyId === null || !keys.has(event.apiKeyId))) return false;
296
- if (accounts && (event.accountId === null || !accounts.has(event.accountId))) return false;
297
- if (query.requestId !== undefined && event.requestId !== query.requestId) return false;
298
- if (query.id !== undefined && event.id !== query.id) return false;
299
- if (query.httpStatus !== undefined && event.httpStatus !== query.httpStatus) return false;
300
- return true;
301
- }
302
-
303
- /** The events matching a query, already newest first. */
304
- export async function queryTelemetryEvents(query: EventQuery): Promise<TelemetryEventRecord[]> {
305
- // Hourly request files only. A `.compacted.jsonl` also ends in `.jsonl`, and
306
- // letting one through meant every summary row was parsed as if it were a
307
- // request — a row with no `tenantId`/`createdAt` that then failed `matches`,
308
- // but only after paying for the parse.
309
- //
310
- // The lower bound is the retention window, not just `since`: a caller that
311
- // asks for all time still must not scan files that have already been aged
312
- // out, and one that passes no `since` at all must not read every file ever
313
- // written. Retention removes whole raw files; no summary is substituted.
314
- const retentionFloor = hourStamp(
315
- new Date(Date.now() - resolveTelemetryRetentionDays() * 24 * 60 * 60_000),
316
- );
317
- const sinceFloor =
318
- query.since === undefined ? retentionFloor : hourStamp(query.since);
319
- const floor = sinceFloor > retentionFloor ? sinceFloor : retentionFloor;
320
- const files = (await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]))
321
- .filter(
322
- (file) =>
323
- file.endsWith(".jsonl") &&
324
- !file.endsWith(".compacted.jsonl") &&
325
- eventFileHour(file) >= floor,
326
- )
327
- .sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
328
- const since = query.since?.getTime();
329
- const until = query.until?.getTime();
330
- const keys = query.apiKeyIds ? new Set(query.apiKeyIds) : undefined;
331
- const accounts = query.accountIds ? new Set(query.accountIds) : undefined;
332
- const events: TelemetryEventRecord[] = [];
333
- // The tenant and request ids are plain substrings of the stored line, so they
334
- // are checked on the raw text: a UUID appearing anywhere in the row is that
335
- // field, and a false negative is not possible while a false positive costs
336
- // only the parse it would have paid anyway.
337
- const allow =
338
- query.tenantId === undefined && query.requestId === undefined && query.id === undefined
339
- ? undefined
340
- : (line: string): boolean =>
341
- (query.tenantId === undefined || line.includes(query.tenantId)) &&
342
- (query.requestId === undefined || line.includes(query.requestId)) &&
343
- (query.id === undefined || line.includes(query.id));
344
- // A single-row lookup: stop at the first (newest) match instead of parsing
345
- // the whole retention window.
346
- const oneRow = query.requestId !== undefined || query.id !== undefined;
347
- // With one file per hour, a plain page of the newest rows does not need to
348
- // parse the whole retention window to return fifty lines. When nothing filters on
349
- // content — no status, no key, no account — the newest rows are the last
350
- // lines, so they are read backwards from the end and the scan stops once the
351
- // page is full.
352
- const tailOnly =
353
- query.limit !== undefined &&
354
- !oneRow &&
355
- query.httpStatus === undefined &&
356
- query.apiKeyIds === undefined &&
357
- query.accountIds === undefined;
358
- if (tailOnly) {
359
- const newest: TelemetryEventRecord[] = [];
360
- for (const file of files) {
361
- for await (const event of readEventFileTail(file, query.tenantId, query.limit - newest.length, since, until)) {
362
- newest.push(event);
363
- }
364
- if (newest.length >= query.limit) break;
365
- }
366
- return newest.slice(0, query.limit);
367
- }
368
- for (const file of files) {
369
- const matched: TelemetryEventRecord[] = [];
370
- for await (const event of readEventFiles([file], allow)) {
371
- if (!matches(event, query, since, until, keys, accounts)) continue;
372
- matched.push(event);
373
- if (oneRow) return matched;
374
- }
375
- matched.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
376
- events.push(...matched);
377
- if (query.limit !== undefined && events.length >= query.limit) return events.slice(0, query.limit);
378
- }
379
- return events;
380
- }
381
-
382
- interface EventTotals {
383
- requests: number;
384
- succeeded: number;
385
- failed: number;
386
- cancelled: number;
387
- truncated: number;
388
- inputTokens: number;
389
- cachedTokens: number;
390
- outputTokens: number;
391
- estimatedCost: number;
392
- unpriced: number;
393
- latencySum: number;
394
- latencyCount: number;
395
- tokensPerSecSum: number;
396
- tokensPerSecCount: number;
397
- }
398
-
399
- export function emptyTotals(): EventTotals {
400
- return { requests: 0, succeeded: 0, failed: 0, cancelled: 0, truncated: 0, inputTokens: 0, cachedTokens: 0, outputTokens: 0, estimatedCost: 0, unpriced: 0, latencySum: 0, latencyCount: 0, tokensPerSecSum: 0, tokensPerSecCount: 0 };
401
- }
402
-
403
- /**
404
-
405
- /** Folds one event into a totals bucket. `failed` counts any non-completed terminal status. */
406
- export function accumulate(totals: EventTotals, event: TelemetryEventRecord): void {
407
- totals.requests += 1;
408
- if (event.status === "completed") totals.succeeded += 1;
409
- else if (event.status === "cancelled") totals.cancelled += 1;
410
- else if (event.status === "truncated") totals.truncated += 1;
411
- else if (event.status !== null) totals.failed += 1;
412
- totals.inputTokens += event.inputTokens ?? 0;
413
- totals.cachedTokens += event.cachedInputTokens ?? 0;
414
- totals.outputTokens += event.outputTokens ?? 0;
415
- if (event.estimatedCostUsd !== null) totals.estimatedCost += event.estimatedCostUsd;
416
- else if (event.status === "completed") totals.unpriced += 1;
417
- if (event.latencyMs !== null) { totals.latencySum += event.latencyMs; totals.latencyCount += 1; }
418
- if (event.tokensPerSec !== null) { totals.tokensPerSecSum += event.tokensPerSec; totals.tokensPerSecCount += 1; }
419
- }
420
-
@@ -62,6 +62,15 @@ export function trackMemoryUsage(component: string, bytes: number): void {
62
62
  memoryUsage.record(component, bytes);
63
63
  }
64
64
 
65
+ /**
66
+ * The recorded memory figures keyed by component name (`rss`, `heap_used`,
67
+ * `heap_total`, `external`). This series is the canonical memory view the
68
+ * Prometheus gauges are derived from, so the two registries cannot disagree.
69
+ */
70
+ export function memoryUsageBytes(): Record<string, number> {
71
+ return memoryUsage.snapshot();
72
+ }
73
+
65
74
  /** Returns the current snapshot for the console performance endpoint. */
66
75
  export function performanceMetricsSnapshot(): PerformanceMetricsSnapshot {
67
76
  return {
@@ -10,7 +10,7 @@
10
10
  import { readFileSync } from "node:fs";
11
11
  import os from "node:os";
12
12
  import { metrics } from "./metrics";
13
- import { trackMemoryUsage } from "./performance-metrics";
13
+ import { memoryUsageBytes, trackMemoryUsage } from "./performance-metrics";
14
14
  import { log } from "./logger";
15
15
  import { resolveMemoryLimitOverrideBytes } from "../config/timeouts";
16
16
  import { CONFIG_SPEC, readFlag } from "../config/spec";
@@ -190,19 +190,24 @@ export class RuntimeMetricsSampler {
190
190
 
191
191
  sample(): void {
192
192
  const memory = process.memoryUsage();
193
- metrics.cartethyia_memory_rss_bytes.set(memory.rss);
194
- metrics.cartethyia_memory_heap_used_bytes.set(memory.heapUsed);
195
- metrics.cartethyia_memory_heap_total_bytes.set(memory.heapTotal);
193
+ // The performance-metrics memory series is the single memory view: record
194
+ // the four figures once, then derive the Prometheus gauges from that same
195
+ // series. `external` has no gauge equivalent, so it lives only in the
196
+ // series (and in the `/system/performance` snapshot).
197
+ trackMemoryUsage("rss", memory.rss);
198
+ trackMemoryUsage("heap_used", memory.heapUsed);
199
+ trackMemoryUsage("heap_total", memory.heapTotal);
200
+ trackMemoryUsage("external", memory.external);
201
+ const recordedMemory = memoryUsageBytes();
202
+ metrics.cartethyia_memory_rss_bytes.set(recordedMemory["rss"] ?? 0);
203
+ metrics.cartethyia_memory_heap_used_bytes.set(recordedMemory["heap_used"] ?? 0);
204
+ metrics.cartethyia_memory_heap_total_bytes.set(recordedMemory["heap_total"] ?? 0);
196
205
  metrics.cartethyia_memory_limit_bytes.set(this.limitBytes ?? 0);
197
206
  this.snapshot = {
198
- rssBytes: memory.rss,
207
+ rssBytes: recordedMemory["rss"] ?? 0,
199
208
  limitBytes: this.limitBytes,
200
209
  sampledAtMs: Date.now(),
201
210
  };
202
- trackMemoryUsage("rss", memory.rss);
203
- trackMemoryUsage("heap_used", memory.heapUsed);
204
- trackMemoryUsage("heap_total", memory.heapTotal);
205
- trackMemoryUsage("external", memory.external);
206
211
 
207
212
  const roundRobin = this.sources.routingRoundRobinEntries();
208
213
  metrics.cartethyia_routing_roundrobin_entries.set(roundRobin.combo, { scope: "combo" });