cartethyia 2.4.6 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/README.md +45 -13
  2. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js +1 -0
  3. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-Bb9CNuoh.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-BXUaRCQQ.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
  8. package/dist/dashboard/assets/CombosPage-CisNZOUt.js +1 -0
  9. package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-BOpI_LOH.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-DAkdvJ4k.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-DeT-VXHd.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-CytxSeGP.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  25. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  26. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
  32. package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-8-eVadhc.js} +1 -1
  33. package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
  34. package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-uEIXPyGO.js} +1 -1
  35. package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +1 -0
  37. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-DqK5E26W.js} +4 -4
  39. package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  44. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  45. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  47. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
  49. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
  51. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
  53. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
  61. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  62. package/dist/dashboard/assets/entry--yj84zk3.js +4 -0
  63. package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
  65. package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
  66. package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
  67. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-D70pChfn.js → index-CcD1eSJ6.js} +2 -2
  71. package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
  73. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
  75. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
  77. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
  81. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  84. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  85. package/dist/dashboard/assets/providers/trae.webp +0 -0
  86. package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
  87. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  88. package/dist/dashboard/assets/select-XPJ-dIw3.js +41 -0
  89. package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
  90. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  91. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  92. package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
  93. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  94. package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
  95. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  96. package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
  97. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  98. package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
  99. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -52
  105. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  106. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  107. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  108. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  109. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  110. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  111. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  112. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  113. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  114. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  115. package/package.json +3 -2
  116. package/scripts/cli/command/console-menu.ts +30 -11
  117. package/scripts/cli/command/start.ts +77 -13
  118. package/scripts/cli/command/tray.ts +47 -8
  119. package/scripts/cli/command/update.ts +1 -1
  120. package/scripts/cli/gateway/instance-lock.ts +105 -0
  121. package/scripts/cli/gateway/spawn.ts +173 -101
  122. package/scripts/cli/ui/background.ts +110 -49
  123. package/src/boot/app.ts +20 -6
  124. package/src/boot/dependencies.ts +19 -0
  125. package/src/boot/elysia-typebox-bridge.ts +37 -0
  126. package/src/boot/lifecycle.ts +2 -0
  127. package/src/config/cooldowns.ts +138 -138
  128. package/src/config/providers.ts +5 -5
  129. package/src/config/runtime.ts +19 -3
  130. package/src/config/spec.ts +9 -0
  131. package/src/console/api-keys/contracts.ts +414 -391
  132. package/src/console/api-keys/routes.ts +880 -870
  133. package/src/console/auth/account-reset-store.ts +14 -0
  134. package/src/console/auth/account-reset.ts +3 -2
  135. package/src/console/auth/setup-service.ts +6 -3
  136. package/src/console/domain-registration.ts +9 -2
  137. package/src/console/observability/contracts.ts +475 -455
  138. package/src/console/observability/store.ts +1015 -952
  139. package/src/console/providers/catalog/contracts.ts +31 -0
  140. package/src/console/providers/catalog/provider-operations.ts +8 -0
  141. package/src/console/providers/catalog/store.ts +1532 -1525
  142. package/src/console/providers/oauth/callback-listener.ts +18 -3
  143. package/src/console/providers/oauth/routes.ts +38 -15
  144. package/src/console/quota/refresh.ts +19 -0
  145. package/src/console/updates/routes.ts +162 -4
  146. package/src/main.ts +3 -0
  147. package/src/network/http2-bidi.ts +85 -0
  148. package/src/network/outbound-fetch.ts +1 -26
  149. package/src/network/pool/agent.ts +478 -460
  150. package/src/network/pool/resolver.ts +1104 -544
  151. package/src/network/pool-health-machine.ts +12 -5
  152. package/src/observability/payload-capture.ts +344 -324
  153. package/src/persistence/db-pg.ts +9 -3
  154. package/src/persistence/db-sqlite.ts +497 -495
  155. package/src/persistence/health-event-flush.ts +137 -0
  156. package/src/persistence/migrate.ts +181 -129
  157. package/src/persistence/schema.ts +1360 -1295
  158. package/src/persistence/settings-revision.ts +25 -0
  159. package/src/persistence/shared-cache.ts +217 -0
  160. package/src/persistence/stores/share-store.ts +556 -528
  161. package/src/persistence/usage-flush.ts +61 -1
  162. package/src/protocol/canonical-model.ts +23 -1
  163. package/src/protocol/response/chat.ts +143 -10
  164. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  165. package/src/providers/default-registry.ts +173 -1
  166. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  167. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  168. package/src/providers/discovery/probing-service.ts +1022 -1001
  169. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
  170. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  171. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
  172. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  173. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  174. package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
  175. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  176. package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
  177. package/src/providers/integrations/claude/claude.ts +606 -582
  178. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  179. package/src/providers/integrations/cline/cline.ts +539 -468
  180. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  181. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  182. package/src/providers/integrations/commandcode/quota.ts +160 -0
  183. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  184. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  185. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  186. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  187. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  188. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  189. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  190. package/src/providers/integrations/cursor/cursor.ts +960 -0
  191. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  192. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  193. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  194. package/src/providers/integrations/github/github.ts +4 -4
  195. package/src/providers/integrations/hermes.ts +79 -8
  196. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  197. package/src/providers/integrations/kilo/kilo.ts +153 -146
  198. package/src/providers/integrations/opencode/provider.ts +532 -538
  199. package/src/providers/integrations/openrouter/provider.ts +139 -281
  200. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  201. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
  202. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  203. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  204. package/src/providers/integrations/qoder/qoder.ts +865 -730
  205. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  206. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  207. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  208. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  209. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  210. package/src/providers/integrations/trae/trae.ts +458 -0
  211. package/src/providers/operations/account-health-service.ts +107 -10
  212. package/src/providers/operations/client-versions.ts +133 -6
  213. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  214. package/src/providers/operations/provider-credential-service.ts +46 -7
  215. package/src/providers/provider-contracts.ts +76 -0
  216. package/src/providers/provider-metadata.ts +78 -6
  217. package/src/providers/provider-model-definition.ts +373 -349
  218. package/src/security/api-key-auth.ts +126 -8
  219. package/src/transport/contracts.ts +3 -0
  220. package/src/transport/dispatch/attempt-finalize.ts +32 -1
  221. package/src/transport/dispatch/attempt-loop.ts +530 -515
  222. package/src/transport/dispatch/generation.ts +88 -21
  223. package/src/transport/dispatch/leases.ts +99 -2
  224. package/src/transport/dispatch/proxy-request.ts +60 -3
  225. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  226. package/src/transport/dispatch/streaming/stream.ts +22 -2
  227. package/src/transport/dispatch/upstream.ts +19 -1
  228. package/src/transport/guards/auth-guard.ts +26 -1
  229. package/src/transport/guards/body-policy.ts +5 -4
  230. package/src/transport/request/preparer.ts +103 -0
  231. package/src/transport/request/state.ts +14 -0
  232. package/src/transport/routing/plan-gate.ts +45 -0
  233. package/src/transport/routing/route-catalog.ts +799 -777
  234. package/src/transport/routing/route-model.ts +52 -1
  235. package/src/transport/routing/router.ts +51 -0
  236. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
  237. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  238. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  239. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  240. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
  241. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  242. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  243. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  244. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  245. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  246. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  247. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
  248. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  249. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
  250. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  251. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  252. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  253. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
  254. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  255. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
  256. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  257. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
  258. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  259. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  260. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  261. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
  262. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  263. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  264. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
  265. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  266. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  267. package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
  268. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  269. package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
  270. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  271. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  272. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  273. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  274. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  275. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  276. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  277. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  278. package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
  279. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  280. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  281. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  282. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  283. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  284. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  285. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  286. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  287. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  288. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  289. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  290. package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
  291. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  292. package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
  293. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  294. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  295. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  296. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  297. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  298. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  299. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  300. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  301. package/src/network/tls/bun-client-hello.ts +0 -112
  302. /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
  303. /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
