cartethyia 2.4.4 → 2.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/.env.example +4 -1
  2. package/README.md +18 -0
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/{BannedPage-TDue2X0p.js → BannedPage-QhvbJB9o.js} +1 -1
  5. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-qaHXuiI5.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BFuQBKez.js → CliToolsPage-DyNXefph.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-BhglcqyR.js → CustomizationPage-D6jEU7nL.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-CeqROJrT.js → LoginPage-BJlMVN4U.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-CRXSAITY.js → OverviewPage-CQxXbo2g.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
  30. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  31. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
  32. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  37. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
  38. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  39. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
  40. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  42. package/dist/dashboard/assets/{UsagePage-B6LA4ewF.js → UsagePage-BAFaSDQV.js} +14 -14
  43. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  44. package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
  45. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  46. package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
  47. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  48. package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
  49. package/dist/dashboard/assets/app-Cjq7nrwu.js.gz +0 -0
  50. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  52. package/dist/dashboard/assets/{assert-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
  53. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  54. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  55. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
  56. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  57. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
  58. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  59. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  60. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
  61. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  62. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  63. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  64. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  66. package/dist/dashboard/assets/cli-tools-DE2ug812.js +1 -0
  67. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  68. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
  69. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  70. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  71. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  72. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BWIP_EvW.js} +1 -1
  73. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  74. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  75. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  76. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  77. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-BKcgVugF.js} +1 -1
  78. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  79. package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
  80. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  81. package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
  82. package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
  83. package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
  84. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  85. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  87. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  88. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  89. package/dist/dashboard/assets/github-badge-Booc5mNe.js +1 -0
  90. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  91. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  92. package/dist/dashboard/assets/{index-BA_ni9Ix.js → index-D70pChfn.js} +2 -2
  93. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  94. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
  95. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  96. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
  97. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  98. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  99. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  100. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  101. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BArzXirQ.js} +1 -1
  102. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  103. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
  104. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  105. package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
  106. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  107. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  108. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  109. package/dist/dashboard/assets/{model-bans-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
  110. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  111. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  113. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  114. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  115. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  116. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  118. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  120. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  121. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  130. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  137. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  138. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  139. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  140. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  141. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  142. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  143. package/dist/dashboard/assets/{routing-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
  144. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  145. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  146. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  147. package/dist/dashboard/assets/{select-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
  148. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  149. package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
  150. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  151. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  152. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  153. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  154. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
  155. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  156. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
  157. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  158. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  159. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
  160. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  161. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  162. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  163. package/dist/dashboard/assets/toast-C76WrMhc.js +2 -0
  164. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  165. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  166. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  167. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  168. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  169. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  170. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  171. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-D87FZwH0.js} +1 -1
  172. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  173. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  174. package/dist/dashboard/index.html +1 -1
  175. package/dist/dashboard/index.html.gz +0 -0
  176. package/dist/dashboard/precompressed-assets.json +123 -0
  177. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  178. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  179. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  180. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  181. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  182. package/package.json +25 -30
  183. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  184. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  185. package/scripts/cli/command/admin.ts +65 -0
  186. package/scripts/cli/command/console-menu.ts +227 -0
  187. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  188. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  189. package/scripts/cli/command/start.ts +13 -0
  190. package/scripts/cli/command/tray.ts +165 -0
  191. package/scripts/cli/command/update.ts +23 -0
  192. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  193. package/scripts/cli/gateway/spawn.ts +101 -0
  194. package/scripts/cli/gateway/stop.ts +51 -0
  195. package/scripts/cli/main.ts +94 -0
  196. package/scripts/cli/support/bun.ts +27 -0
  197. package/scripts/cli/support/env.ts +99 -0
  198. package/scripts/cli/support/platform.ts +64 -0
  199. package/scripts/cli/support/services.ts +88 -0
  200. package/scripts/cli/support/startup-signal.ts +23 -0
  201. package/scripts/cli/support/version.ts +37 -0
  202. package/scripts/cli/tray/hidden-window.vbs +28 -0
  203. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  204. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  205. package/scripts/cli/ui/background.ts +49 -0
  206. package/src/boot/app.ts +3 -1
  207. package/src/boot/dependencies.ts +36 -13
  208. package/src/config/providers.ts +27 -1
  209. package/src/config/spec.ts +11 -0
  210. package/src/console/backup/routes.ts +8 -0
  211. package/src/console/cli-tools/contracts.ts +6 -6
  212. package/src/console/console-router.ts +5 -3
  213. package/src/console/dashboard-assets.ts +81 -11
  214. package/src/console/observability/contracts.ts +54 -22
  215. package/src/console/observability/routes.ts +31 -6
  216. package/src/console/observability/store.ts +68 -28
  217. package/src/console/providers/catalog/public-model-store.ts +91 -60
  218. package/src/console/providers/catalog/store.ts +5 -1
  219. package/src/console/quota/account-quota-tenant.ts +9 -6
  220. package/src/console/response-compression.ts +69 -2
  221. package/src/console/routing/pools/probe-verdict.ts +72 -0
  222. package/src/console/share/share-router.ts +7 -4
  223. package/src/console/share/share-stats.ts +12 -5
  224. package/src/console/shared/read-through-cache.ts +133 -7
  225. package/src/console/updates/repository-updates.ts +39 -34
  226. package/src/network/outbound-fetch.ts +113 -25
  227. package/src/network/tls/bun-client-hello.ts +112 -0
  228. package/src/observability/event-log.ts +29 -8
  229. package/src/observability/metrics.ts +40 -13
  230. package/src/observability/runtime-metrics.ts +7 -0
  231. package/src/persistence/schema.ts +40 -2
  232. package/src/protocol/response/codex.ts +9 -75
  233. package/src/protocol/tool-identity.ts +43 -9
  234. package/src/providers/authentication/jwt-validator.ts +130 -187
  235. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  236. package/src/providers/default-registry.ts +4 -0
  237. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  238. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  239. package/src/providers/discovery/probe-wire.ts +29 -5
  240. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
  241. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  242. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
  243. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  244. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  245. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  246. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  247. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
  248. package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
  249. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  250. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  251. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  252. package/src/providers/integrations/codex/codex.ts +5 -8
  253. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  254. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  255. package/src/providers/integrations/opencode/provider.ts +42 -3
  256. package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
  257. package/src/providers/integrations/shared/install-id.ts +29 -2
  258. package/src/providers/operations/client-versions.ts +11 -1
  259. package/src/providers/operations/model-catalog-cache.ts +18 -3
  260. package/src/providers/operations/provider-catalog-service.ts +76 -0
  261. package/src/transport/dispatch/attempt-finalize.ts +7 -1
  262. package/src/transport/dispatch/attempt-loop.ts +9 -7
  263. package/src/transport/dispatch/empty-stream.ts +17 -26
  264. package/src/transport/failure-policy.ts +20 -0
  265. package/src/transport/routing/route-catalog.ts +42 -3
  266. package/src/transport/surface/chat/encode.ts +18 -31
  267. package/src/transport/surface/messages/stream.ts +3 -2
  268. package/src/transport/translation/tool-repair.ts +37 -16
  269. package/src/workers/account-identity-sweep.ts +14 -12
  270. package/src/workers/daily-checkin.ts +62 -0
  271. package/src/workers/quota-refresh-sweep.ts +27 -2
  272. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  273. package/dist/dashboard/assets/ConsoleLogPage-vkZBxMVo.js +0 -1
  274. package/dist/dashboard/assets/HealthEventsModal-DDkHfwpV.js +0 -1
  275. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  276. package/dist/dashboard/assets/ProviderDetailPage-cNAD_6uN.js +0 -3
  277. package/dist/dashboard/assets/ProvidersPage-Cv5tdxlx.js +0 -1
  278. package/dist/dashboard/assets/ProxyPage-CWvMfzjj.js +0 -7
  279. package/dist/dashboard/assets/QuotaPage-IYQOOHR4.js +0 -1
  280. package/dist/dashboard/assets/SetupPage-BPDNo3Ek.js +0 -1
  281. package/dist/dashboard/assets/SortableList-Bol1ASmI.js +0 -1
  282. package/dist/dashboard/assets/StudioPage-BE5DJONZ.js +0 -31
  283. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  284. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  285. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  286. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  287. package/dist/dashboard/assets/entry-DTiWhIY-.js +0 -4
  288. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  289. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  290. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  291. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  292. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  293. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  294. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  295. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  296. package/scripts/cli/cli.ts +0 -766
  297. package/scripts/cli/version.ts +0 -66
  298. package/scripts/operations/probe-egress.ts +0 -335
  299. package/scripts/operations/restart.ts +0 -120
  300. package/scripts/shared/env.ts +0 -269
  301. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  302. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  303. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  304. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { createHash, randomBytes, randomUUID } from "node:crypto";
19
19
  import {
20
+ base64UrlEncode,
20
21
  nonEmptyTrimmedString,
21
22
  record,
22
23
  } from "../../authentication/oauth-flow-store";
@@ -58,13 +59,6 @@ export function parseQoderOAuthState(value: unknown): QoderOAuthState | undefine
58
59
  return { userId, machineId };
59
60
  }
60
61
 
61
- function base64Url(buffer: Buffer): string {
62
- return buffer
63
- .toString("base64")
64
- .replace(/=/g, "")
65
- .replace(/\+/g, "-")
66
- .replace(/\//g, "_");
67
- }
68
62
 
69
63
  /**
70
64
  * Converts the upstream's expiry hint into a Unix-millisecond timestamp.
@@ -131,8 +125,8 @@ export class QoderOAuthClient extends OAuthDeviceFlow {
131
125
  override async startDeviceAuth(
132
126
  _context?: OAuthDeviceFlowContext,
133
127
  ): Promise<OAuthDeviceStartResult> {
134
- const verifier = base64Url(randomBytes(32));
135
- const challenge = base64Url(createHash("sha256").update(verifier).digest());
128
+ const verifier = base64UrlEncode(randomBytes(32));
129
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
136
130
  const nonce = randomUUID();
137
131
  const machineId = randomUUID();
138
132
  const params = new URLSearchParams({
@@ -53,17 +53,44 @@ function installIdDirectories(): readonly string[] {
53
53
  * process so a request does not see a different identity than the one before
54
54
  * it, which is the least surprising behaviour available when persistence is
55
55
  * impossible.
56
+ *
57
+ * Both maps are bounded: the key is derived from the install-id *path*, so an
58
+ * unbounded map would grow with every directory an operator ever pointed
59
+ * `CARTETHYIA_INSTALL_ID_DIR` at. The cap matches the other per-process
60
+ * identity caches (`qoderMachineIds`, `MAX_SESSION_STATES` in antigravity):
61
+ * oldest-first eviction, which for an identity cache only costs a fresh uuid.
56
62
  */
63
+ const MAX_IN_MEMORY_IDS = 1024;
57
64
  const inMemoryIds = new Map<string, string>();
58
65
  const warnedPaths = new Set<string>();
59
66
 
67
+ /** Evicts oldest-first once the cap is exceeded. */
68
+ function boundMapInsert<K, V>(map: Map<K, V>, key: K, value: V): void {
69
+ map.set(key, value);
70
+ while (map.size > MAX_IN_MEMORY_IDS) {
71
+ const oldest = map.keys().next();
72
+ if (oldest.done === true) break;
73
+ map.delete(oldest.value);
74
+ }
75
+ }
76
+
77
+ /** Evicts oldest-first once the cap is exceeded. */
78
+ function boundSetInsert(set: Set<string>, value: string): void {
79
+ set.add(value);
80
+ while (set.size > MAX_IN_MEMORY_IDS) {
81
+ const oldest = set.values().next();
82
+ if (oldest.done === true) break;
83
+ set.delete(oldest.value);
84
+ }
85
+ }
86
+
60
87
  function fallbackId(key: string, path: string, reason: unknown): string {
61
88
  const existing = inMemoryIds.get(key);
62
89
  if (existing !== undefined) return existing;
63
90
  const id = randomUUID();
64
- inMemoryIds.set(key, id);
91
+ boundMapInsert(inMemoryIds, key, id);
65
92
  if (!warnedPaths.has(key)) {
66
- warnedPaths.add(key);
93
+ boundSetInsert(warnedPaths, key);
67
94
  const message = reason instanceof Error ? reason.message : String(reason);
68
95
  log.warn(
69
96
  `[install-id] could not persist "${path}" (${message}); using an id that ` +
@@ -525,9 +525,19 @@ export const getKimiCliVersion = accessor("kimiCli").get;
525
525
 
526
526
  export const getCodeBuddyVersion = accessor("codebuddy").get;
527
527
  export const getCodeBuddyCnVersion = accessor("codebuddyCn").get;
528
+ /**
529
+ * The CodeBuddy User-Agent.
530
+ *
531
+ * Both identities share the CLI *package* version, which is why `codebuddy`
532
+ * (the npm `@tencent-ai/codebuddy-code` pin, `2.168.2` — the value both
533
+ * references use for CLI) is the version for each. `codebuddyCn` is the CN
534
+ * *desktop* build (`4.12.2.…`), and using it here produced
535
+ * `CLI/4.12.2.41207131 CodeBuddy/4.12.2.41207131` — a CLI claiming a desktop
536
+ * version, which no official client emits.
537
+ */
528
538
  export function buildCodeBuddyUserAgent(
529
539
  identity: "IDE" | "CLI",
530
- version = identity === "CLI" ? getCodeBuddyCnVersion() : getCodeBuddyVersion(),
540
+ version = getCodeBuddyVersion(),
531
541
  ): string {
532
542
  return `${identity}/${version} CodeBuddy/${version}`;
533
543
  }
@@ -14,10 +14,14 @@ import { cachedRead } from "../../console/shared/read-through-cache";
14
14
 
15
15
  /**
16
16
  * Provider entries retained before the least-recently-used catalog is evicted.
17
- * One entry per provider is tiny, but an unbounded cache would pin every
18
- * catalog for the process lifetime.
17
+ *
18
+ * Must clear the bundled provider count — there are **73** of them — because a
19
+ * cap below it thrashes: a catalog rebuild evicts a provider it is about to
20
+ * need again, so `bundledModelCatalog()` re-imports a module it already had.
21
+ * Sized above that count with room for BYOK rows. One entry is a model list, so
22
+ * the bound still matters; it just has to be the right one.
19
23
  */
20
- export const MODEL_CATALOG_CACHE_MAX_ENTRIES = 64;
24
+ export const MODEL_CATALOG_CACHE_MAX_ENTRIES = 128;
21
25
 
22
26
  /**
23
27
  * Cross-instance tier lifetime. Static provider catalogs change only on a
@@ -82,3 +86,14 @@ export function getCachedModels(
82
86
  export function resetModelCatalogCacheForTesting(): void {
83
87
  catalogCache.clear();
84
88
  }
89
+
90
+ /**
91
+ * Live entry count, for the `/metrics` gauge.
92
+ *
93
+ * Exported so a rebuild that saturates the cap is visible from outside the
94
+ * process: before this, every in-process cache except the quota tier was
95
+ * invisible, and growth could only be found with a heap dump.
96
+ */
97
+ export function modelCatalogCacheSize(): number {
98
+ return catalogCache.size;
99
+ }
@@ -20,6 +20,57 @@ import { GATEWAY_USER_AGENT } from "./gateway-user-agent";
20
20
  import { isIP } from "node:net";
21
21
  import { isAddressAllowed } from "../../network/ssrf";
22
22
  import type { SsrfPolicy } from "../../config/network";
23
+
24
+ /**
25
+ * Revision of the compiled provider catalog, and the last one materialized.
26
+ *
27
+ * The snapshot builder re-seeds on **every** rebuild, and a re-seed is a
28
+ * `DELETE` per provider plus a full `INSERT … ON CONFLICT DO UPDATE` of the
29
+ * compiled catalog. That is correct work when the catalog changed and pure
30
+ * waste when it did not — and the invalidation that triggers it is a timer
31
+ * (`account-health-sweep`), so the waste lands on a proxy request.
32
+ *
33
+ * Only the paths that can actually change the compiled catalog bump this: a
34
+ * provider registered, edited, or retired. A cooldown recovery changes account
35
+ * health, not the catalog, so it must not.
36
+ */
37
+ let catalogRevision = 0;
38
+ let lastSeededCatalogRevision = -1;
39
+
40
+ /** Current catalog revision. */
41
+ export function currentCatalogRevision(): number {
42
+ return catalogRevision;
43
+ }
44
+
45
+ /** Bumps the revision after a change to the compiled catalog. */
46
+ export function bumpCatalogRevision(): number {
47
+ catalogRevision += 1;
48
+ return catalogRevision;
49
+ }
50
+
51
+ /**
52
+ * Forces the next re-seed to run even when the revision is unchanged.
53
+ *
54
+ * For the boot path and for tests: the compiled catalog has to be materialized
55
+ * once for the current process regardless of what an earlier call already
56
+ * applied, because a fresh database or a fresh registry is not a revision
57
+ * change.
58
+ */
59
+ export function invalidateCatalogSeed(): void {
60
+ lastSeededCatalogRevision = -1;
61
+ }
62
+
63
+ /**
64
+ * Records that the caller has just materialized the current catalog itself, so
65
+ * the next `refreshActiveProviderCatalogs` is a no-op.
66
+ *
67
+ * The boot path seeds directly rather than through the gate — it must always
68
+ * run, against a database and a registry that are both fresh — and without this
69
+ * the first snapshot build would pay for a second identical seed.
70
+ */
71
+ export function markCatalogSeeded(): void {
72
+ lastSeededCatalogRevision = catalogRevision;
73
+ }
23
74
  /**
24
75
  * Idempotently materializes every shipped provider in the database catalog.
25
76
  * The canonical provider definition supplies identity and routing defaults;
@@ -60,6 +111,9 @@ export async function seedBundledProviders(db: CartethyiaDatabase): Promise<void
60
111
  ),
61
112
  },
62
113
  });
114
+ // The compiled catalog is derived from the registry, and this is the path
115
+ // that reconciles the bundle's own rows into it.
116
+ bumpCatalogRevision();
63
117
  }
64
118
 
65
119
  /**
@@ -94,6 +148,9 @@ export async function retireUnbundledProviders(db: CartethyiaDatabase): Promise<
94
148
  .delete(providers)
95
149
  .where(and(isNull(providers.tenantId), notInArray(providers.id, bundledIds)))
96
150
  .returning({ id: providers.id });
151
+ // Deleting a provider removes it from the compiled catalog, so the next
152
+ // re-seed must run even though no revision bumped on this path otherwise.
153
+ if (retired.length > 0) bumpCatalogRevision();
97
154
  return retired.length;
98
155
  }
99
156
 
@@ -172,13 +229,22 @@ export async function bundledModelCatalog(
172
229
  * boot. Seeding is no longer account-gated, so this is now simply the same walk
173
230
  * as at boot; it exists because an account makes the rows routable and the
174
231
  * snapshot must reflect that without waiting for a restart.
232
+ *
233
+ * Gated on {@link currentCatalogRevision}: this is the seeder the snapshot
234
+ * builder calls on every rebuild, and the invalidation that triggers it is a
235
+ * 30 s timer, so an ungated re-seed could be pushed onto the next proxy
236
+ * request for a catalog that had not changed. The paths that do change the
237
+ * catalog bump the revision; everything else is a no-op here.
175
238
  */
176
239
  export async function refreshActiveProviderCatalogs(
177
240
  db: CartethyiaDatabase,
178
241
  registry: ProviderRegistry,
179
242
  ): Promise<void> {
243
+ const revision = currentCatalogRevision();
244
+ if (revision === lastSeededCatalogRevision) return;
180
245
  const catalog = await bundledModelCatalog(registry);
181
246
  await seedBundledModels(db, catalog.modelsByProvider);
247
+ lastSeededCatalogRevision = revision;
182
248
  }
183
249
 
184
250
 
@@ -217,6 +283,7 @@ export async function registerByokProviders(
217
283
  .from(providers)
218
284
  .where(and(isNotNull(providers.baseUrl), eq(providers.enabled, true)));
219
285
  const hosts = new Map<string, ByokUpstreamHost>();
286
+ let registered = false;
220
287
  for (const row of rows) {
221
288
  if (!row.baseUrl) continue;
222
289
  const baseUrl = row.baseUrl;
@@ -307,7 +374,13 @@ export async function registerByokProviders(
307
374
  }),
308
375
  });
309
376
  byokRegistrationFingerprints.set(providerId, fingerprint);
377
+ registered = true;
310
378
  }
379
+ // A changed BYOK registration changes what the compiled catalog contains, so
380
+ // the next re-seed must run. An unchanged one deliberately does not: this
381
+ // function is called on every console provider mutation, most of which do not
382
+ // alter the registration at all.
383
+ if (registered) bumpCatalogRevision();
311
384
  return hosts;
312
385
  }
313
386
 
@@ -341,6 +414,9 @@ export async function syncByokProvider(
341
414
  if (host === undefined && fingerprintBefore !== undefined) {
342
415
  registry.unregister(normalized);
343
416
  byokRegistrationFingerprints.delete(normalized);
417
+ // The provider left the registry, so the compiled catalog no longer
418
+ // contains it: the next re-seed must run to prune its rows.
419
+ bumpCatalogRevision();
344
420
  }
345
421
  return host;
346
422
  }
@@ -249,7 +249,13 @@ export function captureProviderExchange(
249
249
  method: init?.method ?? "GET",
250
250
  url,
251
251
  ...(headers === undefined ? {} : { headers }),
252
- ...(rawBody === null ? {} : { body: parseCapturedBody(rawBody) }),
252
+ // Capped the same way the response is. A capture is a diagnostic, and an
253
+ // unbounded translated request body per attempt — up to `maxAttempts` of
254
+ // them, all alive until the loop returns — costs more than it can tell
255
+ // an operator.
256
+ ...(rawBody === null
257
+ ? {}
258
+ : { body: parseCapturedBody(rawBody.slice(0, PROVIDER_CAPTURE_MAX_BYTES)) }),
253
259
  };
254
260
  const response = await inner(input, init);
255
261
  sink.response =
@@ -10,7 +10,13 @@
10
10
  */
11
11
  import { GatewayError } from "../../protocol/gateway-error";
12
12
  import type { ResolvedCredential } from "../../providers/provider-contracts";
13
- import { classifyTerminalOutcome, fallbackRetryDelayMs, isRetryableFailure, sleep } from "../failure-policy";
13
+ import {
14
+ classifyTerminalOutcome,
15
+ fallbackRetryDelayMs,
16
+ isRetryableFailure,
17
+ MIN_RETRY_BUDGET_MS,
18
+ sleep,
19
+ } from "../failure-policy";
14
20
  import { resolveRouteMaxAttempts } from "../../config/timeouts";
15
21
  import type { OAuthTokenRefresher } from "../../providers/authentication/oauth-refresh-service";
16
22
  import type { OAuthRefreshService } from "../../providers/authentication/oauth-refresh-service";
@@ -178,12 +184,8 @@ export async function runAttemptLoop<TResult, TAdapter>(
178
184
  const perMemberCap =
179
185
  memberCount > 1 ? Math.max(1, Math.ceil(maxAttempts / memberCount)) : maxAttempts;
180
186
  const attemptsByMember = new Map<string, number>();
181
- // Minimum wall-clock left before a follow-on attempt is worth starting. A
182
- // retry repeats the whole pre-dispatch phase (credential, admission, pool
183
- // slot, prepare), so it needs at least that much budget on top of whatever
184
- // the upstream itself takes. Below this, the attempt would be killed by the
185
- // deadline before it could return anything the client could use.
186
- const MIN_RETRY_BUDGET_MS = 15_000;
187
+ // The budget itself is owned by failure-policy, so this loop, the
188
+ // empty-stream replay, and the Codex retry budget cannot drift apart.
187
189
  let index = 0;
188
190
  let attempts = 0;
189
191
  while (index < candidates.length && attempts < maxAttempts) {
@@ -18,6 +18,11 @@
18
18
  * rather than surfacing an empty body to the client.
19
19
  */
20
20
  import { GatewayError } from "../../protocol/gateway-error";
21
+ import {
22
+ fallbackRetryDelayMs,
23
+ MIN_RETRY_BUDGET_MS,
24
+ sleep as abortableSleep,
25
+ } from "../failure-policy";
21
26
 
22
27
  // ─── Public types ──────────────────────────────────────────────────────────
23
28
 
@@ -38,9 +43,17 @@ export type EmptyStreamFetch = (
38
43
  init: { headers: Record<string, string>; body: string; signal: AbortSignal },
39
44
  ) => Promise<Response>;
40
45
 
46
+ /**
47
+ * Retry-delay policy.
48
+ *
49
+ * Delays come from `fallbackRetryDelayMs` (capped exponential with full
50
+ * jitter), whose base and cap are operator-tunable. There is deliberately no
51
+ * per-caller base: the local formula this replaced was uncapped and
52
+ * unjittered, so a provider returning 200-with-no-events could drive an
53
+ * unbounded wait, and concurrent clients could resynchronize their retries.
54
+ */
41
55
  export interface EmptyStreamPolicy {
42
56
  readonly maxEmptyReplays?: number;
43
- readonly baseDelayMs?: number;
44
57
  readonly firstEventTimeoutMs?: number;
45
58
  readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
46
59
  }
@@ -55,9 +68,8 @@ export interface EmptyStreamResult {
55
68
  // ─── Defaults ──────────────────────────────────────────────────────────────
56
69
 
57
70
  const DEFAULT_MAX_REPLAYS = 2;
58
- const DEFAULT_BASE_DELAY_MS = 500;
59
71
  const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 200_000; // 200s, matches CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS default
60
- const MIN_RETRY_BUDGET_MS = 15_000; // attempt-loop floor
72
+
61
73
 
62
74
  // ─── Helpers ───────────────────────────────────────────────────────────────
63
75
 
@@ -66,27 +78,7 @@ export function isEmptyStream(tally: StreamTally): boolean {
66
78
  return tally.dataEvents === 0 || (!tally.meaningful && !tally.thoughtOnly);
67
79
  }
68
80
 
69
- function defaultSleep(ms: number, signal: AbortSignal): Promise<void> {
70
- return new Promise((resolve, reject) => {
71
- if (signal.aborted) {
72
- reject(new GatewayError("transport_closed", 499, "Aborted"));
73
- return;
74
- }
75
- const timer = setTimeout(() => {
76
- signal.removeEventListener("abort", onAbort);
77
- resolve();
78
- }, ms);
79
- const onAbort = () => {
80
- clearTimeout(timer);
81
- reject(new GatewayError("transport_closed", 499, "Aborted"));
82
- };
83
- signal.addEventListener("abort", onAbort, { once: true });
84
- });
85
- }
86
81
 
87
- function backoffMs(attempt: number, base: number): number {
88
- return base * 2 ** attempt;
89
- }
90
82
 
91
83
  // ─── Main function ─────────────────────────────────────────────────────────
92
84
 
@@ -109,9 +101,8 @@ export async function fetchWithEmptyStreamRecovery(
109
101
  },
110
102
  ): Promise<EmptyStreamResult> {
111
103
  const maxReplays = deps.policy?.maxEmptyReplays ?? DEFAULT_MAX_REPLAYS;
112
- const baseDelay = deps.policy?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
113
104
  const firstEventTimeout = deps.policy?.firstEventTimeoutMs ?? DEFAULT_FIRST_EVENT_TIMEOUT_MS;
114
- const sleep = deps.policy?.sleep ?? defaultSleep;
105
+ const sleep = deps.policy?.sleep ?? abortableSleep;
115
106
 
116
107
  if (endpoints.length === 0) {
117
108
  throw new GatewayError("platform_unavailable", 502, "No endpoints available", {}, "upstream");
@@ -173,7 +164,7 @@ export async function fetchWithEmptyStreamRecovery(
173
164
 
174
165
  // Empty — replay if we haven't exhausted this endpoint's budget
175
166
  if (replay < replaysAllowed) {
176
- const delay = backoffMs(replay, baseDelay);
167
+ const delay = fallbackRetryDelayMs(replay);
177
168
  try {
178
169
  await sleep(delay, deps.signal);
179
170
  } catch (error) {
@@ -884,10 +884,30 @@ export function isRetryableFailure(error: unknown): boolean {
884
884
  return classifyUpstreamFailure(error).retryable;
885
885
  }
886
886
 
887
+ /**
888
+ * Minimum wall-clock budget a follow-on attempt needs to be worth starting.
889
+ *
890
+ * A retry repeats the whole pre-dispatch phase — credential resolution,
891
+ * admission, pool slot, request preparation — so it needs at least this much
892
+ * headroom on top of whatever the upstream itself takes. Below this, the
893
+ * attempt is killed by the deadline before it can return anything usable, and
894
+ * starting it replaces the first attempt's real error with a second one's
895
+ * timeout.
896
+ *
897
+ * Owned here so the attempt loop, the empty-stream replay, and the Codex
898
+ * retry budget cannot drift apart; each previously declared its own copy of
899
+ * the constant.
900
+ */
901
+ export const MIN_RETRY_BUDGET_MS = 15_000;
902
+
887
903
  /**
888
904
  * Capped exponential backoff with full jitter for a zero-based attempt index.
889
905
  * The base and cap are operator-tunable (`CARTETHYIA_FALLBACK_RETRY_BASE_MS`,
890
906
  * `CARTETHYIA_FALLBACK_RETRY_CAP_MS`) and default to 100ms / 2000ms.
907
+ *
908
+ * Used for every retry delay, including the empty-stream replay: jitter keeps
909
+ * concurrent clients from resynchronizing, and the cap keeps a provider that
910
+ * returns 200-with-no-events from driving an unbounded wait.
891
911
  */
892
912
  export function fallbackRetryDelayMs(attemptIndex: number): number {
893
913
  const exp = Math.min(resolveFallbackRetryCapMs(), resolveFallbackRetryBaseMs() * 2 ** attemptIndex);
@@ -16,8 +16,6 @@ import {
16
16
  cliToolSettings,
17
17
  } from "../../persistence/schema";
18
18
  import { DEFAULT_PROVIDER_MAX_INFLIGHT } from "../../persistence/schema";
19
- import "../../providers/integrations/claude/claude-oauth";
20
- import "../../providers/integrations/codex/codex-oauth";
21
19
  import { resolveTenantOverride } from "../../persistence/tenant-scope";
22
20
  import {
23
21
  type RouteCandidate,
@@ -149,6 +147,42 @@ export function buildCapabilityProfile(row: {
149
147
  };
150
148
  }
151
149
 
150
+ /**
151
+ * The capability profile for one model row, memoized across rebuilds.
152
+ *
153
+ * Bounded LRU keyed by `providerId/modelId`: the value is a pure function of
154
+ * the row's metadata, so two rebuilds that see the same row get the same object
155
+ * instead of two identical ones. The bound keeps a long-lived process from
156
+ * pinning one profile per row it has ever seen — a model deleted from the
157
+ * catalog must not leave its profile behind forever.
158
+ */
159
+ const CAPABILITY_PROFILE_CACHE_MAX_ENTRIES = 4096;
160
+ const capabilityProfileCache = new Map<string, Record<string, boolean>>();
161
+
162
+ function capabilityProfileFor(row: {
163
+ modalities: unknown;
164
+ reasoning: boolean;
165
+ toolCall: boolean;
166
+ providerId: string;
167
+ modelId: string;
168
+ }): Record<string, boolean> {
169
+ const key = `${row.providerId}/${row.modelId}`;
170
+ const cached = capabilityProfileCache.get(key);
171
+ if (cached !== undefined) return cached;
172
+ const profile = buildCapabilityProfile(row);
173
+ if (capabilityProfileCache.size >= CAPABILITY_PROFILE_CACHE_MAX_ENTRIES) {
174
+ const oldest = capabilityProfileCache.keys().next().value;
175
+ if (oldest !== undefined) capabilityProfileCache.delete(oldest);
176
+ }
177
+ capabilityProfileCache.set(key, profile);
178
+ return profile;
179
+ }
180
+
181
+ /** Live memo size, for tests and the metrics gauge. */
182
+ export function capabilityProfileCacheSize(): number {
183
+ return capabilityProfileCache.size;
184
+ }
185
+
152
186
  /** Resolves route identity only for bundled adapters without a native User-Agent builder. */
153
187
  export function resolveRouteUserAgent(
154
188
  providerId: string,
@@ -527,7 +561,12 @@ class RouteCatalogRepository {
527
561
  const providerTenantId = providerTenantById.get(model.providerId);
528
562
  if (providerTenantId === undefined) continue;
529
563
  const accounts = accountsByProvider.get(model.providerId) ?? [];
530
- const capabilityProfile = buildCapabilityProfile(model);
564
+ // One profile per model row, memoized across snapshot rebuilds. The
565
+ // profile is derived purely from the row's metadata, is never mutated
566
+ // (grep finds no write to `capability_profile`), and is shared by every
567
+ // candidate this row fans out to — so a rebuild no longer constructs a
568
+ // fresh object per model on top of the fan-out it already had.
569
+ const capabilityProfile = capabilityProfileFor(model);
531
570
  if (accounts.length === 0) {
532
571
  const providerRequiresAccount = requiresAccountById.get(model.providerId) ?? true;
533
572
  if (!providerRequiresAccount) {
@@ -1,4 +1,5 @@
1
1
  import type { CanonicalEvent, CanonicalStopReason } from "../../../protocol/canonical-model";
2
+ import { jsonrepair } from "jsonrepair";
2
3
  import { reasoningSummaryIdentity, reasoningSummarySeparator, type ReasoningSummaryIdentity } from "../reasoning-summary";
3
4
  import { eventText } from "../adapters";
4
5
  import { isRecord } from "../../../protocol/codecs";
@@ -20,50 +21,36 @@ import type { ChatEncodeOptions, ChunkBase, JsonObject } from "./parse";
20
21
  * a raw control character inside a string, a trailing comma, an unquoted key.
21
22
  * The client parses the assembled string with `JSON.parse` and, on failure,
22
23
  * reports the call as `Tool <name> not found` with the parser's message
23
- * (`Unexpected trailing characters`, `Invalid number`). The repair is the same
24
- * one oh-my-pi applies before a tool runs: escape what strict JSON forbids,
25
- * then drop a trailing comma. A value that still does not parse is returned
26
- * unchanged — a half-repaired call is worse than the original error.
24
+ * (`Unexpected trailing characters`, `Invalid number`).
25
+ *
26
+ * Repairing is delegated to `jsonrepair`, which handles the whole family of
27
+ * near-JSON shapes (truncated objects, unquoted keys, single quotes, trailing
28
+ * commas) instead of the two cases a hand-rolled scanner covered.
29
+ *
30
+ * The policy around it is unchanged and is the load-bearing part: the result
31
+ * is parsed only to validate, and anything that does not parse *to an object*
32
+ * is discarded in favour of the original text — a half-repaired call is worse
33
+ * than the original error. The object check matters because `jsonrepair` will
34
+ * happily turn arbitrary prose into a quoted JSON string, which parses but is
35
+ * not a tool-argument object.
27
36
  */
28
37
  export function repairToolArguments(raw: string): string {
29
38
  if (raw.length === 0) return "{}";
30
39
  const repaired = repairToolArgumentText(raw);
31
40
  try {
32
- JSON.parse(repaired);
41
+ const parsed: unknown = JSON.parse(repaired);
42
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return raw;
33
43
  return repaired;
34
44
  } catch {
35
45
  return raw;
36
46
  }
37
47
  }
38
48
 
49
+ /** Repairs near-JSON text into strict JSON. Throws when the text is unrepairable. */
39
50
  function repairToolArgumentText(json: string): string {
40
- const parts: string[] = [];
41
- let last = 0;
42
- let inString = false;
43
- for (let i = 0; i < json.length; i++) {
44
- const code = json.charCodeAt(i);
45
- if (!inString) {
46
- if (code === 0x22) inString = true;
47
- continue;
48
- }
49
- if (code === 0x22) {
50
- inString = false;
51
- continue;
52
- }
53
- if (code === 0x5c) {
54
- i++;
55
- continue;
56
- }
57
- if (code < 0x20) {
58
- const escape =
59
- code === 0x0a ? "\\n" : code === 0x0d ? "\\r" : code === 0x09 ? "\\t" : `\\u${code.toString(16).padStart(4, "0")}`;
60
- parts.push(json.slice(last, i), escape);
61
- last = i + 1;
62
- }
63
- }
64
- const escaped = parts.length === 0 ? json : parts.join("") + json.slice(last);
65
- return escaped.replace(/,(\s*[}\]])/g, "$1");
51
+ return jsonrepair(json);
66
52
  }
53
+
67
54
  export function bytesToString(value: string | Uint8Array): string {
68
55
  return typeof value === "string" ? value : new TextDecoder().decode(value);
69
56
  }
@@ -6,6 +6,7 @@ import {
6
6
  type ReasoningSummaryIdentity,
7
7
  } from "../reasoning-summary";
8
8
  import {
9
+ argumentsFrom,
9
10
  createToolCallTracker,
10
11
  resetToolCallTracker,
11
12
  toolIdentityKey,
@@ -96,7 +97,7 @@ export class MessagesStreamEncoder extends SurfaceStreamEncoder<CanonicalEvent,
96
97
  const index = existing ?? this.nextIndex++;
97
98
  this.tools.indexById.set(callId, index);
98
99
  const sent = this.emittedArguments.get(callId) ?? 0;
99
- const pending = entry.arguments.slice(sent);
100
+ const pending = argumentsFrom(entry, sent);
100
101
  if (!this.openedToolCalls.has(callId)) {
101
102
  this.openedToolCalls.add(callId);
102
103
  out.push(...this.closeOpenBlock());
@@ -120,7 +121,7 @@ export class MessagesStreamEncoder extends SurfaceStreamEncoder<CanonicalEvent,
120
121
  index,
121
122
  delta: { type: "input_json_delta", partial_json: pending },
122
123
  });
123
- this.emittedArguments.set(callId, entry.arguments.length);
124
+ this.emittedArguments.set(callId, sent + pending.length);
124
125
  }
125
126
  return out;
126
127
  }