cartethyia 2.5.4 → 2.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-mUfcknqm.js} +1 -1
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-uPiGVTTK.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  60. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  61. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  62. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
  64. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  66. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  68. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-js6TJvdr.js → index-CmxZr14F.js} +2 -2
  72. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
  74. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
  76. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  77. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  78. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
  80. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
  82. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
  86. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  87. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  88. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
  92. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-WUCXorXw.js → select-BYR_5K2c.js} +1 -1
  94. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
  96. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
  98. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
  100. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
  102. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  103. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  104. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  105. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  106. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +54 -54
  112. package/migrations/README.md +52 -52
  113. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  114. package/package.json +96 -95
  115. package/scripts/cli/admin.ts +125 -0
  116. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  117. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
  118. package/scripts/cli/doctor.ts +37 -0
  119. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  120. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  121. package/scripts/cli/instance.ts +224 -0
  122. package/scripts/cli/main.ts +61 -18
  123. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  124. package/scripts/cli/port.ts +100 -0
  125. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  126. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  127. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  128. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  129. package/scripts/cli/tray/index.ts +62 -12
  130. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  131. package/scripts/cli/{command/update.ts → update.ts} +39 -39
  132. package/scripts/cli/version.ts +67 -0
  133. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  134. package/src/boot/app.ts +697 -693
  135. package/src/boot/dependencies.ts +647 -646
  136. package/src/boot/timeout.ts +2 -13
  137. package/src/config/cooldowns.ts +186 -138
  138. package/src/config/network.ts +8 -14
  139. package/src/config/providers.ts +7 -10
  140. package/src/config/runtime.ts +76 -76
  141. package/src/config/security.ts +15 -0
  142. package/src/config/spec.ts +20 -30
  143. package/src/config/telemetry.ts +0 -20
  144. package/src/config/timeouts.ts +0 -6
  145. package/src/console/admin/model-abuse/contracts.ts +28 -6
  146. package/src/console/admin/model-abuse/routes.ts +56 -3
  147. package/src/console/api-keys/contracts.ts +410 -414
  148. package/src/console/api-keys/routes.ts +881 -880
  149. package/src/console/auth/credentials.ts +3 -3
  150. package/src/console/auth/setup-service.ts +2 -2
  151. package/src/console/backup/export-endpoint.ts +6 -2
  152. package/src/console/backup/nine-router.ts +2 -5
  153. package/src/console/backup/service.ts +4 -0
  154. package/src/console/backup/store.ts +192 -29
  155. package/src/console/backup/validate.ts +3 -45
  156. package/src/console/cli-tools/fs-ops.ts +487 -487
  157. package/src/console/console-router-guards.ts +1 -1
  158. package/src/console/data-management/contracts.ts +13 -6
  159. package/src/console/data-management/operations.ts +23 -18
  160. package/src/console/data-management/routes.ts +0 -1
  161. package/src/console/domain-registration.ts +10 -5
  162. package/src/console/observability/live.ts +22 -0
  163. package/src/console/observability/store.ts +39 -16
  164. package/src/console/observability/usage-dimensions.ts +4 -4
  165. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  166. package/src/console/providers/catalog/model-operations.ts +5 -2
  167. package/src/console/providers/catalog/provider-operations.ts +4 -4
  168. package/src/console/providers/catalog/public-model-store.ts +8 -11
  169. package/src/console/providers/catalog/store.ts +1553 -1553
  170. package/src/console/providers/oauth/routes.ts +8 -12
  171. package/src/console/quota/account-quota-tenant.ts +6 -6
  172. package/src/console/quota/quota-view.ts +1 -1
  173. package/src/console/remote-support/contracts.ts +61 -0
  174. package/src/console/remote-support/routes.ts +65 -0
  175. package/src/console/remote-support/service.ts +207 -0
  176. package/src/console/response-compression.ts +0 -3
  177. package/src/console/routing/pools/probe-result.ts +3 -2
  178. package/src/console/routing/pools/probe-verdict.ts +132 -132
  179. package/src/console/routing/pools/store.ts +8 -8
  180. package/src/console/settings/contracts.ts +58 -2
  181. package/src/console/settings/store.ts +30 -3
  182. package/src/console/shared/cursor.ts +8 -8
  183. package/src/console/shared/errors.ts +1 -3
  184. package/src/network/http2-bidi.ts +2 -4
  185. package/src/network/pool/agent.ts +478 -478
  186. package/src/network/pool/selector.ts +5 -4
  187. package/src/network/retry.ts +1 -2
  188. package/src/network/ssrf.ts +5 -4
  189. package/src/observability/event-compaction.ts +80 -78
  190. package/src/observability/event-day-index.ts +13 -15
  191. package/src/observability/event-log.ts +4 -4
  192. package/src/observability/logger.ts +6 -6
  193. package/src/observability/payload-capture.ts +151 -33
  194. package/src/observability/payload-store.ts +397 -440
  195. package/src/observability/redaction.ts +5 -6
  196. package/src/observability/request-notify.ts +43 -0
  197. package/src/observability/telemetry-status.ts +8 -9
  198. package/src/observability/token-speed.ts +5 -4
  199. package/src/persistence/db-mode.ts +7 -7
  200. package/src/persistence/readiness.ts +1 -1
  201. package/src/persistence/schema.ts +1406 -1402
  202. package/src/persistence/shared-cache.ts +1 -2
  203. package/src/persistence/stores/api-key-store.ts +18 -6
  204. package/src/persistence/stores/share-store.ts +4 -3
  205. package/src/persistence/stores/telemetry-store.ts +236 -237
  206. package/src/protocol/canonical-model.ts +11 -3
  207. package/src/protocol/codecs.ts +138 -4
  208. package/src/protocol/gateway-error.ts +6 -4
  209. package/src/protocol/request/chat.ts +21 -1
  210. package/src/protocol/request/codex.ts +10 -1
  211. package/src/protocol/request/messages.ts +4 -5
  212. package/src/protocol/request/responses.ts +9 -1
  213. package/src/protocol/response/chat.ts +34 -4
  214. package/src/protocol/response/codex.ts +7 -4
  215. package/src/protocol/response/dsml.ts +140 -28
  216. package/src/protocol/response/responses.ts +11 -5
  217. package/src/protocol/stream-error-frames.ts +4 -5
  218. package/src/providers/authentication/jwt-validator.ts +9 -10
  219. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  220. package/src/providers/configured-openai-providers.ts +134 -136
  221. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  222. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  223. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  224. package/src/providers/discovery/probe-phases.ts +4 -4
  225. package/src/providers/discovery/probe-wire.ts +6 -13
  226. package/src/providers/discovery/probing-service.ts +1117 -1045
  227. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  228. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  231. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  232. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  233. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  234. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  235. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  236. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  237. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  238. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  239. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  240. package/src/providers/integrations/claude/claude.ts +608 -608
  241. package/src/providers/integrations/cline/cline-quota.ts +132 -132
  242. package/src/providers/integrations/cline/cline.ts +537 -539
  243. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  244. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  245. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  246. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  247. package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
  248. package/src/providers/integrations/cursor/cursor.ts +11 -163
  249. package/src/providers/integrations/factory/factory-models.ts +3 -5
  250. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  251. package/src/providers/integrations/grok/grok.ts +6 -7
  252. package/src/providers/integrations/hermes.ts +14 -18
  253. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  254. package/src/providers/integrations/kiro/kiro.ts +1 -1
  255. package/src/providers/integrations/mistral.ts +6 -6
  256. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  257. package/src/providers/integrations/opencode/provider.ts +8 -8
  258. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  259. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  260. package/src/providers/integrations/qoder/qoder.ts +884 -865
  261. package/src/providers/integrations/shared/connect.ts +6 -6
  262. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  263. package/src/providers/integrations/trae/trae.ts +2 -2
  264. package/src/providers/integrations/zcode/oauth.ts +6 -1
  265. package/src/providers/operations/account-health-service.ts +1528 -1523
  266. package/src/providers/operations/account-reset-service.ts +1 -1
  267. package/src/providers/operations/client-versions.ts +6 -6
  268. package/src/providers/operations/custom-cli-headers.ts +3 -4
  269. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  270. package/src/providers/operations/provider-catalog-service.ts +3 -4
  271. package/src/providers/provider-metadata.ts +4 -8
  272. package/src/providers/provider-registry.ts +6 -7
  273. package/src/providers/provider-usage.ts +1 -1
  274. package/src/security/abuse.ts +15 -10
  275. package/src/security/access-control.ts +144 -146
  276. package/src/security/admission/contracts.ts +11 -0
  277. package/src/security/admission/in-memory-store.ts +4 -3
  278. package/src/security/admission/lease-sweep.ts +6 -6
  279. package/src/security/admission/reasons.ts +5 -1
  280. package/src/security/admission/redis-store.ts +211 -210
  281. package/src/security/admission/service.ts +4 -0
  282. package/src/security/api-key-auth.ts +504 -504
  283. package/src/security/ip-boundary.ts +155 -15
  284. package/src/security/model-abuse.ts +366 -94
  285. package/src/security/outbound-headers.ts +3 -4
  286. package/src/shared/sleep.ts +4 -0
  287. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  288. package/src/transport/dispatch/attempt-loop.ts +531 -530
  289. package/src/transport/dispatch/stream-bridge.ts +1 -3
  290. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  291. package/src/transport/dispatch/streaming/stream.ts +5 -6
  292. package/src/transport/failure-policy.ts +162 -8
  293. package/src/transport/guards/auth-guard.ts +7 -4
  294. package/src/transport/guards/concurrency-guard.ts +0 -5
  295. package/src/transport/guards/error-lifecycle.ts +13 -6
  296. package/src/transport/guards/pipeline.ts +15 -3
  297. package/src/transport/guards/request-context.ts +8 -2
  298. package/src/transport/request/inflight.ts +5 -6
  299. package/src/transport/request/preparer.ts +2 -2
  300. package/src/transport/request/state.ts +12 -8
  301. package/src/transport/routing/admission.ts +4 -0
  302. package/src/transport/routing/route-catalog.ts +8 -1
  303. package/src/transport/routing/route-model.ts +46 -14
  304. package/src/transport/routing/router.ts +21 -14
  305. package/src/transport/surface/completion.ts +2 -3
  306. package/src/transport/surface/dialects.ts +5 -6
  307. package/src/transport/surface/responses/encode.ts +38 -0
  308. package/src/transport/surface/responses/parse.ts +4 -0
  309. package/src/transport/translation/capabilities.ts +2 -3
  310. package/src/transport/translation/quirks.ts +3 -3
  311. package/src/workers/buddy-growth-tasks.ts +5 -10
  312. package/src/workers/daily-checkin.ts +17 -23
  313. package/src/workers/quota-refresh-sweep.ts +26 -26
  314. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  315. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
  316. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  317. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  318. package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
  319. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  320. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  321. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  322. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  323. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  324. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
  325. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  326. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
  327. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  328. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
  329. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  330. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  331. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
  332. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  333. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
  334. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  335. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
  336. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  337. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  338. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  339. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  340. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
  341. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  342. package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
  343. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  344. package/dist/dashboard/assets/app-COlK95oi.css +0 -2
  345. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  346. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  347. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  348. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  349. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  350. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  351. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  352. package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
  353. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  354. package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
  355. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  356. package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
  357. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  358. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
  360. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  361. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  363. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  365. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
  367. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  368. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  369. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  370. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  371. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  372. package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
  373. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  374. package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
  375. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  376. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  377. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  378. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  379. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  380. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  381. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  382. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  383. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  384. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  385. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  386. package/scripts/cli/command/admin.ts +0 -65
  387. package/scripts/cli/command/doctor.ts +0 -257
  388. package/scripts/cli/gateway/drain-token.ts +0 -78
  389. package/scripts/cli/gateway/instance-lock.ts +0 -105
  390. package/scripts/cli/gateway/stop.ts +0 -51
  391. package/scripts/cli/support/bun.ts +0 -27
  392. package/scripts/cli/support/version.ts +0 -37
  393. /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
  394. /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
  395. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -1,539 +1,537 @@
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
- }
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": the
259
+ // `recommended` bucket is billed, and the flag is not cosmetic —
260
+ // `defineModel` prices a `free` row at zero, so the cost surface
261
+ // would report paid models as free. The upstream settles it:
262
+ // dispatching `anthropic/claude-sonnet-5.5` from `recommended` answers
263
+ // 402 `insufficient_credits`, while a `free` bucket id answers 200. A
264
+ // subscription row is also not free — it bills a subscription — so
265
+ // only the free bucket carries the flag.
266
+ free: freeTier,
267
+ freeTier,
268
+ }),
269
+ );
270
+ }
271
+ }
272
+ // `/models` names free ids the recommended roster does not, so it is read as
273
+ // a second free-tier source rather than a replacement. An id the roster
274
+ // already defined keeps the roster's row: it carries the endpoint's own
275
+ // pricing and subscription classification, which this listing cannot state.
276
+ for (const id of freeIds ?? []) {
277
+ if (seen.has(id)) continue;
278
+ seen.add(id);
279
+ const limits = upstreamLimitsFor(upstream, id);
280
+ out.push(
281
+ defineModel({
282
+ id,
283
+ providerId: "cline",
284
+ wireFamily: "chat",
285
+ endpoint: CLINE_CHAT_PATH,
286
+ vision: limits?.vision ?? false,
287
+ ...(limits?.contextLimit === undefined ? {} : { ctx: limits.contextLimit }),
288
+ ...(limits?.outputLimit === undefined ? {} : { out: limits.outputLimit }),
289
+ reasoning: true,
290
+ // The suffix is the provider's own free-tier spelling, so both
291
+ // statements hold: zero per token, and a row the model list groups
292
+ // under the free tier.
293
+ free: true,
294
+ freeTier: true,
295
+ }),
296
+ );
297
+ }
298
+ return out;
299
+ }
300
+
301
+ /**
302
+ * Upstream limits for one model id, read from Cline's own catalog.
303
+ *
304
+ * The recommended-models roster states no limits at all, but Cline's
305
+ * `/ai/cline/models` catalog does: `context_length` plus
306
+ * `top_provider.max_completion_tokens`, and `architecture.input_modalities`.
307
+ * That is the serving provider's own answer, so it outranks any guess.
308
+ *
309
+ * The two endpoints do not share an id spelling. The roster names a
310
+ * subscription model `cline-pass/kimi-k3` while the catalog files it as
311
+ * `moonshotai/kimi-k3`, so a lookup tries the id as given and then its bare
312
+ * segment — the same bare-id rule `modelsDevCatalog` uses, and safe here
313
+ * because this is one provider's catalog rather than a shared global index.
314
+ */
315
+ interface ClineUpstreamLimits {
316
+ readonly contextLimit?: number;
317
+ readonly outputLimit?: number;
318
+ readonly vision?: boolean;
319
+ }
320
+
321
+ function upstreamCatalogIndex(value: unknown): ReadonlyMap<string, ClineUpstreamLimits> {
322
+ const root = isRecord(value) ? value : undefined;
323
+ const rows = Array.isArray(root?.["data"]) ? root["data"] : Array.isArray(value) ? value : [];
324
+ const byId = new Map<string, ClineUpstreamLimits>();
325
+ for (const row of rows) {
326
+ if (!isRecord(row)) continue;
327
+ const id = typeof row["id"] === "string" ? row["id"].trim().toLowerCase() : "";
328
+ if (id.length === 0) continue;
329
+ const contextLimit = positiveCount(row["context_length"]);
330
+ const topProvider = isRecord(row["top_provider"]) ? row["top_provider"] : undefined;
331
+ const outputLimit = positiveCount(topProvider?.["max_completion_tokens"]);
332
+ const architecture = isRecord(row["architecture"]) ? row["architecture"] : undefined;
333
+ const modalities = Array.isArray(architecture?.["input_modalities"])
334
+ ? architecture["input_modalities"]
335
+ : [];
336
+ const limits: ClineUpstreamLimits = {
337
+ ...(contextLimit === undefined ? {} : { contextLimit }),
338
+ ...(outputLimit === undefined ? {} : { outputLimit }),
339
+ vision: modalities.some((m) => m === "image"),
340
+ };
341
+ byId.set(id, limits);
342
+ // The roster and this catalog spell the same model differently: the roster
343
+ // names a subscription model `cline-pass/kimi-k3` while this catalog files
344
+ // it as `moonshotai/kimi-k3`. Indexing the bare segment too is what lets
345
+ // `upstreamLimitsFor` resolve one to the other. An exact key already
346
+ // recorded stays authoritative, so a provider-prefixed row never shadows a
347
+ // row filed under the bare id itself.
348
+ const slash = id.indexOf("/");
349
+ if (slash >= 0) {
350
+ const bare = id.slice(slash + 1);
351
+ if (!byId.has(bare)) byId.set(bare, limits);
352
+ }
353
+ }
354
+ return byId;
355
+ }
356
+
357
+ function positiveCount(value: unknown): number | undefined {
358
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
359
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
360
+ }
361
+
362
+ /** The catalog row for `id`, falling back to its bare segment. */
363
+ function upstreamLimitsFor(
364
+ index: ReadonlyMap<string, ClineUpstreamLimits>,
365
+ id: string,
366
+ ): ClineUpstreamLimits | undefined {
367
+ const normalized = id.trim().toLowerCase();
368
+ const exact = index.get(normalized);
369
+ if (exact !== undefined) return exact;
370
+ const slash = normalized.indexOf("/");
371
+ return slash < 0 ? undefined : index.get(normalized.slice(slash + 1));
372
+ }
373
+
374
+ function tagsOf(entry: ClineRecommendedModel): readonly string[] {
375
+ return Array.isArray(entry.tags)
376
+ ? entry.tags
377
+ .filter((tag): tag is string => typeof tag === "string")
378
+ .map((tag) => tag.toLowerCase())
379
+ : [];
380
+ }
381
+
382
+ export async function fetchClineRecommendedModels(
383
+ signal: AbortSignal | undefined,
384
+ pass: boolean,
385
+ fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = globalThis.fetch,
386
+ ): Promise<readonly ModelDefinition[] | null> {
387
+ const load = async (): Promise<readonly ModelDefinition[] | null> => {
388
+ const timeout = AbortSignal.timeout(10_000);
389
+ const composed = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
390
+ try {
391
+ const url = `${CLINE_BASE_URL}${CLINE_RECOMMENDED_MODELS_PATH}`;
392
+ const res = await fetcher(url, { headers: { accept: "application/json" }, signal: composed });
393
+ if (!res.ok) return null;
394
+ const json = (await res.json()) as unknown;
395
+ // The limits live in the sibling catalog, not in the roster. Fetch it
396
+ // alongside, and treat its absence as "no upstream limits" rather than a
397
+ // failure: the roster is still useful without them, and `defineModel`
398
+ // falls back to the base catalog.
399
+ const upstream = await fetchClineUpstreamCatalog(fetcher, composed);
400
+ // The recommended roster is not the whole free tier: `/models` names
401
+ // free ids it never lists (minimax among them). Union both, with the
402
+ // recommended roster winning on an id it already defined.
403
+ const freeIds = await fetchClineFreeModelIds(fetcher, composed);
404
+ const models = recommendedModels(json, pass, upstream, freeIds);
405
+ return models.length > 0 ? models : null;
406
+ } catch {
407
+ return null;
408
+ }
409
+ };
410
+ // The default fetcher is the production path: this roster is public and
411
+ // changes slowly, so a short TTL avoids redundant refreshes. A caller that
412
+ // injects its own fetcher (tests, custom egress) always gets a live call.
413
+ if (fetcher !== globalThis.fetch) return load();
414
+ return getCachedModelDiscovery(`cline-recommended:${pass ? "pass" : "free"}`, load);
415
+ }
416
+
417
+ /**
418
+ * Whether the credential belongs to a Cline Pass account.
419
+ *
420
+ * Reads the same `/users/me/plan` endpoint the quota collector uses. A pass
421
+ * account is the only one allowed to see the `clinePass`/`clineCloud` roster;
422
+ * any failure answers `false`, so a free account never has subscription rows
423
+ * written into its catalog.
424
+ */
425
+ export async function clineAccountHasPass(
426
+ credential: string,
427
+ fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = globalThis.fetch,
428
+ ): Promise<boolean> {
429
+ try {
430
+ const token = credential.startsWith("workos:") ? credential : `workos:${credential}`;
431
+ const res = await fetcher("https://api.cline.bot/api/v1/users/me/plan", {
432
+ headers: { accept: "application/json", authorization: `Bearer ${token}` },
433
+ signal: AbortSignal.timeout(10_000),
434
+ });
435
+ if (!res.ok) return false;
436
+ const body = (await res.json()) as { plan?: { displayName?: unknown } };
437
+ const name = typeof body.plan?.displayName === "string" ? body.plan.displayName.toLowerCase() : "";
438
+ return name.includes("pass");
439
+ } catch {
440
+ return false;
441
+ }
442
+ }
443
+
444
+ /** Cline's model catalog, indexed for limit lookup. Empty when unavailable. */
445
+ async function fetchClineUpstreamCatalog(
446
+ fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
447
+ signal: AbortSignal,
448
+ ): Promise<ReadonlyMap<string, ClineUpstreamLimits>> {
449
+ try {
450
+ const res = await fetcher(`${CLINE_BASE_URL}${CLINE_MODELS_CATALOG_PATH}`, {
451
+ headers: { accept: "application/json" },
452
+ signal,
453
+ });
454
+ if (!res.ok) return new Map();
455
+ return upstreamCatalogIndex(await res.json());
456
+ } catch {
457
+ return new Map();
458
+ }
459
+ }
460
+
461
+ /**
462
+ * The free tier as `/models` states it, which is not the same roster as
463
+ * `/ai/cline/recommended-models`.
464
+ *
465
+ * The two endpoints disagree, and the disagreement is the whole reason this
466
+ * exists: `recommended-models` files four free ids, all spelled `cline-free/…`,
467
+ * while `/models` publishes seventeen ids carrying a `:free` suffix — including
468
+ * `minimax/minimax-m3.1-flash:free`, which the recommended roster never names.
469
+ * Reading only the recommended roster therefore could not see minimax at all.
470
+ *
471
+ * The suffix is matched exactly rather than as a substring, the same rule
472
+ * OpenRouter's free discovery uses, so an id whose own name contains "free" is
473
+ * not swept in. A non-ok response is `null` — "this endpoint had nothing to
474
+ * say" — rather than an empty list, so the caller can tell a failed fetch from
475
+ * a genuinely empty tier.
476
+ */
477
+ export async function fetchClineFreeModelIds(
478
+ fetcher: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = globalThis.fetch,
479
+ signal: AbortSignal = AbortSignal.timeout(10_000),
480
+ ): Promise<readonly string[] | null> {
481
+ try {
482
+ const res = await fetcher(`${CLINE_BASE_URL}${CLINE_MODELS_LIST_PATH}`, {
483
+ headers: { accept: "application/json" },
484
+ signal,
485
+ });
486
+ if (!res.ok) return null;
487
+ const body = (await res.json()) as unknown;
488
+ const root = isRecord(body) ? body : undefined;
489
+ const rows = Array.isArray(root?.["data"]) ? root["data"] : Array.isArray(body) ? body : [];
490
+ const ids = new Set<string>();
491
+ for (const row of rows) {
492
+ if (!isRecord(row)) continue;
493
+ const id = typeof row["id"] === "string" ? row["id"].trim() : "";
494
+ if (id.length === 0) continue;
495
+ if (!id.endsWith(":free")) continue;
496
+ ids.add(id);
497
+ }
498
+ return [...ids];
499
+ } catch {
500
+ return null;
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Static seed for Cline's free tier.
506
+ *
507
+ * Every id here is materialized into the `models` table at boot with
508
+ * `source: "builtin"`, and `seedBundledModels` deletes any builtin row this
509
+ * list no longer declares. That makes the list the *owner* of these rows: an id
510
+ * Cline retires from its roster keeps its catalog card — and its route — until
511
+ * it is removed here, because nothing else prunes a builtin row. The seed is
512
+ * therefore kept to ids Cline currently serves on the free tier; a stale entry
513
+ * is a card that can only ever fail a probe.
514
+ *
515
+ * Cline's live roster (`/ai/cline/recommended-models`, read by
516
+ * `fetchClineRecommendedModels`) is the authority for what is routable right
517
+ * now, and "Fetch models" reconciles against it. This list is the offline seed
518
+ * that makes a fresh install usable before the first fetch.
519
+ */
520
+ export const CLINE_MODELS: readonly ModelDefinition[] = openrouterFreeRoster({
521
+ providerId: CLINE_PROVIDER_ID,
522
+ endpoint: CLINE_CHAT_PATH,
523
+ });
524
+
525
+
526
+ export function createClineAdapter(fetchImpl?: typeof fetch): ProviderAdapter {
527
+ return createApiKeyAdapter(
528
+ {
529
+ provider_id: CLINE_PROVIDER_ID,
530
+ base_url: CLINE_BASE_URL,
531
+ endpoint_paths_by_wire_family: { chat: CLINE_CHAT_PATH, responses: "/responses" },
532
+ buildExtraHeaders: (ctx) => clineExtraHeaders(ctx),
533
+ prePayload: clinePrePayload,
534
+ },
535
+ fetchImpl,
536
+ );
537
+ }