@@ -1,468 +1,539 @@
1
- import { createApiKeyAdapter } from "../../configured-provider";
2
- import type { ProviderDispatchTarget, ProviderAdapter, ProviderDispatchContext } from "../../provider-contracts";
3
- import type { ModelDefinition } from "../../provider-model-definition";
4
- import { unwrapProviderToken } from "../../provider-credential-envelope";
5
- import { GatewayError } from "../../../protocol/gateway-error";
6
- import type { CanonicalRequest } from "../../../protocol/canonical-model";
7
- import { completeRequiredSchema, isRecord } from "../../../protocol/codecs";
8
- import { providerBaseUrl } from "../../provider-metadata";
9
- import { getCachedModelDiscovery } from "../../operations/model-discovery-cache";
10
- import { defineModel } from "../../provider-model-definition";
11
- import { clinePlatform, clinePlatformVersion } from "../../operations/cli-platform";
12
- import {
13
- getClineClientVersion,
14
- getClineSdkVersion,
15
- } from "../../operations/client-versions";
16
- export const CLINE_BASE_URL = providerBaseUrl("cline");
17
- export const CLINE_PROVIDER_ID = "cline" as const;
18
- const CLINE_CHAT_PATH = "/chat/completions" as const;
19
- const CLINE_RECOMMENDED_MODELS_PATH = "/ai/cline/recommended-models" as const;
20
- /**
21
- * Cline's own model catalog. Unlike the roster above it states `context_length`
22
- * and `top_provider.max_completion_tokens`, so it is the authority for limits.
23
- */
24
- const CLINE_MODELS_CATALOG_PATH = "/ai/cline/models" as const;
25
-
26
-
27
-
28
- export function workosToken(token: string): string {
29
- return token.startsWith("workos:") ? token : `workos:${token}`;
30
- }
31
-
32
- // Cartethyia expresses this as a per-dispatch buildExtraHeaders hook so every
33
- // request gets fresh client-version headers.
34
- // API-key credentials use direct Bearer format (no workos prefix);
35
- // OAuth and scoped credentials retain the required workos: prefix.
36
-
37
- async function clineExtraHeaders(context: ProviderDispatchContext): Promise<Record<string, string>> {
38
- let authOverride: Record<string, string> = {};
39
- const secret = context.credential.secret;
40
- if (secret && secret.length > 0) {
41
- const raw = new TextDecoder().decode(secret).trim();
42
- if (raw.length > 0) {
43
- // Bearer-strip delegates to the single token-envelope implementation.
44
- const normalized = new TextDecoder().decode(unwrapProviderToken(raw, "bearer"));
45
- const kind = context.credential.credential_kind;
46
- const token = kind === "api_key" ? normalized : workosToken(normalized);
47
- authOverride = { authorization: `Bearer ${token}` };
48
- }
49
- }
50
- const clientVersion = getClineClientVersion();
51
- const sdkVersion = getClineSdkVersion();
52
-
53
- return {
54
- ...authOverride,
55
- "user-agent": `Cline/${clientVersion}`,
56
- // Referer travels in both spellings; some upstream checks are case-sensitive.
57
- "http-referer": "https://cline.bot",
58
- "HTTP-Referer": "https://cline.bot",
59
- "x-platform": clinePlatform(),
60
- "x-platform-version": clinePlatformVersion(),
61
- "x-title": "Cline",
62
- "x-client-type": "cline-sdk",
63
- "x-client-version": clientVersion,
64
- "x-core-version": sdkVersion,
65
- "X-Title": "Cline",
66
- "x-is-multi-root": "false",
67
- accept: "text/event-stream, application/json",
68
- };
69
- }
70
-
71
- // System-prompt injection and materialized-stream handling.
72
- // Cline requires a system/developer message for compatibility; its upstream
73
- // free models also reject empty system content, so the fallback is concise.
74
-
75
- function completeClineToolSchemas(payload: Record<string, unknown>): void {
76
- if (!Array.isArray(payload.tools)) return;
77
- payload.tools = payload.tools.map((tool) => {
78
- if (!isRecord(tool)) return tool;
79
- const functionValue = tool.function;
80
- if (isRecord(functionValue) && "parameters" in functionValue) {
81
- return {
82
- ...tool,
83
- function: { ...functionValue, parameters: completeRequiredSchema(functionValue.parameters) },
84
- };
85
- }
86
- if ("parameters" in tool) return { ...tool, parameters: completeRequiredSchema(tool.parameters) };
87
- return tool;
88
- });
89
- }
90
-
91
- function clinePrePayload(
92
- payload: Record<string, unknown>,
93
- request: CanonicalRequest,
94
- candidate: ProviderDispatchTarget,
95
- ): void {
96
- completeClineToolSchemas(payload);
97
- // 1) Ensure system/developer messages always carry non-empty content.
98
- const rawMessages = payload["messages"];
99
- const messages: Record<string, unknown>[] = Array.isArray(rawMessages)
100
- ? rawMessages.filter(isRecord).map((message) => {
101
- const role = message["role"];
102
- const content = message["content"];
103
- const emptyContent =
104
- content === undefined ||
105
- content === null ||
106
- (typeof content === "string" && content.trim().length === 0) ||
107
- (Array.isArray(content) && content.length === 0);
108
- return (role === "system" || role === "developer") && emptyContent
109
- ? { ...message, content: "You are a helpful assistant." }
110
- : message;
111
- })
112
- : [];
113
- const hasSystem = messages.some(
114
- (message) => message["role"] === "system" || message["role"] === "developer",
115
- );
116
- payload["messages"] = hasSystem
117
- ? messages
118
- : [{ role: "system", content: "You are a helpful assistant." }, ...messages];
119
-
120
- // 2) Materialize deepseek-v4-flash non-stream quirk
121
- const model = typeof payload["model"] === "string" ? (payload["model"] as string) : candidate.model_id;
122
- const needsMaterialize = !request.stream && model.includes("deepseek-v4-flash");
123
- if (needsMaterialize) {
124
- payload["stream"] = true;
125
- // Set reasoning_effort to none for this model to suppress reasoning streaming
126
- payload["reasoning_effort"] = "none";
127
- const cur = typeof payload["max_tokens"] === "number" ? (payload["max_tokens"] as number) : 0;
128
- payload["max_tokens"] = Math.max(cur, 256);
129
- // keep GatewayError reachable
130
- if (!model) throw new GatewayError("invalid_request", 400, "Cline model is required");
131
- }
132
- }
133
-
134
- // (modelEntries / recommendedModels / fetchRecommendedModels) preserved
135
- // as Cartethyia discovery helpers. The factory's discovery endpoint is wired
136
- // to /ai/cline/recommended-models; these helpers can be used by a custom
137
- // discovery layer or tests. Keeping them guarantees the custom tag→vision
138
- // logic and clinePass vs free branching are not lost in the port.
139
-
140
- interface ClineRecommendedModel {
141
- readonly id?: unknown;
142
- readonly name?: unknown;
143
- readonly description?: unknown;
144
- readonly tags?: unknown;
145
- }
146
-
147
- interface ClineRecommendedPayload {
148
- readonly recommended?: unknown;
149
- readonly free?: unknown;
150
- readonly clinePass?: unknown;
151
- readonly clineCloud?: unknown;
152
- }
153
-
154
- function modelEntries(value: unknown): readonly ClineRecommendedModel[] {
155
- return Array.isArray(value)
156
- ? value.filter((entry): entry is ClineRecommendedModel => typeof entry === "object" && entry !== null && !Array.isArray(entry))
157
- : [];
158
- }
159
-
160
- function recommendedModels(
161
- value: unknown,
162
- pass: boolean,
163
- upstream: ReadonlyMap<string, ClineUpstreamLimits> = new Map(),
164
- ): readonly ModelDefinition[] {
165
- if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
166
- const payload = value as ClineRecommendedPayload;
167
- // The endpoint publishes four buckets: `recommended` (the current picks),
168
- // `free` (the zero-cost tier), `clinePass` (the subscription roster) and
169
- // `clineCloud` (cloud-routed). A pass-enabled account may use every one of
170
- // them, so all four are read; a free account sees the free tier only. Reading
171
- // just `free`/`clinePass` silently dropped `recommended` and `clineCloud`
172
- // from the catalog entirely.
173
- //
174
- // The third element marks the zero-cost tier — the `free` bucket alone, not
175
- // `recommended`: a pass account receives `recommended` too, so calling it a
176
- // free-tier row would mislabel a subscription catalog entry. The tier is what
177
- // separates a "Free models (auto)" row from an ordinary fetched one.
178
- //
179
- // Membership of the bucket *is* the rule, deliberately not the id prefix. The
180
- // `free` bucket's ids are mostly `cline-free/…`, but the prefix does not
181
- // define the tier in either direction: a free id need not carry it (the
182
- // catalog serves `deepseek/deepseek-v4-flash` and `z-ai/glm-5.3-flash` on the
183
- // free tier with no prefix at all), and carrying it proves nothing on its own.
184
- // A prefix test would therefore both miss served models and admit retired
185
- // ones, so the bucket the endpoint already sorted them into is the authority.
186
- const buckets: ReadonlyArray<readonly [unknown, boolean, boolean]> = pass
187
- ? [
188
- [payload.recommended, false, false],
189
- [payload.free, false, true],
190
- [payload.clinePass, true, false],
191
- [payload.clineCloud, true, false],
192
- ]
193
- : [
194
- [payload.recommended, false, false],
195
- [payload.free, false, true],
196
- ];
197
- const seen = new Set<string>();
198
- const out: ModelDefinition[] = [];
199
- for (const [bucket, subscription, freeTier] of buckets) {
200
- for (const entry of modelEntries(bucket)) {
201
- const rawId = typeof entry.id === "string" ? entry.id.trim() : "";
202
- if (rawId.length === 0) continue;
203
- // The endpoint already namespaces its own ids (`cline-pass/…`,
204
- // `cline-cloud/…`, `cline-free/…`), so the bucket decides only how the
205
- // row is priced and which catalog entry answers for it — never how the id
206
- // is spelled. Prefixing here double-namespaced every id the endpoint had
207
- // already prefixed (`cline-pass/cline-pass/mimo-v2.6-flash`), which no
208
- // upstream route resolves.
209
- const id = rawId;
210
- if (seen.has(id)) continue;
211
- seen.add(id);
212
- // Limits, in authority order:
213
- // 1. Cline's own `/ai/cline/models` catalog (`context_length`,
214
- // `top_provider.max_completion_tokens`) — the serving provider's
215
- // answer, and the only source that covers the whole roster.
216
- // 2. `defineModel`'s fallback, which is the base catalog's exact row
217
- // when models.dev files the provider (`cline-pass`), else the
218
- // most-agreed row for the bare id, else the documented default.
219
- // The recommended-models endpoint itself states no limits (only
220
- // id/name/description/tags), so a fixed pair here was simply invented:
221
- // the pass entries are 1M-context, and an output cap above the real
222
- // window is unsatisfiable.
223
- const providerId = subscription ? "cline-pass" : "cline";
224
- const limits = upstreamLimitsFor(upstream, id);
225
- const declaredVision = tagsOf(entry).some(
226
- (tag) => tag.includes("vision") || tag.includes("multimodal"),
227
- );
228
- out.push(
229
- defineModel({
230
- id,
231
- providerId,
232
- wireFamily: "chat",
233
- endpoint: CLINE_CHAT_PATH,
234
- // The upstream catalog's `input_modalities` is authoritative when it
235
- // lists the id; the roster's own tags are the fallback signal.
236
- vision: limits?.vision ?? declaredVision,
237
- ...(limits?.contextLimit === undefined ? {} : { ctx: limits.contextLimit }),
238
- ...(limits?.outputLimit === undefined ? {} : { out: limits.outputLimit }),
239
- reasoning: true,
240
- // Two separate statements, so neither has to bend for the other: the
241
- // free tier and the pass roster both bill zero per token, while only
242
- // the free bucket is the tier the model list groups as free.
243
- free: !subscription,
244
- freeTier,
245
- }),
246
- );
247
- }
248
- }
249
- return out;
250
- }
251
-
252
- /**
253
- * Upstream limits for one model id, read from Cline's own catalog.
254
- *
255
- * The recommended-models roster states no limits at all, but Cline's
256
- * `/ai/cline/models` catalog does: `context_length` plus
257
- * `top_provider.max_completion_tokens`, and `architecture.input_modalities`.
258
- * That is the serving provider's own answer, so it outranks any guess.
259
- *
260
- * The two endpoints do not share an id spelling. The roster names a
261
- * subscription model `cline-pass/kimi-k3` while the catalog files it as
262
- * `moonshotai/kimi-k3`, so a lookup tries the id as given and then its bare
263
- * segment — the same bare-id rule `modelsDevCatalog` uses, and safe here
264
- * because this is one provider's catalog rather than a shared global index.
265
- */
266
- interface ClineUpstreamLimits {
267
- readonly contextLimit?: number;
268
- readonly outputLimit?: number;
269
- readonly vision?: boolean;
270
- }
271
-
272
- function upstreamCatalogIndex(value: unknown): ReadonlyMap<string, ClineUpstreamLimits> {
273
- const root = isRecord(value) ? value : undefined;
274
- const rows = Array.isArray(root?.["data"]) ? root["data"] : Array.isArray(value) ? value : [];
275
- const byId = new Map<string, ClineUpstreamLimits>();
276
- for (const row of rows) {
277
- if (!isRecord(row)) continue;
278
- const id = typeof row["id"] === "string" ? row["id"].trim().toLowerCase() : "";
279
- if (id.length === 0) continue;
280
- const contextLimit = positiveCount(row["context_length"]);
281
- const topProvider = isRecord(row["top_provider"]) ? row["top_provider"] : undefined;
282
- const outputLimit = positiveCount(topProvider?.["max_completion_tokens"]);
283
- const architecture = isRecord(row["architecture"]) ? row["architecture"] : undefined;
284
- const modalities = Array.isArray(architecture?.["input_modalities"])
285
- ? architecture["input_modalities"]
286
- : [];
287
- const limits: ClineUpstreamLimits = {
288
- ...(contextLimit === undefined ? {} : { contextLimit }),
289
- ...(outputLimit === undefined ? {} : { outputLimit }),
290
- vision: modalities.some((m) => m === "image"),
291
- };
292
- byId.set(id, limits);
293
- // The roster and this catalog spell the same model differently: the roster
294
- // names a subscription model `cline-pass/kimi-k3` while this catalog files
295
- // it as `moonshotai/kimi-k3`. Indexing the bare segment too is what lets
296
- // `upstreamLimitsFor` resolve one to the other. An exact key already
297
- // recorded stays authoritative, so a provider-prefixed row never shadows a
298
- // row filed under the bare id itself.
299
- const slash = id.indexOf("/");
300
- if (slash >= 0) {
301
- const bare = id.slice(slash + 1);
302
- if (!byId.has(bare)) byId.set(bare, limits);
303
- }
304
- }
305
- return byId;
306
- }
307
-
308
- function positiveCount(value: unknown): number | undefined {
309
- const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
310
- return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
311
- }
312
-
313
- /** The catalog row for `id`, falling back to its bare segment. */
314
- function upstreamLimitsFor(
315
- index: ReadonlyMap<string, ClineUpstreamLimits>,
316
- id: string,
317
- ): ClineUpstreamLimits | undefined {
318
- const normalized = id.trim().toLowerCase();
319
- const exact = index.get(normalized);
320
- if (exact !== undefined) return exact;
321
- const slash = normalized.indexOf("/");
322
- return slash < 0 ? undefined : index.get(normalized.slice(slash + 1));
323
- }
324
-
325
- function tagsOf(entry: ClineRecommendedModel): readonly string[] {
326
- return Array.isArray(entry.tags)
327
- ? entry.tags
328
- .filter((tag): tag is string => typeof tag === "string")
329
- .map((tag) => tag.toLowerCase())
330
- : [];
331
- }
332
-
333
- export async function fetchClineRecommendedModels(
334
- signal: AbortSignal | undefined,
335
- pass: boolean,
336
- fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = globalThis.fetch,
337
- ): Promise<readonly ModelDefinition[] | null> {
338
- const load = async (): Promise<readonly ModelDefinition[] | null> => {
339
- const timeout = AbortSignal.timeout(10_000);
340
- const composed = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
341
- try {
342
- const url = `${CLINE_BASE_URL}${CLINE_RECOMMENDED_MODELS_PATH}`;
343
- const res = await fetcher(url, { headers: { accept: "application/json" }, signal: composed });
344
- if (!res.ok) return null;
345
- const json = (await res.json()) as unknown;
346
- // The limits live in the sibling catalog, not in the roster. Fetch it
347
- // alongside, and treat its absence as "no upstream limits" rather than a
348
- // failure: the roster is still useful without them, and `defineModel`
349
- // falls back to the base catalog.
350
- const upstream = await fetchClineUpstreamCatalog(fetcher, composed);
351
- const models = recommendedModels(json, pass, upstream);
352
- return models.length > 0 ? models : null;
353
- } catch {
354
- return null;
355
- }
356
- };
357
- // The default fetcher is the production path: this roster is public and
358
- // changes slowly, so a short TTL avoids redundant refreshes. A caller that
359
- // injects its own fetcher (tests, custom egress) always gets a live call.
360
- if (fetcher !== globalThis.fetch) return load();
361
- return getCachedModelDiscovery(`cline-recommended:${pass ? "pass" : "free"}`, load);
362
- }
363
-
364
- /**
365
- * Whether the credential belongs to a Cline Pass account.
366
- *
367
- * Reads the same `/users/me/plan` endpoint the quota collector uses. A pass
368
- * account is the only one allowed to see the `clinePass`/`clineCloud` roster;
369
- * any failure answers `false`, so a free account never has subscription rows
370
- * written into its catalog.
371
- */
372
- export async function clineAccountHasPass(
373
- credential: string,
374
- fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = globalThis.fetch,
375
- ): Promise<boolean> {
376
- try {
377
- const token = credential.startsWith("workos:") ? credential : `workos:${credential}`;
378
- const res = await fetcher("https://api.cline.bot/api/v1/users/me/plan", {
379
- headers: { accept: "application/json", authorization: `Bearer ${token}` },
380
- signal: AbortSignal.timeout(10_000),
381
- });
382
- if (!res.ok) return false;
383
- const body = (await res.json()) as { plan?: { displayName?: unknown } };
384
- const name = typeof body.plan?.displayName === "string" ? body.plan.displayName.toLowerCase() : "";
385
- return name.includes("pass");
386
- } catch {
387
- return false;
388
- }
389
- }
390
-
391
- /** Cline's model catalog, indexed for limit lookup. Empty when unavailable. */
392
- async function fetchClineUpstreamCatalog(
393
- fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
394
- signal: AbortSignal,
395
- ): Promise<ReadonlyMap<string, ClineUpstreamLimits>> {
396
- try {
397
- const res = await fetcher(`${CLINE_BASE_URL}${CLINE_MODELS_CATALOG_PATH}`, {
398
- headers: { accept: "application/json" },
399
- signal,
400
- });
401
- if (!res.ok) return new Map();
402
- return upstreamCatalogIndex(await res.json());
403
- } catch {
404
- return new Map();
405
- }
406
- }
407
-
408
- /**
409
- * Static seed for Cline's free tier.
410
- *
411
- * Every id here is materialized into the `models` table at boot with
412
- * `source: "builtin"`, and `seedBundledModels` deletes any builtin row this
413
- * list no longer declares. That makes the list the *owner* of these rows: an id
414
- * Cline retires from its roster keeps its catalog card — and its route — until
415
- * it is removed here, because nothing else prunes a builtin row. The seed is
416
- * therefore kept to ids Cline currently serves on the free tier; a stale entry
417
- * is a card that can only ever fail a probe.
418
- *
419
- * Cline's live roster (`/ai/cline/recommended-models`, read by
420
- * `fetchClineRecommendedModels`) is the authority for what is routable right
421
- * now, and "Fetch models" reconciles against it. This list is the offline seed
422
- * that makes a fresh install usable before the first fetch.
423
- */
424
- export const CLINE_MODELS: readonly ModelDefinition[] = [
425
- // `deepseek/deepseek-v4-flash` is servable on the free tier even though the
426
- // roster's `free` bucket does not list it — the bucket is not the whole tier.
427
- // It is routed and answers for a free account, so it belongs here.
428
- defineModel({
429
- id: "deepseek/deepseek-v4-flash",
430
- ctx: 1_048_576,
431
- out: 384_000,
432
- reasoning: true,
433
- vision: true,
434
- free: true,
435
- }),
436
- // The `free` bucket's published ids are the tier; the upstream id prefixes
437
- // are retained verbatim because they are part of Cline's model contract.
438
- defineModel({
439
- id: "cline-free/mimo-v2.6-flash",
440
- ctx: 1_048_576,
441
- out: 131_072,
442
- reasoning: true,
443
- vision: true,
444
- free: true,
445
- }),
446
- defineModel({
447
- id: "cline-free/muse-spark-1.3-contributor",
448
- ctx: 1_048_576,
449
- out: 943_718,
450
- reasoning: true,
451
- vision: true,
452
- free: true,
453
- }),
454
- ];
455
-
456
-
457
- export function createClineAdapter(fetchImpl?: typeof fetch): ProviderAdapter {
458
- return createApiKeyAdapter(
459
- {
460
- provider_id: CLINE_PROVIDER_ID,
461
- base_url: CLINE_BASE_URL,
462
- endpoint_paths_by_wire_family: { chat: CLINE_CHAT_PATH, responses: "/responses" },
463
- buildExtraHeaders: (ctx) => clineExtraHeaders(ctx),
464
- prePayload: clinePrePayload,
465
- },
466
- fetchImpl,
467
- );
468
- }
1
+ import { createApiKeyAdapter } from "../../configured-provider";
2
+ import type { ProviderDispatchTarget, ProviderAdapter, ProviderDispatchContext } from "../../provider-contracts";
3
+ import type { ModelDefinition } from "../../provider-model-definition";
4
+ import { unwrapProviderToken } from "../../provider-credential-envelope";
5
+ import { GatewayError } from "../../../protocol/gateway-error";
6
+ import type { CanonicalRequest } from "../../../protocol/canonical-model";
7
+ import { completeRequiredSchema, isRecord } from "../../../protocol/codecs";
8
+ import { providerBaseUrl } from "../../provider-metadata";
9
+ import { getCachedModelDiscovery } from "../../operations/model-discovery-cache";
10
+ import { defineModel } from "../../provider-model-definition";
11
+ import { openrouterFreeRoster } from "../../discovery/openrouter-free-roster";
12
+ import { clinePlatform, clinePlatformVersion } from "../../operations/cli-platform";
13
+ import {
14
+ getClineCliVersion,
15
+ getClineSdkVersion,
16
+ } from "../../operations/client-versions";
17
+ export const CLINE_BASE_URL = providerBaseUrl("cline");
18
+ export const CLINE_PROVIDER_ID = "cline" as const;
19
+ const CLINE_CHAT_PATH = "/chat/completions" as const;
20
+ const CLINE_RECOMMENDED_MODELS_PATH = "/ai/cline/recommended-models" as const;
21
+ /** The OpenAI-style listing; its `:free` suffix is a second free-tier roster. */
22
+ const CLINE_MODELS_LIST_PATH = "/models" as const;
23
+ /**
24
+ * Cline's own model catalog. Unlike the roster above it states `context_length`
25
+ * and `top_provider.max_completion_tokens`, so it is the authority for limits.
26
+ */
27
+ const CLINE_MODELS_CATALOG_PATH = "/ai/cline/models" as const;
28
+
29
+
30
+
31
+ export function workosToken(token: string): string {
32
+ return token.startsWith("workos:") ? token : `workos:${token}`;
33
+ }
34
+
35
+ // Cartethyia expresses this as a per-dispatch buildExtraHeaders hook so every
36
+ // request gets fresh client-version headers.
37
+ // API-key credentials use direct Bearer format (no workos prefix);
38
+ // OAuth and scoped credentials retain the required workos: prefix.
39
+
40
+ async function clineExtraHeaders(context: ProviderDispatchContext): Promise<Record<string, string>> {
41
+ let authOverride: Record<string, string> = {};
42
+ const secret = context.credential.secret;
43
+ if (secret && secret.length > 0) {
44
+ const raw = new TextDecoder().decode(secret).trim();
45
+ if (raw.length > 0) {
46
+ // Bearer-strip delegates to the single token-envelope implementation.
47
+ const normalized = new TextDecoder().decode(unwrapProviderToken(raw, "bearer"));
48
+ const kind = context.credential.credential_kind;
49
+ const token = kind === "api_key" ? normalized : workosToken(normalized);
50
+ authOverride = { authorization: `Bearer ${token}` };
51
+ }
52
+ }
53
+ // The chat surface claims the CLI identity, matching the billing surface in
54
+ // `cline-quota.ts` and the reference client: `cline` (npm, bin `cline`) is a
55
+ // separate release line from the VS Code extension, so the client version
56
+ // must come from the CLI package, not the extension manifest. `@cline/sdk`
57
+ // is a dependency of the CLI, so its version stays the core version.
58
+ const clientVersion = getClineCliVersion();
59
+ const sdkVersion = getClineSdkVersion();
60
+ // Cline's own client sends `X-Task-ID` on inference calls as the stable
61
+ // prompt-cache/session key, and omits it on account and discovery calls
62
+ // rather than inventing a per-request identity. `conversation_affinity` is
63
+ // that same value here: stable across the retries of one conversation, so a
64
+ // retried dispatch keeps its cache entry instead of starting a new one.
65
+ const taskId = context.conversation_affinity;
66
+
67
+ return {
68
+ ...authOverride,
69
+ "user-agent": `Cline/${clientVersion}`,
70
+ // One spelling only. Header names are case-insensitive, so sending both
71
+ // `http-referer` and `HTTP-Referer` does not hedge against a case-sensitive
72
+ // check — it makes `Headers` merge them into the single value
73
+ // `https://cline.bot, https://cline.bot`.
74
+ "HTTP-Referer": "https://cline.bot",
75
+ "x-platform": clinePlatform(),
76
+ "x-platform-version": clinePlatformVersion(),
77
+ "x-title": "Cline",
78
+ "x-client-type": "cline-cli",
79
+ "x-client-version": clientVersion,
80
+ "x-core-version": sdkVersion,
81
+ "X-Title": "Cline",
82
+ "x-is-multi-root": "false",
83
+ ...(taskId ? { "x-task-id": taskId } : {}),
84
+ accept: "text/event-stream, application/json",
85
+ };
86
+ }
87
+
88
+ // System-prompt injection and materialized-stream handling.
89
+ // Cline requires a system/developer message for compatibility; its upstream
90
+ // free models also reject empty system content, so the fallback is concise.
91
+
92
+ function completeClineToolSchemas(payload: Record<string, unknown>): void {
93
+ if (!Array.isArray(payload.tools)) return;
94
+ payload.tools = payload.tools.map((tool) => {
95
+ if (!isRecord(tool)) return tool;
96
+ const functionValue = tool.function;
97
+ if (isRecord(functionValue) && "parameters" in functionValue) {
98
+ return {
99
+ ...tool,
100
+ function: { ...functionValue, parameters: completeRequiredSchema(functionValue.parameters) },
101
+ };
102
+ }
103
+ if ("parameters" in tool) return { ...tool, parameters: completeRequiredSchema(tool.parameters) };
104
+ return tool;
105
+ });
106
+ }
107
+
108
+ function clinePrePayload(
109
+ payload: Record<string, unknown>,
110
+ request: CanonicalRequest,
111
+ candidate: ProviderDispatchTarget,
112
+ ): void {
113
+ completeClineToolSchemas(payload);
114
+ // 1) Ensure system/developer messages always carry non-empty content.
115
+ const rawMessages = payload["messages"];
116
+ const messages: Record<string, unknown>[] = Array.isArray(rawMessages)
117
+ ? rawMessages.filter(isRecord).map((message) => {
118
+ const role = message["role"];
119
+ const content = message["content"];
120
+ const emptyContent =
121
+ content === undefined ||
122
+ content === null ||
123
+ (typeof content === "string" && content.trim().length === 0) ||
124
+ (Array.isArray(content) && content.length === 0);
125
+ return (role === "system" || role === "developer") && emptyContent
126
+ ? { ...message, content: "You are a helpful assistant." }
127
+ : message;
128
+ })
129
+ : [];
130
+ const hasSystem = messages.some(
131
+ (message) => message["role"] === "system" || message["role"] === "developer",
132
+ );
133
+ payload["messages"] = hasSystem
134
+ ? messages
135
+ : [{ role: "system", content: "You are a helpful assistant." }, ...messages];
136
+
137
+ // 2) Materialize deepseek-v4-flash non-stream quirk
138
+ const model = typeof payload["model"] === "string" ? (payload["model"] as string) : candidate.model_id;
139
+ const needsMaterialize = !request.stream && model.includes("deepseek-v4-flash");
140
+ if (needsMaterialize) {
141
+ payload["stream"] = true;
142
+ // Set reasoning_effort to none for this model to suppress reasoning streaming
143
+ payload["reasoning_effort"] = "none";
144
+ const cur = typeof payload["max_tokens"] === "number" ? (payload["max_tokens"] as number) : 0;
145
+ payload["max_tokens"] = Math.max(cur, 256);
146
+ // keep GatewayError reachable
147
+ if (!model) throw new GatewayError("invalid_request", 400, "Cline model is required");
148
+ }
149
+ }
150
+
151
+ // (modelEntries / recommendedModels / fetchRecommendedModels) preserved
152
+ // as Cartethyia discovery helpers. The factory's discovery endpoint is wired
153
+ // to /ai/cline/recommended-models; these helpers can be used by a custom
154
+ // discovery layer or tests. Keeping them guarantees the custom tag→vision
155
+ // logic and clinePass vs free branching are not lost in the port.
156
+
157
+ interface ClineRecommendedModel {
158
+ readonly id?: unknown;
159
+ readonly name?: unknown;
160
+ readonly description?: unknown;
161
+ readonly tags?: unknown;
162
+ }
163
+
164
+ interface ClineRecommendedPayload {
165
+ readonly recommended?: unknown;
166
+ readonly free?: unknown;
167
+ readonly clinePass?: unknown;
168
+ readonly clineCloud?: unknown;
169
+ }
170
+
171
+ function modelEntries(value: unknown): readonly ClineRecommendedModel[] {
172
+ return Array.isArray(value)
173
+ ? value.filter((entry): entry is ClineRecommendedModel => typeof entry === "object" && entry !== null && !Array.isArray(entry))
174
+ : [];
175
+ }
176
+
177
+ function recommendedModels(
178
+ value: unknown,
179
+ pass: boolean,
180
+ upstream: ReadonlyMap<string, ClineUpstreamLimits> = new Map(),
181
+ freeIds: readonly string[] | null = null,
182
+ ): readonly ModelDefinition[] {
183
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
184
+ const payload = value as ClineRecommendedPayload;
185
+ // The endpoint publishes four buckets: `recommended` (the current picks),
186
+ // `free` (the zero-cost tier), `clinePass` (the subscription roster) and
187
+ // `clineCloud` (cloud-routed). A pass-enabled account may use every one of
188
+ // them, so all four are read; a free account sees the free tier only. Reading
189
+ // just `free`/`clinePass` silently dropped `recommended` and `clineCloud`
190
+ // from the catalog entirely.
191
+ //
192
+ // The third element marks the zero-cost tier — the `free` bucket alone, not
193
+ // `recommended`: a pass account receives `recommended` too, so calling it a
194
+ // free-tier row would mislabel a subscription catalog entry. The tier is what
195
+ // separates a "Free models (auto)" row from an ordinary fetched one.
196
+ //
197
+ // Membership of the bucket *is* the rule, deliberately not the id prefix. The
198
+ // `free` bucket's ids are mostly `cline-free/…`, but the prefix does not
199
+ // define the tier in either direction: a free id need not carry it (the
200
+ // catalog serves `deepseek/deepseek-v4-flash` and `z-ai/glm-5.3-flash` on the
201
+ // free tier with no prefix at all), and carrying it proves nothing on its own.
202
+ // A prefix test would therefore both miss served models and admit retired
203
+ // ones, so the bucket the endpoint already sorted them into is the authority.
204
+ const buckets: ReadonlyArray<readonly [unknown, boolean, boolean]> = pass
205
+ ? [
206
+ [payload.recommended, false, false],
207
+ [payload.free, false, true],
208
+ [payload.clinePass, true, false],
209
+ [payload.clineCloud, true, false],
210
+ ]
211
+ : [
212
+ [payload.recommended, false, false],
213
+ [payload.free, false, true],
214
+ ];
215
+ const seen = new Set<string>();
216
+ const out: ModelDefinition[] = [];
217
+ for (const [bucket, subscription, freeTier] of buckets) {
218
+ for (const entry of modelEntries(bucket)) {
219
+ const rawId = typeof entry.id === "string" ? entry.id.trim() : "";
220
+ if (rawId.length === 0) continue;
221
+ // The endpoint already namespaces its own ids (`cline-pass/…`,
222
+ // `cline-cloud/…`, `cline-free/…`), so the bucket decides only how the
223
+ // row is priced and which catalog entry answers for it — never how the id
224
+ // is spelled. Prefixing here double-namespaced every id the endpoint had
225
+ // already prefixed (`cline-pass/cline-pass/mimo-v2.6-flash`), which no
226
+ // upstream route resolves.
227
+ const id = rawId;
228
+ if (seen.has(id)) continue;
229
+ seen.add(id);
230
+ // Limits, in authority order:
231
+ // 1. Cline's own `/ai/cline/models` catalog (`context_length`,
232
+ // `top_provider.max_completion_tokens`) — the serving provider's
233
+ // answer, and the only source that covers the whole roster.
234
+ // 2. `defineModel`'s fallback, which is the base catalog's exact row
235
+ // when models.dev files the provider (`cline-pass`), else the
236
+ // most-agreed row for the bare id, else the documented default.
237
+ // The recommended-models endpoint itself states no limits (only
238
+ // id/name/description/tags), so a fixed pair here was simply invented:
239
+ // the pass entries are 1M-context, and an output cap above the real
240
+ // window is unsatisfiable.
241
+ const providerId = subscription ? "cline-pass" : "cline";
242
+ const limits = upstreamLimitsFor(upstream, id);
243
+ const declaredVision = tagsOf(entry).some(
244
+ (tag) => tag.includes("vision") || tag.includes("multimodal"),
245
+ );
246
+ out.push(
247
+ defineModel({
248
+ id,
249
+ providerId,
250
+ wireFamily: "chat",
251
+ endpoint: CLINE_CHAT_PATH,
252
+ // The upstream catalog's `input_modalities` is authoritative when it
253
+ // lists the id; the roster's own tags are the fallback signal.
254
+ vision: limits?.vision ?? declaredVision,
255
+ ...(limits?.contextLimit === undefined ? {} : { ctx: limits.contextLimit }),
256
+ ...(limits?.outputLimit === undefined ? {} : { out: limits.outputLimit }),
257
+ reasoning: true,
258
+ // `free` follows the free bucket, not "not a subscription".
259
+ //
260
+ // This used to read `!subscription`, which made every `recommended`
261
+ // row free: that bucket is billed, and the flag is not cosmetic —
262
+ // `defineModel` prices a `free` row at zero, so the cost surface
263
+ // reported paid models as free. The upstream settles it: dispatching
264
+ // `anthropic/claude-sonnet-5.5` from `recommended` answers 402
265
+ // `insufficient_credits`, while a `free` bucket id answers 200. A
266
+ // subscription row is also not free — it bills a subscription — so
267
+ // only the free bucket carries the flag.
268
+ free: freeTier,
269
+ freeTier,
270
+ }),
271
+ );
272
+ }
273
+ }
274
+ // `/models` names free ids the recommended roster does not, so it is read as
275
+ // a second free-tier source rather than a replacement. An id the roster
276
+ // already defined keeps the roster's row: it carries the endpoint's own
277
+ // pricing and subscription classification, which this listing cannot state.
278
+ for (const id of freeIds ?? []) {
279
+ if (seen.has(id)) continue;
280
+ seen.add(id);
281
+ const limits = upstreamLimitsFor(upstream, id);
282
+ out.push(
283
+ defineModel({
284
+ id,
285
+ providerId: "cline",
286
+ wireFamily: "chat",
287
+ endpoint: CLINE_CHAT_PATH,
288
+ vision: limits?.vision ?? false,
289
+ ...(limits?.contextLimit === undefined ? {} : { ctx: limits.contextLimit }),
290
+ ...(limits?.outputLimit === undefined ? {} : { out: limits.outputLimit }),
291
+ reasoning: true,
292
+ // The suffix is the provider's own free-tier spelling, so both
293
+ // statements hold: zero per token, and a row the model list groups
294
+ // under the free tier.
295
+ free: true,
296
+ freeTier: true,
297
+ }),
298
+ );
299
+ }
300
+ return out;
301
+ }
302
+
303
+ /**
304
+ * Upstream limits for one model id, read from Cline's own catalog.
305
+ *
306
+ * The recommended-models roster states no limits at all, but Cline's
307
+ * `/ai/cline/models` catalog does: `context_length` plus
308
+ * `top_provider.max_completion_tokens`, and `architecture.input_modalities`.
309
+ * That is the serving provider's own answer, so it outranks any guess.
310
+ *
311
+ * The two endpoints do not share an id spelling. The roster names a
312
+ * subscription model `cline-pass/kimi-k3` while the catalog files it as
313
+ * `moonshotai/kimi-k3`, so a lookup tries the id as given and then its bare
314
+ * segment — the same bare-id rule `modelsDevCatalog` uses, and safe here
315
+ * because this is one provider's catalog rather than a shared global index.
316
+ */
317
+ interface ClineUpstreamLimits {
318
+ readonly contextLimit?: number;
319
+ readonly outputLimit?: number;
320
+ readonly vision?: boolean;
321
+ }
322
+
323
+ function upstreamCatalogIndex(value: unknown): ReadonlyMap<string, ClineUpstreamLimits> {
324
+ const root = isRecord(value) ? value : undefined;
325
+ const rows = Array.isArray(root?.["data"]) ? root["data"] : Array.isArray(value) ? value : [];
326
+ const byId = new Map<string, ClineUpstreamLimits>();
327
+ for (const row of rows) {
328
+ if (!isRecord(row)) continue;
329
+ const id = typeof row["id"] === "string" ? row["id"].trim().toLowerCase() : "";
330
+ if (id.length === 0) continue;
331
+ const contextLimit = positiveCount(row["context_length"]);
332
+ const topProvider = isRecord(row["top_provider"]) ? row["top_provider"] : undefined;
333
+ const outputLimit = positiveCount(topProvider?.["max_completion_tokens"]);
334
+ const architecture = isRecord(row["architecture"]) ? row["architecture"] : undefined;
335
+ const modalities = Array.isArray(architecture?.["input_modalities"])
336
+ ? architecture["input_modalities"]
337
+ : [];
338
+ const limits: ClineUpstreamLimits = {
339
+ ...(contextLimit === undefined ? {} : { contextLimit }),
340
+ ...(outputLimit === undefined ? {} : { outputLimit }),
341
+ vision: modalities.some((m) => m === "image"),
342
+ };
343
+ byId.set(id, limits);
344
+ // The roster and this catalog spell the same model differently: the roster
345
+ // names a subscription model `cline-pass/kimi-k3` while this catalog files
346
+ // it as `moonshotai/kimi-k3`. Indexing the bare segment too is what lets
347
+ // `upstreamLimitsFor` resolve one to the other. An exact key already
348
+ // recorded stays authoritative, so a provider-prefixed row never shadows a
349
+ // row filed under the bare id itself.
350
+ const slash = id.indexOf("/");
351
+ if (slash >= 0) {
352
+ const bare = id.slice(slash + 1);
353
+ if (!byId.has(bare)) byId.set(bare, limits);
354
+ }
355
+ }
356
+ return byId;
357
+ }
358
+
359
+ function positiveCount(value: unknown): number | undefined {
360
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
361
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
362
+ }
363
+
364
+ /** The catalog row for `id`, falling back to its bare segment. */
365
+ function upstreamLimitsFor(
366
+ index: ReadonlyMap<string, ClineUpstreamLimits>,
367
+ id: string,
368
+ ): ClineUpstreamLimits | undefined {
369
+ const normalized = id.trim().toLowerCase();
370
+ const exact = index.get(normalized);
371
+ if (exact !== undefined) return exact;
372
+ const slash = normalized.indexOf("/");
373
+ return slash < 0 ? undefined : index.get(normalized.slice(slash + 1));
374
+ }
375
+
376
+ function tagsOf(entry: ClineRecommendedModel): readonly string[] {
377
+ return Array.isArray(entry.tags)
378
+ ? entry.tags
379
+ .filter((tag): tag is string => typeof tag === "string")
380
+ .map((tag) => tag.toLowerCase())
381
+ : [];
382
+ }
383
+
384
+ export async function fetchClineRecommendedModels(
385
+ signal: AbortSignal | undefined,
386
+ pass: boolean,
387
+ fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = globalThis.fetch,
388
+ ): Promise<readonly ModelDefinition[] | null> {
389
+ const load = async (): Promise<readonly ModelDefinition[] | null> => {
390
+ const timeout = AbortSignal.timeout(10_000);
391
+ const composed = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
392
+ try {
393
+ const url = `${CLINE_BASE_URL}${CLINE_RECOMMENDED_MODELS_PATH}`;
394
+ const res = await fetcher(url, { headers: { accept: "application/json" }, signal: composed });
395
+ if (!res.ok) return null;
396
+ const json = (await res.json()) as unknown;
397
+ // The limits live in the sibling catalog, not in the roster. Fetch it
398
+ // alongside, and treat its absence as "no upstream limits" rather than a
399
+ // failure: the roster is still useful without them, and `defineModel`
400
+ // falls back to the base catalog.
401
+ const upstream = await fetchClineUpstreamCatalog(fetcher, composed);
402
+ // The recommended roster is not the whole free tier: `/models` names
403
+ // free ids it never lists (minimax among them). Union both, with the
404
+ // recommended roster winning on an id it already defined.
405
+ const freeIds = await fetchClineFreeModelIds(fetcher, composed);
406
+ const models = recommendedModels(json, pass, upstream, freeIds);
407
+ return models.length > 0 ? models : null;
408
+ } catch {
409
+ return null;
410
+ }
411
+ };
412
+ // The default fetcher is the production path: this roster is public and
413
+ // changes slowly, so a short TTL avoids redundant refreshes. A caller that
414
+ // injects its own fetcher (tests, custom egress) always gets a live call.
415
+ if (fetcher !== globalThis.fetch) return load();
416
+ return getCachedModelDiscovery(`cline-recommended:${pass ? "pass" : "free"}`, load);
417
+ }
418
+
419
+ /**
420
+ * Whether the credential belongs to a Cline Pass account.
421
+ *
422
+ * Reads the same `/users/me/plan` endpoint the quota collector uses. A pass
423
+ * account is the only one allowed to see the `clinePass`/`clineCloud` roster;
424
+ * any failure answers `false`, so a free account never has subscription rows
425
+ * written into its catalog.
426
+ */
427
+ export async function clineAccountHasPass(
428
+ credential: string,
429
+ fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = globalThis.fetch,
430
+ ): Promise<boolean> {
431
+ try {
432
+ const token = credential.startsWith("workos:") ? credential : `workos:${credential}`;
433
+ const res = await fetcher("https://api.cline.bot/api/v1/users/me/plan", {
434
+ headers: { accept: "application/json", authorization: `Bearer ${token}` },
435
+ signal: AbortSignal.timeout(10_000),
436
+ });
437
+ if (!res.ok) return false;
438
+ const body = (await res.json()) as { plan?: { displayName?: unknown } };
439
+ const name = typeof body.plan?.displayName === "string" ? body.plan.displayName.toLowerCase() : "";
440
+ return name.includes("pass");
441
+ } catch {
442
+ return false;
443
+ }
444
+ }
445
+
446
+ /** Cline's model catalog, indexed for limit lookup. Empty when unavailable. */
447
+ async function fetchClineUpstreamCatalog(
448
+ fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
449
+ signal: AbortSignal,
450
+ ): Promise<ReadonlyMap<string, ClineUpstreamLimits>> {
451
+ try {
452
+ const res = await fetcher(`${CLINE_BASE_URL}${CLINE_MODELS_CATALOG_PATH}`, {
453
+ headers: { accept: "application/json" },
454
+ signal,
455
+ });
456
+ if (!res.ok) return new Map();
457
+ return upstreamCatalogIndex(await res.json());
458
+ } catch {
459
+ return new Map();
460
+ }
461
+ }
462
+
463
+ /**
464
+ * The free tier as `/models` states it, which is not the same roster as
465
+ * `/ai/cline/recommended-models`.
466
+ *
467
+ * The two endpoints disagree, and the disagreement is the whole reason this
468
+ * exists: `recommended-models` files four free ids, all spelled `cline-free/…`,
469
+ * while `/models` publishes seventeen ids carrying a `:free` suffix — including
470
+ * `minimax/minimax-m3.1-flash:free`, which the recommended roster never names.
471
+ * Reading only the recommended roster therefore could not see minimax at all.
472
+ *
473
+ * The suffix is matched exactly rather than as a substring, the same rule
474
+ * OpenRouter's free discovery uses, so an id whose own name contains "free" is
475
+ * not swept in. A non-ok response is `null` — "this endpoint had nothing to
476
+ * say" — rather than an empty list, so the caller can tell a failed fetch from
477
+ * a genuinely empty tier.
478
+ */
479
+ export async function fetchClineFreeModelIds(
480
+ fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = globalThis.fetch,
481
+ signal: AbortSignal = AbortSignal.timeout(10_000),
482
+ ): Promise<readonly string[] | null> {
483
+ try {
484
+ const res = await fetcher(`${CLINE_BASE_URL}${CLINE_MODELS_LIST_PATH}`, {
485
+ headers: { accept: "application/json" },
486
+ signal,
487
+ });
488
+ if (!res.ok) return null;
489
+ const body = (await res.json()) as unknown;
490
+ const root = isRecord(body) ? body : undefined;
491
+ const rows = Array.isArray(root?.["data"]) ? root["data"] : Array.isArray(body) ? body : [];
492
+ const ids = new Set<string>();
493
+ for (const row of rows) {
494
+ if (!isRecord(row)) continue;
495
+ const id = typeof row["id"] === "string" ? row["id"].trim() : "";
496
+ if (id.length === 0) continue;
497
+ if (!id.endsWith(":free")) continue;
498
+ ids.add(id);
499
+ }
500
+ return [...ids];
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Static seed for Cline's free tier.
508
+ *
509
+ * Every id here is materialized into the `models` table at boot with
510
+ * `source: "builtin"`, and `seedBundledModels` deletes any builtin row this
511
+ * list no longer declares. That makes the list the *owner* of these rows: an id
512
+ * Cline retires from its roster keeps its catalog card — and its route — until
513
+ * it is removed here, because nothing else prunes a builtin row. The seed is
514
+ * therefore kept to ids Cline currently serves on the free tier; a stale entry
515
+ * is a card that can only ever fail a probe.
516
+ *
517
+ * Cline's live roster (`/ai/cline/recommended-models`, read by
518
+ * `fetchClineRecommendedModels`) is the authority for what is routable right
519
+ * now, and "Fetch models" reconciles against it. This list is the offline seed
520
+ * that makes a fresh install usable before the first fetch.
521
+ */
522
+ export const CLINE_MODELS: readonly ModelDefinition[] = openrouterFreeRoster({
523
+ providerId: CLINE_PROVIDER_ID,
524
+ endpoint: CLINE_CHAT_PATH,
525
+ });
526
+
527
+
528
+ export function createClineAdapter(fetchImpl?: typeof fetch): ProviderAdapter {
529
+ return createApiKeyAdapter(
530
+ {
531
+ provider_id: CLINE_PROVIDER_ID,
532
+ base_url: CLINE_BASE_URL,
533
+ endpoint_paths_by_wire_family: { chat: CLINE_CHAT_PATH, responses: "/responses" },
534
+ buildExtraHeaders: (ctx) => clineExtraHeaders(ctx),
535
+ prePayload: clinePrePayload,
536
+ },
537
+ fetchImpl,
538
+ );
539
+ }