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
@@ -884,10 +884,30 @@ export function isRetryableFailure(error: unknown): boolean {
884
884
  return classifyUpstreamFailure(error).retryable;
885
885
  }
886
886
 
887
+ /**
888
+ * Minimum wall-clock budget a follow-on attempt needs to be worth starting.
889
+ *
890
+ * A retry repeats the whole pre-dispatch phase — credential resolution,
891
+ * admission, pool slot, request preparation — so it needs at least this much
892
+ * headroom on top of whatever the upstream itself takes. Below this, the
893
+ * attempt is killed by the deadline before it can return anything usable, and
894
+ * starting it replaces the first attempt's real error with a second one's
895
+ * timeout.
896
+ *
897
+ * Owned here so the attempt loop, the empty-stream replay, and the Codex
898
+ * retry budget cannot drift apart; each previously declared its own copy of
899
+ * the constant.
900
+ */
901
+ export const MIN_RETRY_BUDGET_MS = 15_000;
902
+
887
903
  /**
888
904
  * Capped exponential backoff with full jitter for a zero-based attempt index.
889
905
  * The base and cap are operator-tunable (`CARTETHYIA_FALLBACK_RETRY_BASE_MS`,
890
906
  * `CARTETHYIA_FALLBACK_RETRY_CAP_MS`) and default to 100ms / 2000ms.
907
+ *
908
+ * Used for every retry delay, including the empty-stream replay: jitter keeps
909
+ * concurrent clients from resynchronizing, and the cap keeps a provider that
910
+ * returns 200-with-no-events from driving an unbounded wait.
891
911
  */
