cartethyia 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -25,11 +25,65 @@ async function qoderVersion(response: Response): Promise<string | null> {
25
25
  }
26
26
  }
27
27
 
28
+ /**
29
+ * Newest Trae IDE version from the official changelog.
30
+ *
31
+ * Trae publishes no npm package and no release API, so the changelog is the
32
+ * only machine-readable source of the current client version. It matters
33
+ * upstream: intl `llm_raw_chat` answers `4015` unless the request carries the
34
+ * *current* client version, so a stale pin is an outage, not a cosmetic drift.
35
+ *
36
+ * The page embeds its content as JSON, and its newest entry is the first dated
37
+ * heading — not the highest semver, because this vendor's hotfix line numbers
38
+ * past its feature line (3.5.104 shipped after 3.5.97). Taking `max(semver)`
39
+ * would report an old build as current, so this walks the document in order and
40
+ * returns the first version following a dated release heading.
41
+ */
42
+ async function traeVersion(response: Response): Promise<string | null> {
43
+ try {
44
+ const text = await response.text();
45
+ const heading =
46
+ /"insert":"((?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},\s*\d{4}\s*\([^)]*\))\\n"/g;
47
+ for (const match of text.matchAll(heading)) {
48
+ const window = text.slice(match.index ?? 0, (match.index ?? 0) + 900);
49
+ const found = window.match(/TraeCode\s+v?(\d+\.\d+\.\d+)|v?(\d+\.\d+\.\d+)\s+is\s+released/i);
50
+ const version = found?.[1] ?? found?.[2];
51
+ if (version && isSemverish(version)) return version.trim();
52
+ }
53
+ return null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
28
59
  async function clineClientVersion(response: Response): Promise<string | null> {
29
60
  const data = (await response.json()) as { version?: unknown };
30
61
  return isSemverish(data.version) ? data.version.trim() : null;
31
62
  }
32
63
 
64
+ /**
65
+ * Newest Cursor build from the official download API, as the IDE's own header
66
+ * spells it.
67
+ *
68
+ * Cursor's `x-cursor-client-version` is not the IDE semver on its own: the wire
69
+ * value is `cli-<version>-<sha7>` (observed live as `cli-2026.10.19-6bbf2c4`).
70
+ * The download API is the only source that returns both halves — `version` and
71
+ * `commitSha` — so they are joined here rather than guessed. A semver alone
72
+ * would be a different identity than the one the IDE sends.
73
+ */
74
+ async function cursorVersion(response: Response): Promise<string | null> {
75
+ try {
76
+ const data = (await response.json()) as { version?: unknown; commitSha?: unknown };
77
+ const version = data.version;
78
+ if (!isSemverish(version)) return null;
79
+ const sha = typeof data.commitSha === "string" ? data.commitSha.trim() : "";
80
+ const short = sha.length >= 7 ? sha.slice(0, 7) : "";
81
+ return short.length > 0 ? `cli-${version.trim()}-${short}` : version.trim();
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
33
87
  async function clineSdkVersion(response: Response): Promise<string | null> {
34
88
  const data = (await response.json()) as {
35
89
  version?: unknown;
@@ -177,7 +231,7 @@ export const VERSION_SOURCES = {
177
231
  },
178
232
  qoder: {
179
233
  key: "qoder",
180
- fallback: "1.1.73",
234
+ fallback: "1.1.74",
181
235
  sources: [
182
236
  {
183
237
  url: "https://registry.npmjs.org/@qoder-ai/qodercli/latest",
@@ -222,6 +276,11 @@ export const VERSION_SOURCES = {
222
276
  fallback: "0.0.95",
223
277
  sources: [{ url: "https://registry.npmjs.org/@cline/sdk/latest", extract: clineSdkVersion }],
224
278
  },
279
+ clineCli: {
280
+ key: "cline-cli",
281
+ fallback: "3.0.73",
282
+ sources: [{ url: "https://registry.npmjs.org/cline/latest", extract: clineSdkVersion }],
283
+ },
225
284
  codex: {
226
285
  key: "codex",
227
286
  fallback: "0.163.0",
@@ -257,7 +316,7 @@ export const VERSION_SOURCES = {
257
316
  },
258
317
  workbuddyCli: {
259
318
  key: "workbuddy",
260
- fallback: "2.168.2",
319
+ fallback: "2.169.0",
261
320
  sources: [
262
321
  { url: "https://registry.npmjs.org/@tencent-ai/codebuddy-code/latest" },
263
322
  { url: "https://registry.npmmirror.com/@tencent-ai/codebuddy-code/latest" },
@@ -284,7 +343,7 @@ export const VERSION_SOURCES = {
284
343
  },
285
344
  codebuddy: {
286
345
  key: "codebuddy",
287
- fallback: "2.168.2",
346
+ fallback: "2.169.0",
288
347
  sources: [
289
348
  { url: "https://registry.npmjs.org/@tencent-ai/codebuddy-code/latest" },
290
349
  { url: "https://registry.npmmirror.com/@tencent-ai/codebuddy-code/latest" },
@@ -342,6 +401,46 @@ export const VERSION_SOURCES = {
342
401
  fallback: "1.4.10-R6427.1",
343
402
  sources: [{ url: "https://api.meta.ai/muse-code/channels/muse-stable" }],
344
403
  },
404
+ trae: {
405
+ key: "trae",
406
+ // Fallback = the donor's last observed client version. Discovery replaces
407
+ // it within one monitor tick; it is only what a cold process sends before
408
+ // the first successful fetch.
409
+ fallback: "3.6.2",
410
+ sources: [
411
+ {
412
+ url: "https://docs.trae.ai/ide/changelog?_lang=en",
413
+ extract: traeVersion,
414
+ },
415
+ ],
416
+ },
417
+ cursor: {
418
+ key: "cursor",
419
+ // Fallback = the last observed wire version. Discovery replaces it within
420
+ // one monitor tick; it is only what a cold process sends before the first
421
+ // successful fetch.
422
+ fallback: "cli-2026.10.19-6bbf2c4",
423
+ sources: [
424
+ {
425
+ url: "https://cursor.com/api/download?platform=win32-x64-user&releaseTrack=stable",
426
+ extract: cursorVersion,
427
+ },
428
+ ],
429
+ },
430
+ hermes: {
431
+ key: "hermes",
432
+ // Hermes DOES publish a semver: the GitHub release tag is `v0.22.2`. The
433
+ // comment this replaced claimed no semver existed and pinned a date tag
434
+ // (`v2026.9.24`) as the version — a tag, not a release, and not the
435
+ // identity the client sends.
436
+ fallback: "0.22.2",
437
+ sources: [
438
+ {
439
+ url: "https://api.github.com/repos/NousResearch/hermes-agent/releases/latest",
440
+ extract: githubReleaseVersion,
441
+ },
442
+ ],
443
+ },
345
444
  zcode: {
346
445
  key: "zcode",
347
446
  fallback: "3.14.3",
@@ -357,11 +456,15 @@ export const VERSION_SOURCES = {
357
456
  const resolvers = {
358
457
  antigravity: createClientVersionResolver({ ...VERSION_SOURCES.antigravity, client: "antigravity" }),
359
458
  qoder: createClientVersionResolver({ ...VERSION_SOURCES.qoder, client: "qoder" }),
459
+ hermes: createClientVersionResolver({ ...VERSION_SOURCES.hermes, client: "hermes" }),
460
+ trae: createClientVersionResolver({ ...VERSION_SOURCES.trae, client: "trae" }),
461
+ cursor: createClientVersionResolver({ ...VERSION_SOURCES.cursor, client: "cursor" }),
360
462
  opencode: createClientVersionResolver({ ...VERSION_SOURCES.opencode, client: "opencode" }),
361
463
  commandcode: createClientVersionResolver({ ...VERSION_SOURCES.commandcode, client: "command-code" }),
362
464
  grok: createClientVersionResolver({ ...VERSION_SOURCES.grok, client: "grok" }),
363
465
  clineClient: createClientVersionResolver({ ...VERSION_SOURCES.clineClient, client: "cline" }),
364
466
  clineSdk: createClientVersionResolver({ ...VERSION_SOURCES.clineSdk, client: "cline-sdk" }),
467
+ clineCli: createClientVersionResolver({ ...VERSION_SOURCES.clineCli, client: "cline-cli" }),
365
468
  codex: createClientVersionResolver({ ...VERSION_SOURCES.codex, client: "codex" }),
366
469
  workbuddyClient: createClientVersionResolver({ ...VERSION_SOURCES.workbuddyClient, client: "workbuddy" }),
367
470
  workbuddyCnClient: createClientVersionResolver({ ...VERSION_SOURCES.workbuddyCnClient, client: "workbuddy-cn" }),
@@ -384,9 +487,16 @@ const PROVIDER_VERSION_RESOLVERS: Readonly<Record<string, ClientVersionResolver>
384
487
  opencode: resolvers.opencode,
385
488
  "opencode-zen": resolvers.opencode,
386
489
  "opencode-go": resolvers.opencode,
387
- cline: resolvers.clineClient,
490
+ // The identity this provider is dispatched under is the CLI's, so the route
491
+ // identity reports the CLI version. `clineClient` (the VS Code extension
492
+ // manifest) is no longer the client this provider claims to be.
493
+ cline: resolvers.clineCli,
388
494
  codex: resolvers.codex,
389
495
  qoder: resolvers.qoder,
496
+ hermes: resolvers.hermes,
497
+ trae: resolvers.trae,
498
+ traecn: resolvers.trae,
499
+ cursor: resolvers.cursor,
390
500
  commandcode: resolvers.commandcode,
391
501
  workbuddy: resolvers.workbuddyClient,
392
502
  workbuddycn: resolvers.workbuddyCnClient,
@@ -484,6 +594,8 @@ function accessor<K extends keyof typeof resolvers>(name: K): {
484
594
  }
485
595
 
486
596
  export const getQoderVersion = accessor("qoder").get;
597
+ export const getHermesVersion = accessor("hermes").get;
598
+ export const getTraeVersion = accessor("trae").get;
487
599
  export const getAntigravityVersion = accessor("antigravity").get;
488
600
 
489
601
  export const getOpenCodeVersion = accessor("opencode").get;
@@ -503,8 +615,10 @@ export function buildGrokAuthUserAgent(version = getGrokVersion()): string {
503
615
 
504
616
  export const getClineClientVersion = accessor("clineClient").get;
505
617
  export const getClineSdkVersion = accessor("clineSdk").get;
618
+ export const getClineCliVersion = accessor("clineCli").get;
506
619
  export const _resetClineClientVersionCache = accessor("clineClient").reset;
507
620
  export const _resetClineSdkVersionCache = accessor("clineSdk").reset;
621
+ export const _resetClineCliVersionCache = accessor("clineCli").reset;
508
622
 
509
623
  export const getCodexVersion = accessor("codex").get;
510
624
 
@@ -513,9 +627,16 @@ export const getWorkBuddyCnClientVersion = accessor("workbuddyCnClient").get;
513
627
  export const getWorkBuddyCliVersion = accessor("workbuddyCli").get;
514
628
 
515
629
  export function buildWorkBuddyUserAgent(
630
+ identity = "WB",
516
631
  clientVersion = getWorkBuddyClientVersion(),
517
632
  cliVersion = getWorkBuddyCliVersion(),
518
633
  ): string {
634
+ // 1:1 with codebuddy2api. The CLI profile is `CLI/<v> CodeBuddy/<v>` and the
635
+ // WorkBuddy profile is `WorkBuddy/<wb> WorkBuddy AI/<wb> CLI/<cli>`, which
636
+ // sends NO SDK headers — two separate profiles, not one string with a switch.
637
+ // Whether SDK headers ride along is decided by `workbuddyHeaders`, not here, so
638
+ // this builder only decides the UA text.
639
+ if (identity === "CLI") return `CLI/${cliVersion} CodeBuddy/${cliVersion}`;
519
640
  return `WorkBuddy/${clientVersion} WorkBuddy AI/${clientVersion} CLI/${cliVersion}`;
520
641
  }
521
642
 
@@ -525,11 +646,27 @@ export const getKimiCliVersion = accessor("kimiCli").get;
525
646
 
526
647
  export const getCodeBuddyVersion = accessor("codebuddy").get;
527
648
  export const getCodeBuddyCnVersion = accessor("codebuddyCn").get;
649
+ /**
650
+ * The CodeBuddy User-Agent.
651
+ *
652
+ * Both identities share the CLI *package* version, which is why `codebuddy`
653
+ * (the npm `@tencent-ai/codebuddy-code` pin, `2.169.0` — the value both
654
+ * references use for CLI) is the version for each. `codebuddyCn` is the CN
655
+ * *desktop* build (`4.12.2.…`), and using it here produced
656
+ * `CLI/4.12.2.41207131 CodeBuddy/4.12.2.41207131` — a CLI claiming a desktop
657
+ * version, which no official client emits.
658
+ */
528
659
  export function buildCodeBuddyUserAgent(
529
660
  identity: "IDE" | "CLI",
530
- version = identity === "CLI" ? getCodeBuddyCnVersion() : getCodeBuddyVersion(),
661
+ version = getCodeBuddyVersion(),
662
+ cnVersion = getCodeBuddyCnVersion(),
531
663
  ): string {
532
- return `${identity}/${version} CodeBuddy/${version}`;
664
+ // 1:1 with codebuddy2api's identity_headers(): the CLI profile uses ONE version
665
+ // (the npm CLI package pin) for both halves, and the IDE profile uses the other
666
+ // product's version. This previously always used `version`, so "CLI" produced
667
+ // `CLI/<ide> CodeBuddy/<ide>` — a CLI claiming the desktop build.
668
+ const resolved = identity === "CLI" ? cnVersion : version;
669
+ return `${identity}/${resolved} CodeBuddy/${resolved}`;
533
670
  }
534
671
 
535
672
  export const _resetCodeBuddyVersionCache = accessor("codebuddy").reset;
@@ -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
+ }
@@ -1,86 +1,95 @@
1
- /** Durable materialization of bundled provider model metadata. */
2
- import { and, eq, sql } from "drizzle-orm";
3
- import type { CartethyiaDatabase } from "../../persistence/db-handle";
4
- import { models } from "../../persistence/schema";
5
- import type { ModelDefinition } from "../provider-model-definition";
6
-
7
- /** Persists compiled provider model metadata and reconciles drifted capability flags. */
8
- export async function seedBundledModels(
9
- db: CartethyiaDatabase,
10
- builtinModels: ReadonlyMap<string, readonly ModelDefinition[]>,
11
- ): Promise<void> {
12
- for (const [providerId, definitions] of builtinModels) {
13
- if (definitions.length === 0) continue;
14
- // Drop builtin rows the current catalog no longer declares, including rows
15
- // whose model id is still present but whose endpoint path drifted (e.g. a
16
- // provider that moved from `/chat/completions` to `/v2/chat/completions`).
17
- // The unique key is (provider, model, endpoint), so a changed endpoint
18
- // would otherwise insert a second row and leave the stale route behind —
19
- // the model list would then show duplicates and probes could hit the dead
20
- // path. Composite `NOT IN` keeps exactly the (model, endpoint) pairs the
21
- // catalog owns.
22
- const pairs = definitions.map(
23
- (definition) => sql`(${definition.modelId}, ${definition.endpointPath})`,
24
- );
25
- await db
26
- .delete(models)
27
- .where(
28
- and(
29
- eq(models.providerId, providerId),
30
- eq(models.source, "builtin"),
31
- sql`(${models.modelId}, ${models.endpointPath}) NOT IN (${sql.join(pairs, sql`, `)})`,
32
- ),
33
- );
34
- }
35
- const now = new Date();
36
- const seen = new Set<string>();
37
- const rows = [...builtinModels.entries()].flatMap(([providerId, definitions]) =>
38
- definitions.flatMap((definition) => {
39
- const key = `${providerId}\u0000${definition.modelId}\u0000${definition.endpointPath}`;
40
- if (seen.has(key)) return [];
41
- seen.add(key);
42
- return [
43
- {
44
- providerId,
45
- modelId: definition.modelId,
46
- wireFamily: definition.wireFamily,
47
- serviceKind: definition.serviceKind ?? "llm",
48
- endpointPath: definition.endpointPath,
49
- contextLimit: definition.contextLimit,
50
- outputLimit: definition.outputLimit,
51
- modalities: definition.modalities,
52
- reasoning: definition.reasoning,
53
- toolCall: definition.toolCall,
54
- cost: definition.cost,
55
- source: "builtin",
56
- sourceUpdatedAt: now,
57
- enabled: true,
58
- },
59
- ];
60
- }),
61
- );
62
- if (rows.length === 0) return;
63
- // Reconcile static metadata on conflict rather than skipping: capability
64
- // flags (toolCall/reasoning/webSearch), wire family, limits, and cost can
65
- // drift on a pre-existing row (e.g. a discovery seeded `tool_call=false`
66
- // before the static catalog was corrected). `source` is forced back to
67
- // `builtin`: a fetch upsert may have flipped a builtin row to `discovered`,
68
- // and the static catalog owns these ids. `enabled` is operator-owned and
69
- // must never be reset here.
70
- await db.insert(models).values(rows).onConflictDoUpdate({
71
- target: [models.providerId, models.modelId, models.endpointPath],
72
- set: {
73
- wireFamily: sql`excluded.wire_family`,
74
- serviceKind: sql`excluded.service_kind`,
75
- endpointPath: sql`excluded.endpoint_path`,
76
- contextLimit: sql`excluded.context_limit`,
77
- outputLimit: sql`excluded.output_limit`,
78
- modalities: sql`excluded.modalities`,
79
- reasoning: sql`excluded.reasoning`,
80
- toolCall: sql`excluded.tool_call`,
81
- cost: sql`excluded.cost`,
82
- source: sql`'builtin'`,
83
- sourceUpdatedAt: sql`excluded.source_updated_at`,
84
- },
85
- });
86
- }
1
+ /** Durable materialization of bundled provider model metadata. */
2
+ import { and, eq, sql } from "drizzle-orm";
3
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
4
+ import { models } from "../../persistence/schema";
5
+ import type { ModelDefinition } from "../provider-model-definition";
6
+
7
+ /** Persists compiled provider model metadata and reconciles drifted capability flags. */
8
+ export async function seedBundledModels(
9
+ db: CartethyiaDatabase,
10
+ builtinModels: ReadonlyMap<string, readonly ModelDefinition[]>,
11
+ ): Promise<void> {
12
+ for (const [providerId, definitions] of builtinModels) {
13
+ if (definitions.length === 0) continue;
14
+ // Drop builtin rows the current catalog no longer declares, including rows
15
+ // whose model id is still present but whose endpoint path drifted (e.g. a
16
+ // provider that moved from `/chat/completions` to `/v2/chat/completions`).
17
+ // The unique key is (provider, model, endpoint), so a changed endpoint
18
+ // would otherwise insert a second row and leave the stale route behind —
19
+ // the model list would then show duplicates and probes could hit the dead
20
+ // path. Composite `NOT IN` keeps exactly the (model, endpoint) pairs the
21
+ // catalog owns.
22
+ const pairs = definitions.map(
23
+ (definition) => sql`(${definition.modelId}, ${definition.endpointPath})`,
24
+ );
25
+ await db
26
+ .delete(models)
27
+ .where(
28
+ and(
29
+ eq(models.providerId, providerId),
30
+ eq(models.source, "builtin"),
31
+ sql`(${models.modelId}, ${models.endpointPath}) NOT IN (${sql.join(pairs, sql`, `)})`,
32
+ ),
33
+ );
34
+ }
35
+ const now = new Date();
36
+ const seen = new Set<string>();
37
+ const rows = [...builtinModels.entries()].flatMap(([providerId, definitions]) =>
38
+ definitions.flatMap((definition) => {
39
+ const key = `${providerId}\u0000${definition.modelId}\u0000${definition.endpointPath}`;
40
+ if (seen.has(key)) return [];
41
+ seen.add(key);
42
+ return [
43
+ {
44
+ providerId,
45
+ modelId: definition.modelId,
46
+ wireFamily: definition.wireFamily,
47
+ serviceKind: definition.serviceKind ?? "llm",
48
+ endpointPath: definition.endpointPath,
49
+ contextLimit: definition.contextLimit,
50
+ outputLimit: definition.outputLimit,
51
+ modalities: definition.modalities,
52
+ reasoning: definition.reasoning,
53
+ toolCall: definition.toolCall,
54
+ cost: definition.cost,
55
+ // A static row either knows it is refused on a free plan or says
56
+ // nothing. Never coerced to false: the column is three-valued, and
57
+ // "nobody established it" is not the same claim as "served on free".
58
+ proOnly: definition.proOnly ?? null,
59
+ // Same three-valued rule for the inverse: the provider's directory
60
+ // states the tier, so the flag is carried rather than derived.
61
+ freeTier: definition.freeTier ?? null,
62
+ source: "builtin",
63
+ sourceUpdatedAt: now,
64
+ enabled: true,
65
+ },
66
+ ];
67
+ }),
68
+ );
69
+ if (rows.length === 0) return;
70
+ // Reconcile static metadata on conflict rather than skipping: capability
71
+ // flags (toolCall/reasoning/webSearch), wire family, limits, and cost can
72
+ // drift on a pre-existing row (e.g. a discovery seeded `tool_call=false`
73
+ // before the static catalog was corrected). `source` is forced back to
74
+ // `builtin`: a fetch upsert may have flipped a builtin row to `discovered`,
75
+ // and the static catalog owns these ids. `enabled` is operator-owned and
76
+ // must never be reset here.
77
+ await db.insert(models).values(rows).onConflictDoUpdate({
78
+ target: [models.providerId, models.modelId, models.endpointPath],
79
+ set: {
80
+ wireFamily: sql`excluded.wire_family`,
81
+ serviceKind: sql`excluded.service_kind`,
82
+ endpointPath: sql`excluded.endpoint_path`,
83
+ contextLimit: sql`excluded.context_limit`,
84
+ outputLimit: sql`excluded.output_limit`,
85
+ modalities: sql`excluded.modalities`,
86
+ reasoning: sql`excluded.reasoning`,
87
+ toolCall: sql`excluded.tool_call`,
88
+ cost: sql`excluded.cost`,
89
+ proOnly: sql`excluded.pro_only`,
90
+ freeTier: sql`excluded.free_tier`,
91
+ source: sql`'builtin'`,
92
+ sourceUpdatedAt: sql`excluded.source_updated_at`,
93
+ },
94
+ });
95
+ }
@@ -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
  }