cartethyia 2.7.9 → 2.8.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 (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -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" });
@@ -6,7 +6,7 @@ import { DEFAULT_BOUNDS } from "../config/network";
6
6
  import { type SourceSurface, type UsageRecord } from "../protocol/canonical-model";
7
7
  import { log } from "./logger";
8
8
  import { scrubSecrets } from "./redaction";
9
- import { clientNameFromUserAgent } from "./client-name";
9
+ import { telemetryInsertRow, tokenCountToInt, type TelemetryInsertRowSource } from "./telemetry-row";
10
10
 
11
11
 
12
12
  /**
@@ -73,10 +73,6 @@ export interface TelemetryEventInput {
73
73
  readonly degenerateStop?: boolean;
74
74
  }
75
75
 
76
- function tokenCountToInt(value: number | "unavailable" | undefined): number | undefined {
77
- return typeof value === "number" ? Math.round(value) : undefined;
78
- }
79
-
80
76
  /** Conservative byte estimate for queue accounting.
81
77
  * Telemetry events contain only bounded scalar metadata; serializing every
82
78
  * event just to measure queue size added a JSON allocation on every request.
@@ -104,23 +100,19 @@ function estimateTelemetryEventBytes(event: TelemetryEventInput): number {
104
100
  }
105
101
 
106
102
  /** Maps a durable event to its `telemetry_events` insert row for the
107
- * batched multi-row writer (`TelemetryBatchBuffer.drain`). */
103
+ * batched multi-row writer (`TelemetryBatchBuffer.drain`). Normalizes the
104
+ * `TelemetryEventInput` shape (nested `usage`, token-count coercion) into the
105
+ * shared record shape; the column spelling itself lives in
106
+ * {@link telemetryInsertRow}, which the reconciliation backfill shares. */
108
107
  function telemetryEventRow(event: TelemetryEventInput) {
109
- // Every column below has a live producer: `accountId`/`networkPoolId` flow
110
- // `ttfbMs` is measured by the model-probe path, and `usage` (including
111
- // `cachedInputTokens` stays — `health` aggregates it into the Overview
112
- // cache-hit rate.
113
- return {
114
- // Stable identity: the request's own id. Retries reuse the materialized
115
- // row (the buffer retries the same rows across its five attempts), and
116
- // the (tenant_id, request_id) unique boundary makes a re-drain or a
117
- // cross-process race converge to one row via ON CONFLICT DO NOTHING
118
- // instead of double-charging.
108
+ // Only the normalization lives here: collapse the nested `usage` record onto
109
+ // the flat shared shape, and coerce token counts to the integers the schema
110
+ // columns store. Every field below has a live producer; the column spelling
111
+ // and the client-name derivation are `telemetryInsertRow`'s.
112
+ const source: TelemetryInsertRowSource = {
119
113
  id: event.requestId,
120
114
  tenantId: event.tenantId,
121
115
  requestId: event.requestId,
122
- // Stamped here, not by the reader. A row without it has no real time, and a
123
- // reader that invented one made every such row look like it happened just now.
124
116
  createdAt: new Date(),
125
117
  sourceSurface: event.sourceSurface,
126
118
  requestedModel: event.requestedModel,
@@ -129,8 +121,6 @@ function telemetryEventRow(event: TelemetryEventInput) {
129
121
  endpoint: event.endpoint ?? null,
130
122
  apiKeyId: event.apiKeyId ?? null,
131
123
  userAgent: event.userAgent ?? null,
132
- // Canonical client grouping, stamped once so SQL breakdowns group by it.
133
- clientName: clientNameFromUserAgent(event.userAgent),
134
124
  clientIp: event.clientIp ?? null,
135
125
  providerId: event.providerId ?? null,
136
126
  accountId: event.accountId ?? null,
@@ -152,18 +142,18 @@ function telemetryEventRow(event: TelemetryEventInput) {
152
142
  estimatedCostUsd:
153
143
  event.usage?.estimated_cost === null || event.usage === undefined
154
144
  ? null
155
- : String(event.usage.estimated_cost),
145
+ : event.usage.estimated_cost,
156
146
  // Provider-billed credit (Tencent buddy-meter `credit`, Qoder `credits`);
157
147
  // NULL when the upstream reported no credit field.
158
- // Numeric columns are string-typed in Drizzle, like `estimatedCostUsd`.
159
- creditUsed: event.usage?.credit_used != null ? String(event.usage.credit_used) : null,
148
+ creditUsed: event.usage?.credit_used ?? null,
160
149
  resolveMs: event.resolveMs ?? null,
161
- tokensPerSec: event.tokensPerSec != null ? String(event.tokensPerSec) : null,
150
+ tokensPerSec: event.tokensPerSec ?? null,
162
151
  firstContentDeltaAtMs: event.firstContentDeltaAtMs ?? null,
163
152
  lastEventAtMs: event.lastEventAtMs ?? null,
164
153
  payloadCaptureState: event.payloadCaptureState ?? null,
165
- degenerateStop: event.degenerateStop ?? null,
154
+ ...(event.degenerateStop !== undefined ? { degenerateStop: event.degenerateStop } : {}),
166
155
  };
156
+ return telemetryInsertRow(source);
167
157
  }
168
158
 
169
159
  interface TelemetryBatchBufferOptions {
@@ -184,7 +174,7 @@ const DRAIN_MAX_ATTEMPTS = 5;
184
174
  const DRAIN_RETRY_BASE_MS = 25;
185
175
  const DRAIN_RETRY_MAX_MS = 1_000;
186
176
 
187
- import { exponentialBackoff, type BackoffOptions } from "../network/backoff";
177
+ import { exponentialBackoff, type BackoffOptions } from "../network/util/backoff";
188
178
 
189
179
  /** Exponential retry delay for the `attempt`-th retry, clamped to the ceiling. */
190
180
  function drainRetryDelayMs(attempt: number, baseDelayMs = DRAIN_RETRY_BASE_MS): number {
@@ -33,7 +33,7 @@ import { join } from "node:path";
33
33
  import type { CartethyiaDatabase } from "../persistence/db-handle";
34
34
  import { telemetryEvents, telemetryReconciliationState } from "../persistence/schema";
35
35
  import { TELEMETRY_EVENT_DIR } from "./event-log";
36
- import { clientNameFromUserAgent } from "./client-name";
36
+ import { telemetryInsertRow } from "./telemetry-row";
37
37
  import type { TelemetryEventRecord } from "./event-log";
38
38
  import { hourStamp } from "../shared/time";
39
39
 
@@ -143,43 +143,15 @@ function parseChunk(text: string): { readonly events: TelemetryEventRecord[]; re
143
143
  return { events, carry };
144
144
  }
145
145
 
146
- /** The columns the backfill inserts. Mirrors `telemetryEventRow` on the write path. */
146
+ /** The columns the backfill inserts. Shared with the write path via
147
+ * {@link telemetryInsertRow}, so a recovered row carries exactly the columns
148
+ * the request path persists — including creditUsed, resolveMs, tokensPerSec,
149
+ * firstContentDeltaAtMs, lastEventAtMs, payloadCaptureState, and degenerateStop,
150
+ * which a hand-maintained mirror used to drop. */
147
151
  function toRow(event: TelemetryEventRecord): typeof telemetryEvents.$inferInsert {
148
- return {
149
- // `id = requestId` is what makes the unique index a working idempotency
150
- // boundary: a retry of the same request carries the same id.
151
- id: event.requestId,
152
- tenantId: event.tenantId!,
153
- requestId: event.requestId,
154
- sourceSurface: event.sourceSurface as typeof telemetryEvents.$inferInsert.sourceSurface,
155
- requestedModel: event.requestedModel,
156
- reportedModel: event.reportedModel,
157
- requestedEffort: event.requestedEffort,
158
- providerId: event.providerId,
159
- accountId: event.accountId,
160
- networkPoolId: event.networkPoolId,
161
- endpoint: event.endpoint,
162
- apiKeyId: event.apiKeyId,
163
- userAgent: event.userAgent,
164
- // Derived the same way ingestion derives it, so a backfilled row groups
165
- // with the rows the request path wrote instead of forming its own bucket.
166
- clientName: clientNameFromUserAgent(event.userAgent),
167
- clientIp: event.clientIp,
168
- latencyMs: event.latencyMs,
169
- ttfbMs: event.ttfbMs,
170
- stream: event.stream,
171
- httpStatus: event.httpStatus,
172
- status: event.status as typeof telemetryEvents.$inferInsert.status,
173
- errorCategory: event.errorCategory,
174
- errorOrigin: event.errorOrigin,
175
- inputTokens: event.inputTokens,
176
- cachedInputTokens: event.cachedInputTokens,
177
- outputTokens: event.outputTokens,
178
- reasoningTokens: event.reasoningTokens,
179
- estimatedCostUsd:
180
- event.estimatedCostUsd === null ? null : String(event.estimatedCostUsd),
181
- createdAt: new Date(event.createdAt),
182
- };
152
+ // `parseChunk` guarantees `tenantId` is a string before a record reaches
153
+ // here; the type keeps it optional because the raw shape predates that gate.
154
+ return telemetryInsertRow({ ...event, tenantId: event.tenantId! });
183
155
  }
184
156
 
185
157
  /**