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
@@ -12,6 +12,8 @@ import { createAnthropicCompatibleAdapter } from "./integrations/anthropic";
12
12
  import { defineModel } from "./provider-model-definition";
13
13
  import type { ProviderModelDiscovery } from "./discovery/discovery-types";
14
14
  import type { ProviderDispatchContext } from "./provider-contracts";
15
+ import type { QoderRealm } from "./integrations/qoder/qoder-realm";
16
+ import type { TraeRealm } from "./integrations/trae/trae-realm";
15
17
  import type { WireFamily } from "../protocol/canonical-model";
16
18
  import type { CanonicalRequest } from "../protocol/canonical-model";
17
19
 
@@ -25,6 +27,106 @@ function configuredProvider(
25
27
  };
26
28
  }
27
29
 
30
+ /**
31
+ * Qoder's live model list for one realm.
32
+ *
33
+ * The request has to be signed with the account's COSY identity and has to
34
+ * carry the body that was signed — a bare GET is refused — so discovery cannot
35
+ * use the generic bearer helper. It resolves the same identity dispatch uses
36
+ * (PAT exchange or the persisted OAuth state) and then lists against the
37
+ * realm's own gateway hosts.
38
+ */
39
+ /**
40
+ * Trae model discovery for one realm.
41
+ *
42
+ * The listing is per account region: it answers on CN and on the intl region
43
+ * host that serves the account. This returns the static roster when the live
44
+ * call yields nothing, rather than raising, so one unsupported account cannot
45
+ * fail the whole catalog refresh.
46
+ */
47
+ function traeDiscovery(
48
+ realm: TraeRealm,
49
+ providerId: string,
50
+ ): NonNullable<ProviderModuleCapabilities["loadModelDiscovery"]> {
51
+ return async () => async (request: Parameters<ProviderModelDiscovery>[0]) => {
52
+ const { discoverTraeModels } = await import("./integrations/trae/trae");
53
+ const { TRAE_INTL_MODELS, TRAE_CN_MODELS } = await import("./integrations/trae/trae-catalog");
54
+ const secret = request.credential.trim();
55
+ const fetcher = request.fetcher as unknown as typeof fetch | undefined;
56
+ if (secret.length === 0 || fetcher === undefined) {
57
+ return realm === "cn" ? TRAE_CN_MODELS : TRAE_INTL_MODELS;
58
+ }
59
+ let token = secret;
60
+ if (secret.startsWith("{")) {
61
+ try {
62
+ const parsed = JSON.parse(secret) as Record<string, unknown>;
63
+ const found = ["accessToken", "access_token", "token"].find(
64
+ (key) => typeof parsed[key] === "string" && (parsed[key] as string).length > 0,
65
+ );
66
+ if (found !== undefined) token = parsed[found] as string;
67
+ } catch {
68
+ // Use the raw secret as the token.
69
+ }
70
+ }
71
+ const region = request.authState?.["region"];
72
+ const rows = await discoverTraeModels({
73
+ realm,
74
+ token,
75
+ uid: request.authState?.["accountId"] as string | undefined ?? "",
76
+ fetcher,
77
+ providerId,
78
+ ...(typeof region === "string" ? { region } : {}),
79
+ });
80
+ if (rows.length === 0) return realm === "cn" ? TRAE_CN_MODELS : TRAE_INTL_MODELS;
81
+ const fallback = realm === "cn" ? TRAE_CN_MODELS : TRAE_INTL_MODELS;
82
+ const byId = new Map(fallback.map((model) => [model.modelId, model]));
83
+ return rows.map((row) => {
84
+ const existing = byId.get(row.id);
85
+ if (existing === undefined) {
86
+ // Discovered upstream but absent from the static roster: clone the
87
+ // realm's first row so a new model is routable with the shape this
88
+ // provider serves, carrying the window upstream reported.
89
+ const template = fallback[0];
90
+ if (template === undefined) return undefined;
91
+ return { ...template, modelId: row.id, contextLimit: row.ctx ?? null };
92
+ }
93
+ return row.ctx === undefined ? existing : { ...existing, contextLimit: row.ctx };
94
+ }).filter((model): model is NonNullable<typeof model> => model !== undefined);
95
+ };
96
+ }
97
+
98
+ function qoderDiscovery(
99
+ realm: QoderRealm,
100
+ providerId: string,
101
+ ): NonNullable<ProviderModuleCapabilities["loadModelDiscovery"]> {
102
+ return async () => async (request: Parameters<ProviderModelDiscovery>[0]) => {
103
+ const { discoverQoderModels } = await import("./integrations/qoder/qoder-catalog");
104
+ const { resolveQoderAuth, qoderSignerFor, qoderEncodeBody } = await import("./integrations/qoder/qoder");
105
+ const secret = request.credential.trim();
106
+ if (secret.length === 0) throw new Error("Qoder credential is required");
107
+ const fetcher = request.fetcher as unknown as typeof fetch | undefined;
108
+ // The discovery context carries only the secret plus the non-secret
109
+ // `authState` an OAuth login persisted; the account id lives there too, so
110
+ // an OAuth credential resolves to the same identity dispatch would use.
111
+ const auth = await resolveQoderAuth({
112
+ secret,
113
+ authState: request.authState,
114
+ signal: AbortSignal.timeout(15_000),
115
+ fetcher: (url: string, init: RequestInit) => (fetcher ?? globalThis.fetch)(url, init),
116
+ });
117
+ const signer = qoderSignerFor(auth);
118
+ return discoverQoderModels({
119
+ realm,
120
+ providerId,
121
+ credential: secret,
122
+ headers: (url: string, body: string) => Promise.resolve(signer(url, body)),
123
+ ...(fetcher === undefined ? {} : { fetcher }),
124
+ encode: qoderEncodeBody,
125
+ });
126
+ };
127
+ }
128
+
129
+
28
130
  /**
29
131
  * Builds the `loadAdapter` + `loadModelDiscovery` pair every plain bearer
30
132
  * provider needs.
@@ -201,6 +303,29 @@ export const PROVIDER_CAPABILITIES = {
201
303
  loadAuthentication: oauthCapability(() => import("./integrations/kimi/kimi-oauth"), "kimiCodeOAuthClient", { withRefresher: true }),
202
304
  loadQuotaCollector: quotaCapability(() => import("./integrations/kimi/kimi-quota"), "fetchKimiQuota"),
203
305
  },
306
+ cursor: {
307
+ // Cursor's Run is a Connect-RPC bidi stream on a *dynamically discovered*
308
+ // agent origin, not the API host the auth and catalog calls use. The path
309
+ // is pinned here so a probe verifies the endpoint dispatch actually calls;
310
+ // the origin itself is resolved per dispatch from GetServerConfig.
311
+ endpointPathsByWireFamily: { chat: "/agent.v1.AgentService/Run" },
312
+ loadAdapter: async () => (await import("./integrations/cursor/cursor")).cursorAdapter,
313
+ loadModels: async () => (await import("./integrations/cursor/cursor-catalog")).CURSOR_MODELS,
314
+ loadAuthentication: oauthCapability(() => import("./integrations/cursor/cursor-auth"), "cursorOAuthClient", {
315
+ withRefresher: true,
316
+ }),
317
+ // No quota collector: no Cursor quota endpoint is established by the donor
318
+ // or the API evidence, and inventing one would publish a balance this
319
+ // gateway cannot measure.
320
+ //
321
+ // The roster is per-account and per-plan, and Cursor declares each model's
322
+ // capabilities, so discovery is the catalog rather than a static list.
323
+ loadModelDiscovery: async () => async ({ credential, fetcher }) =>
324
+ (await import("./integrations/cursor/cursor-catalog")).discoverCursorModels({
325
+ credential,
326
+ ...(fetcher === undefined ? {} : { fetcher }),
327
+ }),
328
+ },
204
329
  cerebras: {
205
330
  endpointPathsByWireFamily: { chat: "/chat/completions" },
206
331
  loadAdapter: async () => createApiKeyAdapter((await import("./integrations/cerebras")).CEREBRAS_SPEC),
@@ -274,6 +399,10 @@ export const PROVIDER_CAPABILITIES = {
274
399
  loadAdapter: async () => createApiKeyAdapter((await import("./integrations/opencode/provider")).OPENCODE_GO_SPEC),
275
400
  loadModels: async () => (await import("./integrations/opencode/provider")).OPENCODE_GO_MODELS,
276
401
  loadModelDiscovery: openAIModelDiscovery("opencode-go", { transformBaseUrl: (baseUrl) => `${baseUrl.replace(/\/+$/, "")}/zen/go/v1`, headers: (credential) => ({ authorization: `Bearer ${credential}` }) }),
402
+ // The Go plan publishes a real usage surface; the anonymous free tier has
403
+ // none (it is rate-limited by egress IP, not by a balance), so this is
404
+ // deliberately registered on the keyed tier only.
405
+ loadQuotaCollector: async () => (await import("./integrations/opencode/opencode-go-quota")).fetchOpenCodeGoQuota,
277
406
  },
278
407
  agentrouter: {
279
408
  loadAdapter: async () => (await import("./integrations/agentrouter/agentrouter")).createAgentRouterAdapter(),
@@ -320,7 +449,10 @@ export const PROVIDER_CAPABILITIES = {
320
449
  },
321
450
  kilo: {
322
451
  loadAdapter: async () => (await import("./integrations/kilo/kilo")).kiloAdapter,
323
- loadModels: async () => (await import("./integrations/kilo/kilo")).KILO_MODELS,
452
+ loadModels: async () => {
453
+ const spec = await import("./integrations/kilo/kilo");
454
+ return [...spec.KILO_MODELS, ...spec.KILO_FREE_MODELS];
455
+ },
324
456
  // No `withRefresher`: Kilo Code has no refresh grant, so this client
325
457
  // declares no refresher and the stored token is used exactly as issued.
326
458
  // Registering one would make the 401 retry path call a method that always
@@ -335,6 +467,14 @@ export const PROVIDER_CAPABILITIES = {
335
467
  commandcode: {
336
468
  loadAdapter: async () => (await import("./integrations/commandcode/commandcode")).createCommandCodeAdapter(),
337
469
  loadModels: async () => (await import("./integrations/commandcode/commandcode")).COMMANDCODE_MODELS,
470
+ // The Provider API publishes the real roster with each model's wire, so the
471
+ // static seed above is the offline floor rather than the catalog.
472
+ loadModelDiscovery: async () => async (input) =>
473
+ (await import("./integrations/commandcode/discovery")).discoverCommandCodeModels(input),
474
+ loadQuotaCollector: quotaCapability(
475
+ () => import("./integrations/commandcode/quota"),
476
+ "fetchCommandCodeQuota",
477
+ ),
338
478
  },
339
479
  qoder: {
340
480
  loadAdapter: async () => (await import("./integrations/qoder/qoder")).createQoderAdapter(),
@@ -343,6 +483,42 @@ export const PROVIDER_CAPABILITIES = {
343
483
  // device flow, so the issued token is used exactly as issued until the
344
484
  // operator re-runs login — same shape as Kilo Code.
345
485
  loadAuthentication: oauthCapability(() => import("./integrations/qoder/qoder-oauth"), "qoderOAuthClient"),
486
+ // Discovery signed by the account's own COSY identity: the model list is
487
+ // per-account and per-realm, and a GET without the signed body is 403.
488
+ loadModelDiscovery: qoderDiscovery("intl", "qoder"),
489
+ },
490
+ // Separate provider, not a region flag: a CN account has its own gateway
491
+ // host, OAuth client id, and model roster, and is refused by the global host.
492
+ qodercn: {
493
+ loadAdapter: async () => (await import("./integrations/qoder/qoder")).createQoderCnAdapter(),
494
+ loadModels: async () => (await import("./integrations/qoder/qoder-catalog")).QODER_CN_MODELS,
495
+ // The CN client, not the intl one: a device login opened on
496
+ // `qoder.com/device/selectAccounts` polled `openapi.qoder.sh`, which a CN
497
+ // account is refused by. `qoderCnOAuthClient` is the same class bound to
498
+ // `QODER_REALMS.cn` — its own login/token/userinfo hosts, client id, and
499
+ // native redirect.
500
+ loadAuthentication: oauthCapability(() => import("./integrations/qoder/qoder-oauth"), "qoderCnOAuthClient"),
501
+ loadModelDiscovery: qoderDiscovery("cn", "qodercn"),
502
+ },
503
+ trae: {
504
+ loadAdapter: async () => (await import("./integrations/trae/trae")).createTraeAdapter(),
505
+ loadModels: async () => (await import("./integrations/trae/trae-catalog")).TRAE_INTL_MODELS,
506
+ // A Trae credential is an access/refresh pair and the refresh token is what
507
+ // keeps it alive, so the refresher is registered rather than optional:
508
+ // without it the account dies at first expiry.
509
+ loadAuthentication: oauthCapability(() => import("./integrations/trae/trae-oauth"), "traeOAuthClient", { withRefresher: true }),
510
+ // Live model list is intl-unsupported: upstream answers `403 no schema
511
+ // found`, so discovery returning nothing is the truthful result and the
512
+ // static roster above stays authoritative.
513
+ loadModelDiscovery: traeDiscovery("intl", "trae"),
514
+ },
515
+ traecn: {
516
+ loadAdapter: async () => (await import("./integrations/trae/trae")).createTraeCnAdapter(),
517
+ loadModels: async () => (await import("./integrations/trae/trae-catalog")).TRAE_CN_MODELS,
518
+ // The CN client, not the intl one: the exchange runs on the CN account
519
+ // host, and a refresh posted to the intl host is refused.
520
+ loadAuthentication: oauthCapability(() => import("./integrations/trae/trae-oauth"), "traeCnOAuthClient", { withRefresher: true }),
521
+ loadModelDiscovery: traeDiscovery("cn", "traecn"),
346
522
  },
347
523
  inferhub: {
348
524
  loadAdapter: async () => (await import("./integrations/inferhub")).createInferhubAdapter(),
@@ -166,6 +166,16 @@ const decoder = new TextDecoder();
166
166
 
167
167
  const FLAG_REASONING = 1;
168
168
  const FLAG_TOOL_CALL = 2;
169
+
170
+ /**
171
+ * Ceiling on {@link PackedCatalog}'s row-metadata cache.
172
+ *
173
+ * Sized well under the catalog's 8941 rows: a gateway resolves a working set of
174
+ * models, not the whole catalog, and every entry is rebuilt from the blob in a
175
+ * few typed-array reads. Holding more than this trades RAM for reads measured
176
+ * in nanoseconds.
177
+ */
178
+ export const METADATA_CACHE_MAX_ENTRIES = 2048;
169
179
  const SLASH = 0x2f;