892
912
  export function fallbackRetryDelayMs(attemptIndex: number): number {
893
913
  const exp = Math.min(resolveFallbackRetryCapMs(), resolveFallbackRetryBaseMs() * 2 ** attemptIndex);
@@ -1,3 +1,4 @@
1
+ import { log } from "../../observability/logger";
1
2
  // API-key authentication guard for `/v1/*`: token resolution, scope check,
2
3
  // client-router denylist, and the model-abuse ban gate.
3
4
  import { Elysia } from "elysia";
@@ -39,7 +40,31 @@ export function createApiKeyAuthenticationMiddleware(deps: {
39
40
  const pathname = fastPathname(request.url);
40
41
  if (!pathname.startsWith("/v1/")) return;
41
42
  const token = requestToken(request.headers);
42
- const authorization = await resolveApiKeyAuthorization(deps.db, token);
43
+ // A database failure here is not "the key is bad". Left to propagate it
44
+ // surfaced as a 500 whose log line carried the whole statement and its
45
+ // bound parameters — including the key hash — once per request. Under an
46
+ // overloaded database that is a credential leak written at request rate.
47
+ //
48
+ // It is also the wrong answer: the caller cannot fix it by sending a
49
+ // different key, so it is a capacity signal, not an auth one.
50
+ const authorization = await resolveApiKeyAuthorization(deps.db, token).catch((error: unknown) => {
51
+ // Only the error's identity is logged, never its message: a drizzle
52
+ // query error carries the full statement and its bound parameters, and
53
+ // on this path one of those parameters is the key hash.
54
+ // A sanitized stand-in, not the original: a drizzle query error's
55
+ // message is the statement plus its bound parameters, and one of those
56
+ // parameters is the key hash.
57
+ const safe = new Error("[redacted: credential-store query failed]");
58
+ safe.name = error instanceof Error ? error.name : "Error";
59
+ log.error("[auth] key lookup failed; treating as unavailable", safe);
60
+ throw new GatewayError(
61
+ "accounts_unavailable",
62
+ 503,
63
+ "The gateway could not verify this API key because its credential store is unavailable. Retry shortly.",
64
+ {},
65
+ "cartethyia",
66
+ );
67
+ });
43
68
  if (!authorization)
44
69
  throw new GatewayError("invalid_request", 401, "This API key is unknown, revoked, or expired. Issue a new key and send it as: Authorization: Bearer <key>.");
45
70
  const decision = createAccessDecision({
@@ -17,10 +17,11 @@ export function isProxyRequest(request: Request): boolean {
17
17
  * This list must mirror the routes `app.ts` actually mounts. It drives
18
18
  * body-read policy — a JSON route rejects a missing or non-JSON content-type
19
19
  * with 415, while an unrouted path is not read at all — so an entry with no
20
- * handler behind it describes a route the gateway does not serve. Two such
21
- * entries (`/v1/embeddings`, `/v1/audio/speech`) sit outside this list: they
22
- * advertise a surface that 404s, and the route-table test pins them absent so
23
- * re-adding one is a deliberate act.
20
+ * handler behind it describes a route the gateway does not serve. Three such
21
+ * entries (`/v1/embeddings`, `/v1/audio/transcriptions`, `/v1/audio/speech`)
22
+ * were listed for a while with nothing mounted — they advertised a surface
23
+ * that 404d — and the route-table test pinned them absent so re-adding one was
24
+ * a deliberate act. They are mounted now, as passthrough surfaces.
24
25
  *
25
26
  * The native service routes (`NATIVE_SERVICE_PATHS` — System One, web search,
26
27
  * image and video generation) are included: they read a JSON body through the
@@ -1,7 +1,9 @@
1
1
  import { GatewayError } from "../../protocol/gateway-error";
2
2
  import type { CanonicalRequest, ServiceKind } from "../../protocol/canonical-model";
3
+ import { toolResultParts } from "../../protocol/canonical-model";
3
4
  import type { ApiKeyAdmissionService } from "../../security/admission/service";
4
5
  import type { RouteCandidate as RouteCandidate, InMemoryRouteSnapshotService, RoutePlan } from "../routing/route-model";
6
+ import type { RouteContinuationAffinity } from "../routing/route-model";
5
7
  import { resolveAliasTarget, type RoutingEngine } from "../routing/router";
6
8
  import { projectForRoute, routeCapabilitiesFor } from "../translation/capabilities";
7
9
  import type { RequiredCapability } from "../translation/capabilities";
@@ -14,6 +16,7 @@ import { parseThinkingSuffix, withThinkingSuffixIntent } from "../translation/th
14
16
  import { nativeServicePathFor } from "../contracts";
15
17
  import { log } from "../../observability/logger";
16
18
  import { BUDDY_PROVIDER_IDS } from "../../providers/provider-metadata";
19
+ import { cursorRunSessions } from "../../providers/integrations/cursor/cursor-session-manager";
17
20
  import {
18
21
  degradedRequestVariants,
19
22
  degradeEncryptedReasoning,
@@ -32,6 +35,90 @@ function requestNeedsToolRepair(request: CanonicalRequest): boolean {
32
35
  return false;
33
36
  }
34
37
 
38
+ /** Prefix of a provider-continuation tool-call id the gateway itself minted. */
39
+ const CURSOR_CALL_PREFIX = "call_cursor_";
40
+
41
+ /**
42
+ * The provider-continuation tool results this request carries.
43
+ *
44
+ * Only ids the gateway minted (`call_cursor_<run-id>_<exec-id>`) are considered.
45
+ * Any other id belongs to a conversation the client owns; it is not a
46
+ * continuation and must not influence routing.
47
+ */
48
+ export function continuationCallIds(request: CanonicalRequest): readonly string[] {
49
+ const ids: string[] = [];
50
+ for (const message of request.messages) {
51
+ if (message.role !== "tool" && message.role !== "user") continue;
52
+ for (const part of toolResultParts(message)) {
53
+ if (!part.call_id.startsWith(CURSOR_CALL_PREFIX)) continue;
54
+ if (!ids.includes(part.call_id)) ids.push(part.call_id);
55
+ }
56
+ }
57
+ return ids;
58
+ }
59
+
60
+ /**
61
+ * Resolves a request's pending tool results to the one account that owns them.
62
+ *
63
+ * Every pending id must resolve to the *same* live Run and the same account.
64
+ * Ids resolving to different Runs, or to no live Run at all, are refused rather
65
+ * than partially applied: one result cannot be delivered to another account's
66
+ * upstream turn, and a request that cannot be satisfied as a whole must not be
67
+ * dispatched at all.
68
+ *
69
+ * Returns `undefined` when the request carries no continuation ids — the
70
+ * ordinary routing path.
71
+ */
72
+ export function resolveContinuationAffinity(
73
+ request: CanonicalRequest,
74
+ authorization: ResolvedApiKey,
75
+ ): RouteContinuationAffinity | undefined {
76
+ const ids = continuationCallIds(request);
77
+ if (ids.length === 0) return undefined;
78
+ const scope = {
79
+ tenantId: authorization.tenantId,
80
+ apiKeyId: authorization.id,
81
+ };
82
+ let affinity: RouteContinuationAffinity | undefined;
83
+ for (const callId of ids) {
84
+ // The registry resolves by tenant + API key + provider + account, so a call
85
+ // id replayed from another key or tenant resolves nothing.
86
+ const record = lookupPendingCall(callId, scope);
87
+ if (record === undefined) {
88
+ throw new GatewayError(
89
+ "invalid_request",
90
+ 400,
91
+ "This tool result refers to a Cursor tool call that is no longer pending. Resend the complete conversation.",
92
+ { callId },
93
+ );
94
+ }
95
+ if (affinity !== undefined && affinity.providerAccountId !== record.providerAccountId) {
96
+ throw new GatewayError(
97
+ "invalid_request",
98
+ 400,
99
+ "These tool results belong to different Cursor accounts; resend them one account at a time.",
100
+ { callIds: [...ids] },
101
+ );
102
+ }
103
+ affinity = {
104
+ providerId: record.providerId,
105
+ providerAccountId: record.providerAccountId,
106
+ };
107
+ }
108
+ return affinity;
109
+ }
110
+
111
+ /** Looks up one pending call across every provider that records continuations. */
112
+ function lookupPendingCall(
113
+ callId: string,
114
+ scope: { readonly tenantId: string; readonly apiKeyId: string },
115
+ ): { readonly providerId: string; readonly providerAccountId: string } | undefined {
116
+ const record = cursorRunSessions.describePendingCall(callId, scope);
117
+ return record === undefined
118
+ ? undefined
119
+ : { providerId: record.providerId, providerAccountId: record.providerAccountId };
120
+ }
121
+
35
122
  export interface PreparedProxyRequest {
36
123
  readonly canonicalRequest: CanonicalRequest;
37
124
  readonly authorization: ResolvedApiKey;
@@ -42,6 +129,13 @@ export interface PreparedProxyRequest {
42
129
  readonly degradedCapabilities?: readonly RequiredCapability[];
43
130
  /** Snapshot-consistent plan used to reserve each individual attempt. */
44
131
  readonly plan: RoutePlan;
132
+ /**
133
+ * The account this request must reach because a provider session is already
134
+ * live on it. Set only when the request carried a live tool result; the
135
+ * dispatch path uses it to borrow the held session instead of acquiring a
136
+ * second account and pool slot.
137
+ */
138
+ readonly continuationAffinity?: RouteContinuationAffinity;
45
139
  readonly estimatedInputTokens: number;
46
140
  readonly estimatedOutputTokens: number;
47
141
  readonly deadlineMs: number;
@@ -150,6 +244,13 @@ export class ProxyRequestPreparer {
150
244
  // requires the `image` capability — it plans like the text request it has
151
245
  // become, instead of being routed to a vision route for nothing.
152
246
  const requestWithAttachments = dropCorruptAttachments(request).request;
247
+ // Continuation affinity is resolved before anything else: a request
248
+ // carrying a tool result for a live provider session must be planned against
249
+ // the one account that owns that session, before capability derivation or
250
+ // degradation could narrow the candidate set past it. This is also the
251
+ // point where a mismatched or expired call id becomes a 400 the caller can
252
+ // act on, instead of a dispatch that silently starts a new upstream turn.
253
+ const continuationAffinity = resolveContinuationAffinity(request, authorization);
153
254
  // Capability-aware routing: derive requirements BEFORE planning so the
154
255
  // router filters candidates against snapshot profiles. When nothing
155
256
  // supports the full request, degrade (same greedy order) and re-plan
@@ -176,6 +277,7 @@ export class ProxyRequestPreparer {
176
277
  variant.required,
177
278
  allowCliMappings,
178
279
  authorization.cliMappingOwnerId ?? authorization.id,
280
+ continuationAffinity,
179
281
  );
180
282
  } catch (error) {
181
283
  if (error instanceof GatewayError && error.code === "capability_unsupported") continue;
@@ -303,6 +405,7 @@ export class ProxyRequestPreparer {
303
405
  eligibleRouteCandidates: eligible,
304
406
  degradedCapabilities: degraded,
305
407
  plan,
408
+ ...(continuationAffinity === undefined ? {} : { continuationAffinity }),
306
409
  estimatedInputTokens,
307
410
  estimatedOutputTokens,
308
411
  deadlineMs: input.deadlineMs,
@@ -4,6 +4,7 @@ import type { ClientIdentity } from "../../security/abuse";
4
4
  import type { ResolvedApiKey } from "../../security/api-key-auth";
5
5
  import type { PreparedProxyRequest } from "./preparer";
6
6
  import type { PayloadCaptureState, TelemetryPayloadCapture } from "../../observability/payload-capture";
7
+ import type { TelemetryPayloadDepth, TelemetryPayloadMode } from "../../console/settings/contracts";
7
8
  import { createInFlightRegistry, type InFlightSnapshot } from "./inflight";
8
9
  import { log } from "../../observability/logger";
9
10
  import { fastPathname } from "../pathname";
@@ -103,6 +104,19 @@ export interface ProxyRequestState {
103
104
  * hint at it, and they look identical in both cases.
104
105
  */
105
106
  payloadCaptureState?: PayloadCaptureState;
107
+ /**
108
+ * This request's resolved payload-capture preference, memoized on the request.
109
+ *
110
+ * The preference is a tenant setting, so it cannot change between attempts of
111
+ * one request. It was resolved twice — once per attempt to decide whether to
112
+ * tee the provider body, and again on the terminal attempt to decide what to
113
+ * store — so a request with several attempts paid for it several times.
114
+ * Memoized here, the second read is free and the two cannot disagree about
115
+ * whether capture is on.
116
+ */
117
+ payloadCapturePreference?:
118
+ | { readonly ok: true; readonly mode: TelemetryPayloadMode; readonly depth: TelemetryPayloadDepth }
119
+ | { readonly ok: false };
106
120
  /**
107
121
  * Set by the dispatch handler synchronously before returning a streaming
108
122
  * response. `afterResponse` lifecycle hooks read it to skip finalization
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Whether an account's reported plan is known to be a free one.
3
+ *
4
+ * CodeBuddy is the reason this exists. It refuses a set of models on a free
5
+ * plan with `11158 "model [X] is not allowed for user type [personal_free_user]"`,
6
+ * and the plan is the only thing that predicts it — the model directory
7
+ * publishes nothing usable. Routing therefore has to keep those models away
8
+ * from accounts that are known to be free, and the only per-account signal is
9
+ * the plan string the quota read reported: `"Free Plan Subscription"`.
10
+ *
11
+ * The match is deliberately narrow and the default is deliberately permissive.
12
+ * Only a plan that names itself free excludes an account. Anything else — a
13
+ * paid plan name, a provider that reports no plan, a plan string that changed
14
+ * shape — leaves the account in rotation, because the alternative is worse:
15
+ * guessing "free" for an unrecognised plan would take a working account out of
16
+ * rotation on the strength of a string we failed to parse. A wrong free match
17
+ * costs one refused request that the cooldown machinery already handles; a
18
+ * wrong paid match silently blackholes a model.
19
+ */
20
+ const FREE_PLAN_MARKERS: readonly string[] = ["free"];
21
+
22
+ export function isFreePlanAccount(plan: string | null | undefined): boolean {
23
+ if (plan === null || plan === undefined) return false;
24
+ const normalized = plan.trim().toLowerCase();
25
+ if (normalized.length === 0) return false;
26
+ return FREE_PLAN_MARKERS.some((marker) => normalized.includes(marker));
27
+ }
28
+
29
+ /**
30
+ * Whether this candidate can serve a request, given what is known about the
31
+ * model's plan gate and the account's plan.
32
+ *
33
+ * Returns `true` for every case except the one that is known to fail: a model
34
+ * the upstream refuses on a free plan, on an account the upstream reports as
35
+ * free. That pair is a certain refusal, so sending it wastes the request and
36
+ * advances the account's failure streak toward a six-hour park for something
37
+ * that is not the account's fault.
38
+ */
39
+ export function planAllowsCandidate(candidate: {
40
+ readonly pro_only?: boolean;
41
+ readonly account_plan?: string | null;
42
+ }): boolean {
43
+ if (candidate.pro_only !== true) return true;
44
+ return !isFreePlanAccount(candidate.account_plan);
45
+ }