170
180
 
171
181
  /** Lowercases and trims an ASCII id into the exact bytes the blob stores. */
@@ -182,6 +192,12 @@ export function lowerAscii(value: string): Uint8Array {
182
192
  * Holds typed-array views (zero-copy) and nothing else. Metadata objects are
183
193
  * created only for rows a caller actually resolves, and cached, so a long-lived
184
194
  * process pays for the handful of ids it queried rather than for all 8090.
195
+ *
196
+ * That cache is **bounded**: an entry is rebuilt from the blob in a few
197
+ * typed-array reads, so holding one at the cost of another is never worth it,
198
+ * and an unbounded memo over 8941 rows is memory the operator pays for at idle
199
+ * whether or not those rows are ever queried again. Bounded LRU, oldest
200
+ * insertion evicted first — the same shape the rest of the codebase uses.
185
201
  */
186
202
  export class PackedCatalog {
187
203
  private readonly bytes: Uint8Array;
@@ -399,6 +415,10 @@ export class PackedCatalog {
399
415
  reasoning: (flags & FLAG_REASONING) !== 0,
400
416
  toolCall: (flags & FLAG_TOOL_CALL) !== 0,
401
417
  };
418
+ if (this.metadataCache.size >= METADATA_CACHE_MAX_ENTRIES) {
419
+ const oldest = this.metadataCache.keys().next().value;
420
+ if (oldest !== undefined) this.metadataCache.delete(oldest);
421
+ }
402
422
  this.metadataCache.set(index, metadata);
403
423
  return metadata;
404
424
  }
@@ -134,25 +134,38 @@ const UNKNOWN_COST: ModelsDevModelCost = {
134
134
  };
135
135
 
136
136
  /**
137
- * Decoded once, at module load.
137
+ * The packed catalog, decompressed on first use and memoized.
138
138
  *
139
139
  * Brotli + base64, not `JSON.parse`: the blob is columnar, so this allocates
140
140
  * one buffer and takes typed-array views over it rather than materialising
141
141
  * 8900 objects. `CATALOG_BLOB_BYTES` is the decompressed length, checked here
142
- * so a consumer paired with a half-regenerated blob fails at boot rather than
143
- * resolving a truncated catalog.
142
+ * so a consumer paired with a half-regenerated blob fails on first access
143
+ * rather than resolving a truncated catalog.
144
+ *
145
+ * The decode is lazy on purpose. It is ~105 KB of base64 that expands to a
146
+ * much larger buffer, and it used to run at module load — so every process,
147
+ * including the ones that never resolve a model (a CLI subcommand, a migration
148
+ * run, a test that only touches the console), paid it on the boot path.
149
+ * Deferring it moves that cost to the first catalog lookup and pays it once.
144
150
  */
145
- const CATALOG_BYTES = brotliDecompressSync(Buffer.from(CATALOG_BLOB_BROTLI_BASE64, "base64"));
146
- if (CATALOG_BYTES.byteLength !== CATALOG_BLOB_BYTES) {
147
- throw new Error(
148
- `The packed model catalog is ${CATALOG_BYTES.byteLength} B, expected ` +
149
- `${CATALOG_BLOB_BYTES} B — regenerate it.`,
150
- );
151
- }
151
+ let catalogCache: PackedCatalog | undefined;
152
152
 
153
- // `PackedCatalog` copies into a 4-byte-aligned buffer of its own, so the
154
- // decompressed buffer is dropped here rather than held beside the copy.
155
- const CATALOG = new PackedCatalog(CATALOG_BYTES);
153
+ function catalog(): PackedCatalog {
154
+ const cached = catalogCache;
155
+ if (cached !== undefined) return cached;
156
+ const bytes = brotliDecompressSync(Buffer.from(CATALOG_BLOB_BROTLI_BASE64, "base64"));
157
+ if (bytes.byteLength !== CATALOG_BLOB_BYTES) {
158
+ throw new Error(
159
+ `The packed model catalog is ${bytes.byteLength} B, expected ` +
160
+ `${CATALOG_BLOB_BYTES} B — regenerate it.`,
161
+ );
162
+ }
163
+ // `PackedCatalog` copies into a 4-byte-aligned buffer of its own, so the
164
+ // decompressed buffer is dropped here rather than held beside the copy.
165
+ const packed = new PackedCatalog(bytes);
166
+ catalogCache = packed;
167
+ return packed;
168
+ }
156
169
 
157
170
  /**
158
171
  * Resolves model metadata and pricing exclusively from the packed base catalog
@@ -160,9 +173,13 @@ const CATALOG = new PackedCatalog(CATALOG_BYTES);
160
173
  */
161
174
  export class ModelsDevCatalog {
162
175
  /** When the generator fetched the upstream catalogs. */
163
- readonly fetchedAt: string = CATALOG.fetchedAt;
176
+ get fetchedAt(): string {
177
+ return catalog().fetchedAt;
178
+ }
164
179
  /** Every provider key the catalog files. */
165
- readonly providers: readonly string[] = CATALOG.providers;
180
+ get providers(): readonly string[] {
181
+ return catalog().providers;
182
+ }
166
183
 
167
184
  /**
168
185
  * Metadata for an exact `(providerIndex, idLower)` pair, or `undefined`.
@@ -172,19 +189,19 @@ export class ModelsDevCatalog {
172
189
  * matches exactly keeps winning.
173
190
  */
174
191
  private exactMetadata(providerLower: string, idLower: Uint8Array): ModelsDevModelMetadata | undefined {
175
- const direct = CATALOG.exactIndex(CATALOG.providerIndex(providerLower), idLower);
176
- if (direct >= 0) return CATALOG.metadataAt(direct);
192
+ const direct = catalog().exactIndex(catalog().providerIndex(providerLower), idLower);
193
+ if (direct >= 0) return catalog().metadataAt(direct);
177
194
  const mapped = CATALOG_PROVIDER_IDS[providerLower];
178
195
  if (mapped === undefined) return undefined;
179
- return CATALOG.metadataAt(CATALOG.exactIndex(CATALOG.providerIndex(mapped), idLower));
196
+ return catalog().metadataAt(catalog().exactIndex(catalog().providerIndex(mapped), idLower));
180
197
  }
181
198
 
182
199
  /** Metadata rows whose *bare* id equals `bare`, in catalog order. */
183
200
  private metadataForBare(bare: Uint8Array): ModelsDevModelMetadata[] {
184
- const [start, end] = CATALOG.bareRange(bare);
201
+ const [start, end] = catalog().bareRange(bare);
185
202
  const out: ModelsDevModelMetadata[] = [];
186
203
  for (let position = start; position < end; position += 1) {
187
- const metadata = CATALOG.metadataAt(CATALOG.bareOrderAt(position));
204
+ const metadata = catalog().metadataAt(catalog().bareOrderAt(position));
188
205
  if (metadata !== undefined) out.push(metadata);
189
206
  }
190
207
  return out;
@@ -327,8 +344,8 @@ export class ModelsDevCatalog {
327
344
  return own;
328
345
  }
329
346
  const idLower = lowerAscii(modelId);
330
- const row = CATALOG.globalCostRow(bareBytes(idLower));
331
- const global = row >= 0 ? CATALOG.rowCost(row) : undefined;
347
+ const row = catalog().globalCostRow(bareBytes(idLower));
348
+ const global = row >= 0 ? catalog().rowCost(row) : undefined;
332
349
  if (global !== undefined && global.pricing_model !== "unknown") return global;
333
350
  return own ?? global ?? UNKNOWN_COST;
334
351
  }
@@ -54,10 +54,18 @@ export async function fetchOpenAICompatibleModels(
54
54
  modalityContextLimit(entry);
55
55
  const contextWindow = parsedContext ?? fallback?.contextLimit ?? 200_000;
56
56
 
57
+ // `max_completion_tokens` first: it is the name an OpenAI-shaped catalog
58
+ // uses, and it is the name this gateway's own `/v1/models` emits. A
59
+ // reader that knew only `max_output_tokens`/`maxTokens` saw none of them
60
+ // and fell through to the 64_192 default, so every model on such an
61
+ // upstream was advertised at the same wrong ceiling no matter what the
62
+ // upstream reported.
57
63
  const parsedOutput =
64
+ positiveNumber(entry.max_completion_tokens) ??
58
65
  positiveNumber(entry.max_output_tokens) ??
59
66
  positiveNumber((entry as Record<string, unknown>).max_output_tokens) ??
60
67
  positiveNumber(entry.maxTokens) ??
68
+ positiveNumber((entry as Record<string, unknown>).completion_token_limit) ??
61
69
  modalityOutputLimit(entry);
62
70
  const maxOutputTokens = parsedOutput ?? fallback?.outputLimit ?? 64_192;
63
71
  const outputTokens = Math.min(maxOutputTokens, contextWindow);
@@ -74,7 +82,14 @@ export async function fetchOpenAICompatibleModels(
74
82
  // the upstream declares for the model it serves. A catalog that declares
75
83
  // it as an `input_modalities[]` array instead of a `modality` string was
76
84
  // read as text-only, so a vision model lost image input.
77
- const hasImage = rawModality.includes("image") || modalityImageInput(entry);
85
+ // `capabilities.input` is a third spelling — an array of modality names,
86
+ // used by OpenAI-shaped catalogs including this gateway's own. Read
87
+ // alongside the other two so a vision model is not downgraded to
88
+ // text-only, which then drops image parts before they reach the upstream.
89
+ const hasImage =
90
+ rawModality.includes("image") ||
91
+ modalityImageInput(entry) ||
92
+ capabilityImageInput(entry);
78
93
  const responsesWire = isResponsesNativeModelId(id);
79
94
  byId.set(id, {
80
95
  modelId: id,
@@ -83,8 +98,14 @@ export async function fetchOpenAICompatibleModels(
83
98
  contextLimit: contextWindow,
84
99
  outputLimit: outputTokens,
85
100
  modalities: { input: hasImage ? ["text", "image"] : ["text"], output: ["text"] },
86
- reasoning: false,
87
- toolCall: true,
101
+ // Upstream declares reasoning, so believe it rather than pinning false:
102
+ // a reasoning model advertised as non-reasoning is routed without the
103
+ // thinking budget it needs, and the caller's `reasoning_effort` is
104
+ // dropped as unsupported.
105
+ reasoning: entry.reasoning === true,
106
+ // Same for tool calling — an upstream that names a model as
107
+ // non-tool-capable must not have tools routed to it.
108
+ toolCall: entry.tool_call === false ? false : true,
88
109
  cost,
89
110
  });
90
111
  }
@@ -150,6 +171,21 @@ function modalityImageInput(entry: Record<string, unknown>): boolean {
150
171
 
151
172
 
152
173
 
174
+ /**
175
+ * Image input from a `capabilities.input` array.
176
+ *
177
+ * The field is a plain list of modality names (`["text","image"]`), not the
178
+ * structured `input_modalities[]` objects the sibling reader expects. A catalog
179
+ * that spells it this way was read as text-only.
180
+ */
181
+ function capabilityImageInput(entry: Record<string, unknown>): boolean {
182
+ const capabilities = entry.capabilities;
183
+ if (!isRecord(capabilities)) return false;
184
+ const inputs = capabilities.input;
185
+ if (!Array.isArray(inputs)) return false;
186
+ return inputs.some((name) => typeof name === "string" && name.toLowerCase().includes("image"));
187
+ }
188
+
153
189
  function extractEntries(payload: unknown): readonly Record<string, unknown>[] | null {
154
190
  if (Array.isArray(payload)) return payload.filter(isRecord);
155
191
  if (typeof payload !== "object" || payload === null) return null;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The free tier shared by the OpenRouter-backed providers.
3
+ *
4
+ * Cline, Kilo Code and OpenRouter serve one model catalog: the same 469 ids,
5
+ * and the same 17 of them carrying a `:free` suffix. Verified against all three
6
+ * live listings — every id below is present under `openrouter`, `kilo` and
7
+ * `cline` — so this roster is a catalog fact, not a per-provider guess, and it
8
+ * lives in one place rather than being restated three times.
9
+ *
10
+ * The limits, modalities and capability flags are read from OpenRouter's own
11
+ * `/api/v1/models`, which states `context_length`,
12
+ * `top_provider.max_completion_tokens`, `architecture.input_modalities` and
13
+ * `supported_parameters`. That is the serving catalog's declaration, so these
14
+ * are measurements rather than defaults.
15
+ *
16
+ * This is a floor, not the authority: each provider also discovers live, and a
17
+ * row the listing carries that this roster does not is still found there. The
18
+ * roster exists so a fresh install has a usable free tier before any fetch, and
19
+ * so the free section is populated even when discovery is unavailable.
20
+ */
21
+ import { defineModel } from "../provider-model-definition";
22
+ import type { ModelDefinition } from "../provider-model-definition";
23
+
24
+ export interface FreeRosterOptions {
25
+ readonly providerId: string;
26
+ /** The wire suffix that completes the provider's base URL. */
27
+ readonly endpoint: string;
28
+ }
29
+
30
+ interface FreeRow {
31
+ readonly id: string;
32
+ readonly ctx: number;
33
+ readonly out: number;
34
+ readonly vision?: boolean;
35
+ /** `supported_parameters` omits `tools` on the safety classifier only. */
36
+ readonly toolCall?: boolean;
37
+ }
38
+
39
+ /**
40
+ * The 17 `:free` ids, with the limits the catalog states for each.
41
+ *
42
+ * Every row declares `reasoning`: `supported_parameters` lists it on all
43
+ * seventeen. `toolCall` is `true` unless stated otherwise, because a row that
44
+ * advertises tools must keep them — a false `toolCall` makes the preflight
45
+ * strip `tools` from the request, which is a wrong capability rather than a
46
+ * conservative one.
47
+ */
48
+ const FREE_ROWS: readonly FreeRow[] = [
49
+ { id: "cohere/north-mini-code:free", ctx: 256_000, out: 64_000 },
50
+ { id: "dots-studio/dots-3-note-preview:free", ctx: 512_000, out: 460_800, vision: true },
51
+ { id: "google/gemma-4-26b-a4b-it:free", ctx: 262_144, out: 32_768, vision: true },
52
+ { id: "google/gemma-4-31b-it:free", ctx: 262_144, out: 32_768, vision: true },
53
+ { id: "liquid/lfm-2.5-2.6b:free", ctx: 65_536, out: 8_192 },
54
+ { id: "minimax/minimax-m3.1-flash:free", ctx: 1_000_000, out: 524_288, vision: true },
55
+ {
56
+ id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
57
+ ctx: 256_000,
58
+ out: 65_536,
59
+ vision: true,
60
+ },
61
+ { id: "nvidia/nemotron-3-super-120b-a12b:free", ctx: 262_144, out: 235_929 },
62
+ { id: "nvidia/nemotron-3-ultra-550b-a55b:free", ctx: 1_000_000, out: 65_536 },
63
+ {
64
+ id: "nvidia/nemotron-3.5-content-safety:free",
65
+ ctx: 128_000,
66
+ out: 8_192,
67
+ vision: true,
68
+ toolCall: false,
69
+ },
70
+ { id: "nvidia/nemotron-3.5-lightning:free", ctx: 1_000_000, out: 65_536 },
71
+ { id: "nvidia/nemotron-3.5-super-vl:free", ctx: 1_000_000, out: 131_072, vision: true },
72
+ { id: "poolside/laguna-s-2.1:free", ctx: 262_144, out: 32_768 },
73
+ { id: "poolside/laguna-xs-2.1:free", ctx: 262_144, out: 32_768 },
74
+ { id: "thinkingmachines/inkling-small:free", ctx: 1_048_576, out: 262_144, vision: true },
75
+ { id: "thinkingmachines/inkling:free", ctx: 1_048_576, out: 262_144, vision: true },
76
+ { id: "xingchen-agi/xing4.0-29b-a4b:free", ctx: 262_144, out: 235_929 },
77
+ ];
78
+
79
+ /** Builds this roster for one provider, since each spells its own endpoint. */
80
+ export function openrouterFreeRoster(options: FreeRosterOptions): readonly ModelDefinition[] {
81
+ return FREE_ROWS.map((row) =>
82
+ defineModel({
83
+ id: row.id,
84
+ providerId: options.providerId,
85
+ wireFamily: "chat",
86
+ endpoint: options.endpoint,
87
+ ctx: row.ctx,
88
+ out: row.out,
89
+ vision: row.vision ?? false,
90
+ reasoning: true,
91
+ toolCall: row.toolCall ?? true,
92
+ // `free` is the billing statement and `freeTier` the catalog one. The
93
+ // `:free` suffix makes both true, and the model list groups on the tier.
94
+ free: true,
95
+ freeTier: true,
96
+ }),
97
+ );
98
+ }
@@ -172,17 +172,41 @@ export function staticEndpointForWire(
172
172
  return catalog.get(providerId)?.find((def) => def.wireFamily === wireFamily)?.endpointPath;
173
173
  }
174
174
 
175
+ /**
176
+ * Compiled-pattern memo.
177
+ *
178
+ * `matchModelWireFamily` runs per model on every discovery pass and on every
179
+ * manual registration, and it used to compile each rule's `RegExp` inside that
180
+ * loop — so the same handful of patterns were recompiled once per model per
181
+ * pass. Keyed by the pattern source, which is exactly what `new RegExp(source)`
182
+ * derives from.
183
+ *
184
+ * A pattern that fails to compile is not memoized; the tolerance stays at read
185
+ * time because an invalid pattern is rejected at write time, and a stored one
186
+ * must not throw during a listing.
187
+ */
188
+ const compiledPatternCache = new Map<string, RegExp>();
189
+
190
+ function compilePattern(pattern: string): RegExp | undefined {
191
+ const cached = compiledPatternCache.get(pattern);
192
+ if (cached !== undefined) return cached;
193
+ try {
194
+ const compiled = new RegExp(pattern);
195
+ compiledPatternCache.set(pattern, compiled);
196
+ return compiled;
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ }
201
+
175
202
  function matchModelWireFamily(
176
203
  modelId: string,
177
204
  rules: ReadonlyArray<{ pattern: string; wire_family: WireFamily }> | undefined,
178
205
  ): WireFamily | undefined {
179
206
  if (!rules?.length) return undefined;
180
207
  for (const rule of rules) {
181
- try {
182
- if (new RegExp(rule.pattern).test(modelId)) return rule.wire_family as WireFamily;
183
- } catch {
184
- // Invalid pattern is rejected at write time; be tolerant at read time.
185
- }
208
+ const pattern = compilePattern(rule.pattern);
209
+ if (pattern !== undefined && pattern.test(modelId)) return rule.wire_family as WireFamily;
186
210
  }
187
211
  return undefined;
188
212
  }