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
@@ -25,11 +25,65 @@ async function qoderVersion(response: Response): Promise<string | null> {
25
25
  }
26
26
  }
27
27
 
28
+ /**
29
+ * Newest Trae IDE version from the official changelog.
30
+ *
31
+ * Trae publishes no npm package and no release API, so the changelog is the
32
+ * only machine-readable source of the current client version. It matters
33
+ * upstream: intl `llm_raw_chat` answers `4015` unless the request carries the
34
+ * *current* client version, so a stale pin is an outage, not a cosmetic drift.
35
+ *
36
+ * The page embeds its content as JSON, and its newest entry is the first dated
37
+ * heading — not the highest semver, because this vendor's hotfix line numbers
38
+ * past its feature line (3.5.104 shipped after 3.5.97). Taking `max(semver)`
39
+ * would report an old build as current, so this walks the document in order and
40
+ * returns the first version following a dated release heading.
41
+ */
42
+ async function traeVersion(response: Response): Promise<string | null> {
43
+ try {
44
+ const text = await response.text();
45
+ const heading =
46
+ /"insert":"((?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},\s*\d{4}\s*\([^)]*\))\\n"/g;
47
+ for (const match of text.matchAll(heading)) {
48
+ const window = text.slice(match.index ?? 0, (match.index ?? 0) + 900);
49
+ const found = window.match(/TraeCode\s+v?(\d+\.\d+\.\d+)|v?(\d+\.\d+\.\d+)\s+is\s+released/i);
50
+ const version = found?.[1] ?? found?.[2];
51
+ if (version && isSemverish(version)) return version.trim();
52
+ }
53
+ return null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
28
59
  async function clineClientVersion(response: Response): Promise<string | null> {
29
60
  const data = (await response.json()) as { version?: unknown };
30
61
  return isSemverish(data.version) ? data.version.trim() : null;
31
62
  }
32
63
 
64
+ /**
65
+ * Newest Cursor build from the official download API, as the IDE's own header
66
+ * spells it.
67
+ *
68
+ * Cursor's `x-cursor-client-version` is not the IDE semver on its own: the wire
69
+ * value is `cli-<version>-<sha7>` (observed live as `cli-2026.10.19-6bbf2c4`).
70
+ * The download API is the only source that returns both halves — `version` and
71
+ * `commitSha` — so they are joined here rather than guessed. A semver alone
72
+ * would be a different identity than the one the IDE sends.
73
+ */
74
+ async function cursorVersion(response: Response): Promise<string | null> {
75
+ try {
76
+ const data = (await response.json()) as { version?: unknown; commitSha?: unknown };
77
+ const version = data.version;
78
+ if (!isSemverish(version)) return null;
79
+ const sha = typeof data.commitSha === "string" ? data.commitSha.trim() : "";
80
+ const short = sha.length >= 7 ? sha.slice(0, 7) : "";
81
+ return short.length > 0 ? `cli-${version.trim()}-${short}` : version.trim();
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
33
87
  async function clineSdkVersion(response: Response): Promise<string | null> {
34
88
  const data = (await response.json()) as {
35
89
  version?: unknown;
@@ -177,7 +231,7 @@ export const VERSION_SOURCES = {
177
231
  },
178
232
  qoder: {
179
233
  key: "qoder",
180
- fallback: "1.1.73",
234
+ fallback: "1.1.74",
181
235
  sources: [
182
236
  {
183
237
  url: "https://registry.npmjs.org/@qoder-ai/qodercli/latest",
@@ -222,6 +276,11 @@ export const VERSION_SOURCES = {
222
276
  fallback: "0.0.95",
223
277
  sources: [{ url: "https://registry.npmjs.org/@cline/sdk/latest", extract: clineSdkVersion }],
224
278
  },
279
+ clineCli: {
280
+ key: "cline-cli",
281
+ fallback: "3.0.73",
282
+ sources: [{ url: "https://registry.npmjs.org/cline/latest", extract: clineSdkVersion }],
283
+ },
225
284
  codex: {
226
285
  key: "codex",
227
286
  fallback: "0.163.0",
@@ -257,7 +316,7 @@ export const VERSION_SOURCES = {
257
316
  },
258
317
  workbuddyCli: {
259
318
  key: "workbuddy",
260
- fallback: "2.168.2",
319
+ fallback: "2.169.0",
261
320
  sources: [
262
321
  { url: "https://registry.npmjs.org/@tencent-ai/codebuddy-code/latest" },
263
322
  { url: "https://registry.npmmirror.com/@tencent-ai/codebuddy-code/latest" },
@@ -284,7 +343,7 @@ export const VERSION_SOURCES = {
284
343
  },
285
344
  codebuddy: {
286
345
  key: "codebuddy",
287
- fallback: "2.168.2",
346
+ fallback: "2.169.0",
288
347
  sources: [
289
348
  { url: "https://registry.npmjs.org/@tencent-ai/codebuddy-code/latest" },
290
349
  { url: "https://registry.npmmirror.com/@tencent-ai/codebuddy-code/latest" },
@@ -342,6 +401,46 @@ export const VERSION_SOURCES = {
342
401
  fallback: "1.4.10-R6427.1",
343
402
  sources: [{ url: "https://api.meta.ai/muse-code/channels/muse-stable" }],
344
403
  },
404
+ trae: {
405
+ key: "trae",
406
+ // Fallback = the donor's last observed client version. Discovery replaces
407
+ // it within one monitor tick; it is only what a cold process sends before
408
+ // the first successful fetch.
409
+ fallback: "3.6.2",
410
+ sources: [
411
+ {
412
+ url: "https://docs.trae.ai/ide/changelog?_lang=en",
413
+ extract: traeVersion,
414
+ },
415
+ ],
416
+ },
417
+ cursor: {
418
+ key: "cursor",
419
+ // Fallback = the last observed wire version. Discovery replaces it within
420
+ // one monitor tick; it is only what a cold process sends before the first
421
+ // successful fetch.
422
+ fallback: "cli-2026.10.19-6bbf2c4",
423
+ sources: [
424
+ {
425
+ url: "https://cursor.com/api/download?platform=win32-x64-user&releaseTrack=stable",
426
+ extract: cursorVersion,
427
+ },
428
+ ],
429
+ },
430
+ hermes: {
431
+ key: "hermes",
432
+ // Hermes DOES publish a semver: the GitHub release tag is `v0.22.2`. The
433
+ // comment this replaced claimed no semver existed and pinned a date tag
434
+ // (`v2026.9.24`) as the version — a tag, not a release, and not the
435
+ // identity the client sends.
436
+ fallback: "0.22.2",
437
+ sources: [
438
+ {
439
+ url: "https://api.github.com/repos/NousResearch/hermes-agent/releases/latest",
440
+ extract: githubReleaseVersion,
441
+ },
442
+ ],
443
+ },
345
444
  zcode: {
346
445
  key: "zcode",
347
446
  fallback: "3.14.3",
@@ -357,11 +456,15 @@ export const VERSION_SOURCES = {
357
456
  const resolvers = {
358
457
  antigravity: createClientVersionResolver({ ...VERSION_SOURCES.antigravity, client: "antigravity" }),
359
458
  qoder: createClientVersionResolver({ ...VERSION_SOURCES.qoder, client: "qoder" }),
459
+ hermes: createClientVersionResolver({ ...VERSION_SOURCES.hermes, client: "hermes" }),
460
+ trae: createClientVersionResolver({ ...VERSION_SOURCES.trae, client: "trae" }),
461
+ cursor: createClientVersionResolver({ ...VERSION_SOURCES.cursor, client: "cursor" }),
360
462
  opencode: createClientVersionResolver({ ...VERSION_SOURCES.opencode, client: "opencode" }),
361
463
  commandcode: createClientVersionResolver({ ...VERSION_SOURCES.commandcode, client: "command-code" }),
362
464
  grok: createClientVersionResolver({ ...VERSION_SOURCES.grok, client: "grok" }),
363
465
  clineClient: createClientVersionResolver({ ...VERSION_SOURCES.clineClient, client: "cline" }),
364
466
  clineSdk: createClientVersionResolver({ ...VERSION_SOURCES.clineSdk, client: "cline-sdk" }),
467
+ clineCli: createClientVersionResolver({ ...VERSION_SOURCES.clineCli, client: "cline-cli" }),
365
468
  codex: createClientVersionResolver({ ...VERSION_SOURCES.codex, client: "codex" }),
366
469
  workbuddyClient: createClientVersionResolver({ ...VERSION_SOURCES.workbuddyClient, client: "workbuddy" }),
367
470
  workbuddyCnClient: createClientVersionResolver({ ...VERSION_SOURCES.workbuddyCnClient, client: "workbuddy-cn" }),
@@ -384,9 +487,16 @@ const PROVIDER_VERSION_RESOLVERS: Readonly<Record<string, ClientVersionResolver>
384
487
  opencode: resolvers.opencode,
385
488
  "opencode-zen": resolvers.opencode,
386
489
  "opencode-go": resolvers.opencode,
387
- cline: resolvers.clineClient,
490
+ // The identity this provider is dispatched under is the CLI's, so the route
491
+ // identity reports the CLI version. `clineClient` (the VS Code extension
492
+ // manifest) is no longer the client this provider claims to be.
493
+ cline: resolvers.clineCli,
388
494
  codex: resolvers.codex,
389
495
  qoder: resolvers.qoder,
496
+ hermes: resolvers.hermes,
497
+ trae: resolvers.trae,
498
+ traecn: resolvers.trae,
499
+ cursor: resolvers.cursor,
390
500
  commandcode: resolvers.commandcode,
391
501
  workbuddy: resolvers.workbuddyClient,
392
502
  workbuddycn: resolvers.workbuddyCnClient,
@@ -484,6 +594,8 @@ function accessor<K extends keyof typeof resolvers>(name: K): {
484
594
  }
485
595
 
486
596
  export const getQoderVersion = accessor("qoder").get;
597
+ export const getHermesVersion = accessor("hermes").get;
598
+ export const getTraeVersion = accessor("trae").get;
487
599
  export const getAntigravityVersion = accessor("antigravity").get;
488
600
 
489
601
  export const getOpenCodeVersion = accessor("opencode").get;
@@ -503,8 +615,10 @@ export function buildGrokAuthUserAgent(version = getGrokVersion()): string {
503
615
 
504
616
  export const getClineClientVersion = accessor("clineClient").get;
505
617
  export const getClineSdkVersion = accessor("clineSdk").get;
618
+ export const getClineCliVersion = accessor("clineCli").get;
506
619
  export const _resetClineClientVersionCache = accessor("clineClient").reset;
507
620
  export const _resetClineSdkVersionCache = accessor("clineSdk").reset;
621
+ export const _resetClineCliVersionCache = accessor("clineCli").reset;
508
622
 
509
623
  export const getCodexVersion = accessor("codex").get;
510
624
 
@@ -513,9 +627,16 @@ export const getWorkBuddyCnClientVersion = accessor("workbuddyCnClient").get;
513
627
  export const getWorkBuddyCliVersion = accessor("workbuddyCli").get;
514
628
 
515
629
  export function buildWorkBuddyUserAgent(
630
+ identity = "WB",
516
631
  clientVersion = getWorkBuddyClientVersion(),
517
632
  cliVersion = getWorkBuddyCliVersion(),
518
633
  ): string {
634
+ // 1:1 with codebuddy2api. The CLI profile is `CLI/<v> CodeBuddy/<v>` and the
635
+ // WorkBuddy profile is `WorkBuddy/<wb> WorkBuddy AI/<wb> CLI/<cli>`, which
636
+ // sends NO SDK headers — two separate profiles, not one string with a switch.
637
+ // Whether SDK headers ride along is decided by `workbuddyHeaders`, not here, so
638
+ // this builder only decides the UA text.
639
+ if (identity === "CLI") return `CLI/${cliVersion} CodeBuddy/${cliVersion}`;
519
640
  return `WorkBuddy/${clientVersion} WorkBuddy AI/${clientVersion} CLI/${cliVersion}`;
520
641
  }
521
642
 
@@ -529,7 +650,7 @@ export const getCodeBuddyCnVersion = accessor("codebuddyCn").get;
529
650
  * The CodeBuddy User-Agent.
530
651
  *
531
652
  * Both identities share the CLI *package* version, which is why `codebuddy`
532
- * (the npm `@tencent-ai/codebuddy-code` pin, `2.168.2` — the value both
653
+ * (the npm `@tencent-ai/codebuddy-code` pin, `2.169.0` — the value both
533
654
  * references use for CLI) is the version for each. `codebuddyCn` is the CN
534
655
  * *desktop* build (`4.12.2.…`), and using it here produced
535
656
  * `CLI/4.12.2.41207131 CodeBuddy/4.12.2.41207131` — a CLI claiming a desktop
@@ -538,8 +659,14 @@ export const getCodeBuddyCnVersion = accessor("codebuddyCn").get;
538
659
  export function buildCodeBuddyUserAgent(
539
660
  identity: "IDE" | "CLI",
540
661
  version = getCodeBuddyVersion(),
662
+ cnVersion = getCodeBuddyCnVersion(),
541
663
  ): string {
542
- return `${identity}/${version} CodeBuddy/${version}`;
664
+ // 1:1 with codebuddy2api's identity_headers(): the CLI profile uses ONE version
665
+ // (the npm CLI package pin) for both halves, and the IDE profile uses the other
666
+ // product's version. This previously always used `version`, so "CLI" produced
667
+ // `CLI/<ide> CodeBuddy/<ide>` — a CLI claiming the desktop build.
668
+ const resolved = identity === "CLI" ? cnVersion : version;
669
+ return `${identity}/${resolved} CodeBuddy/${resolved}`;
543
670
  }
544
671
 
545
672
  export const _resetCodeBuddyVersionCache = accessor("codebuddy").reset;
@@ -1,86 +1,95 @@
1
- /** Durable materialization of bundled provider model metadata. */
2
- import { and, eq, sql } from "drizzle-orm";
3
- import type { CartethyiaDatabase } from "../../persistence/db-handle";
4
- import { models } from "../../persistence/schema";
5
- import type { ModelDefinition } from "../provider-model-definition";
6
-
7
- /** Persists compiled provider model metadata and reconciles drifted capability flags. */
8
- export async function seedBundledModels(
9
- db: CartethyiaDatabase,
10
- builtinModels: ReadonlyMap<string, readonly ModelDefinition[]>,
11
- ): Promise<void> {
12
- for (const [providerId, definitions] of builtinModels) {
13
- if (definitions.length === 0) continue;
14
- // Drop builtin rows the current catalog no longer declares, including rows
15
- // whose model id is still present but whose endpoint path drifted (e.g. a
16
- // provider that moved from `/chat/completions` to `/v2/chat/completions`).
17
- // The unique key is (provider, model, endpoint), so a changed endpoint
18
- // would otherwise insert a second row and leave the stale route behind —
19
- // the model list would then show duplicates and probes could hit the dead
20
- // path. Composite `NOT IN` keeps exactly the (model, endpoint) pairs the
21
- // catalog owns.
22
- const pairs = definitions.map(
23
- (definition) => sql`(${definition.modelId}, ${definition.endpointPath})`,
24
- );
25
- await db
26
- .delete(models)
27
- .where(
28
- and(
29
- eq(models.providerId, providerId),
30
- eq(models.source, "builtin"),
31
- sql`(${models.modelId}, ${models.endpointPath}) NOT IN (${sql.join(pairs, sql`, `)})`,
32
- ),
33
- );
34
- }
35
- const now = new Date();
36
- const seen = new Set<string>();
37
- const rows = [...builtinModels.entries()].flatMap(([providerId, definitions]) =>
38
- definitions.flatMap((definition) => {
39
- const key = `${providerId}\u0000${definition.modelId}\u0000${definition.endpointPath}`;
40
- if (seen.has(key)) return [];
41
- seen.add(key);
42
- return [
43
- {
44
- providerId,
45
- modelId: definition.modelId,
46
- wireFamily: definition.wireFamily,
47
- serviceKind: definition.serviceKind ?? "llm",
48
- endpointPath: definition.endpointPath,
49
- contextLimit: definition.contextLimit,
50
- outputLimit: definition.outputLimit,
51
- modalities: definition.modalities,
52
- reasoning: definition.reasoning,
53
- toolCall: definition.toolCall,
54
- cost: definition.cost,
55
- source: "builtin",
56
- sourceUpdatedAt: now,
57
- enabled: true,
58
- },
59
- ];
60
- }),
61
- );
62
- if (rows.length === 0) return;
63
- // Reconcile static metadata on conflict rather than skipping: capability
64
- // flags (toolCall/reasoning/webSearch), wire family, limits, and cost can
65
- // drift on a pre-existing row (e.g. a discovery seeded `tool_call=false`
66
- // before the static catalog was corrected). `source` is forced back to
67
- // `builtin`: a fetch upsert may have flipped a builtin row to `discovered`,
68
- // and the static catalog owns these ids. `enabled` is operator-owned and
69
- // must never be reset here.
70
- await db.insert(models).values(rows).onConflictDoUpdate({
71
- target: [models.providerId, models.modelId, models.endpointPath],
72
- set: {
73
- wireFamily: sql`excluded.wire_family`,
74
- serviceKind: sql`excluded.service_kind`,
75
- endpointPath: sql`excluded.endpoint_path`,
76
- contextLimit: sql`excluded.context_limit`,
77
- outputLimit: sql`excluded.output_limit`,
78
- modalities: sql`excluded.modalities`,
79
- reasoning: sql`excluded.reasoning`,
80
- toolCall: sql`excluded.tool_call`,
81
- cost: sql`excluded.cost`,
82
- source: sql`'builtin'`,
83
- sourceUpdatedAt: sql`excluded.source_updated_at`,
84
- },
85
- });
86
- }
1
+ /** Durable materialization of bundled provider model metadata. */
2
+ import { and, eq, sql } from "drizzle-orm";
3
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
4
+ import { models } from "../../persistence/schema";
5
+ import type { ModelDefinition } from "../provider-model-definition";
6
+
7
+ /** Persists compiled provider model metadata and reconciles drifted capability flags. */
8
+ export async function seedBundledModels(
9
+ db: CartethyiaDatabase,
10
+ builtinModels: ReadonlyMap<string, readonly ModelDefinition[]>,
11
+ ): Promise<void> {
12
+ for (const [providerId, definitions] of builtinModels) {
13
+ if (definitions.length === 0) continue;
14
+ // Drop builtin rows the current catalog no longer declares, including rows
15
+ // whose model id is still present but whose endpoint path drifted (e.g. a
16
+ // provider that moved from `/chat/completions` to `/v2/chat/completions`).
17
+ // The unique key is (provider, model, endpoint), so a changed endpoint
18
+ // would otherwise insert a second row and leave the stale route behind —
19
+ // the model list would then show duplicates and probes could hit the dead
20
+ // path. Composite `NOT IN` keeps exactly the (model, endpoint) pairs the
21
+ // catalog owns.
22
+ const pairs = definitions.map(
23
+ (definition) => sql`(${definition.modelId}, ${definition.endpointPath})`,
24
+ );
25
+ await db
26
+ .delete(models)
27
+ .where(
28
+ and(
29
+ eq(models.providerId, providerId),
30
+ eq(models.source, "builtin"),
31
+ sql`(${models.modelId}, ${models.endpointPath}) NOT IN (${sql.join(pairs, sql`, `)})`,
32
+ ),
33
+ );
34
+ }
35
+ const now = new Date();
36
+ const seen = new Set<string>();
37
+ const rows = [...builtinModels.entries()].flatMap(([providerId, definitions]) =>
38
+ definitions.flatMap((definition) => {
39
+ const key = `${providerId}\u0000${definition.modelId}\u0000${definition.endpointPath}`;
40
+ if (seen.has(key)) return [];
41
+ seen.add(key);
42
+ return [
43
+ {
44
+ providerId,
45
+ modelId: definition.modelId,
46
+ wireFamily: definition.wireFamily,
47
+ serviceKind: definition.serviceKind ?? "llm",
48
+ endpointPath: definition.endpointPath,
49
+ contextLimit: definition.contextLimit,
50
+ outputLimit: definition.outputLimit,
51
+ modalities: definition.modalities,
52
+ reasoning: definition.reasoning,
53
+ toolCall: definition.toolCall,
54
+ cost: definition.cost,
55
+ // A static row either knows it is refused on a free plan or says
56
+ // nothing. Never coerced to false: the column is three-valued, and
57
+ // "nobody established it" is not the same claim as "served on free".
58
+ proOnly: definition.proOnly ?? null,
59
+ // Same three-valued rule for the inverse: the provider's directory
60
+ // states the tier, so the flag is carried rather than derived.
61
+ freeTier: definition.freeTier ?? null,
62
+ source: "builtin",
63
+ sourceUpdatedAt: now,
64
+ enabled: true,
65
+ },
66
+ ];
67
+ }),
68
+ );
69
+ if (rows.length === 0) return;
70
+ // Reconcile static metadata on conflict rather than skipping: capability
71
+ // flags (toolCall/reasoning/webSearch), wire family, limits, and cost can
72
+ // drift on a pre-existing row (e.g. a discovery seeded `tool_call=false`
73
+ // before the static catalog was corrected). `source` is forced back to
74
+ // `builtin`: a fetch upsert may have flipped a builtin row to `discovered`,
75
+ // and the static catalog owns these ids. `enabled` is operator-owned and
76
+ // must never be reset here.
77
+ await db.insert(models).values(rows).onConflictDoUpdate({
78
+ target: [models.providerId, models.modelId, models.endpointPath],
79
+ set: {
80
+ wireFamily: sql`excluded.wire_family`,
81
+ serviceKind: sql`excluded.service_kind`,
82
+ endpointPath: sql`excluded.endpoint_path`,
83
+ contextLimit: sql`excluded.context_limit`,
84
+ outputLimit: sql`excluded.output_limit`,
85
+ modalities: sql`excluded.modalities`,
86
+ reasoning: sql`excluded.reasoning`,
87
+ toolCall: sql`excluded.tool_call`,
88
+ cost: sql`excluded.cost`,
89
+ proOnly: sql`excluded.pro_only`,
90
+ freeTier: sql`excluded.free_tier`,
91
+ source: sql`'builtin'`,
92
+ sourceUpdatedAt: sql`excluded.source_updated_at`,
93
+ },
94
+ });
95
+ }
@@ -29,8 +29,31 @@ const CRED_CACHE_MAX_ENTRIES = 2_000;
29
29
  interface CredCacheEntry {
30
30
  readonly at: number;
31
31
  readonly value: ResolvedCredential;
32
+ /**
33
+ * When this entry must stop being served, or `undefined` for "the default
34
+ * TTL applies".
35
+ *
36
+ * Per-entry rather than global because a credential's usable lifetime is a
37
+ * property of the credential: an OAuth token with 30 minutes left can be
38
+ * served for the full window, one with 20 seconds left for almost none of
39
+ * it. Storing the bound on the entry lets both share one cache.
40
+ */
41
+ readonly expiresAt?: number;
32
42
  }
33
43
 
44
+ /**
45
+ * Margin kept between a cached OAuth token's expiry and the moment the cache
46
+ * stops serving it.
47
+ *
48
+ * The old rule refused to cache OAuth at all unless the token outlived the
49
+ * whole 5 s window, which meant every attempt paid a SELECT + AES-GCM decrypt
50
+ * for every OAuth account — the largest remaining per-request DB read. Caching
51
+ * against the token's own expiry is the same safety with a fraction of the
52
+ * reads. The margin covers clock skew between this process and the issuer, and
53
+ * the time a request already in flight may hold the value.
54
+ */
55
+ const CRED_CACHE_EXPIRY_MARGIN_MS = 30_000;
56
+
34
57
  const credCache = new Map<string, CredCacheEntry>();
35
58
 
36
59
  /**
@@ -159,9 +182,14 @@ export async function resolveCredentialForAccount(
159
182
  // the cached value would contain a soon-to-expire secret.
160
183
  const now = Date.now();
161
184
  const cached = credCache.get(credCacheKey(providerId, accountId));
162
- if (cached && now - cached.at < CRED_CACHE_TTL_MS) {
163
- const kind = (cached.value as { credential_kind?: string }).credential_kind;
164
- if (kind !== "oauth") return cached.value;
185
+ if (cached) {
186
+ // Two independent bounds: the cache window (rotation freshness) and, for a
187
+ // credential whose usable life was known, the moment that life ends. The
188
+ // shorter one wins, so a token about to expire is never served past it and
189
+ // no credential is served stale for longer than the window.
190
+ const withinWindow = now - cached.at < CRED_CACHE_TTL_MS;
191
+ const withinLife = cached.expiresAt === undefined || now < cached.expiresAt;
192
+ if (withinWindow && withinLife) return cached.value;
165
193
  }
166
194
 
167
195
  const parsedProviderId = parseProviderId(providerId);
@@ -208,11 +236,22 @@ export async function resolveCredentialForAccount(
208
236
  ...(authState === undefined ? {} : { auth_state: authState }),
209
237
  };
210
238
  const value = resolver.resolve(parsedProviderId, [alternative]).credential;
211
- // Cache non-OAuth credentials unconditionally; OAuth only when far from expiry.
239
+ // Every credential is cached; an OAuth token with a known expiry simply gets
240
+ // a shorter bound. Refusing to cache OAuth was the safe-looking choice that
241
+ // cost a SELECT + decrypt per attempt, and it was stricter than necessary:
242
+ // the risk is serving an expired token, which a TTL tied to that token
243
+ // expiry prevents as completely as not caching does.
212
244
  const isOAuth = row.credentialKind === "oauth";
213
- const safeToCache = !isOAuth || (dueAt !== undefined && dueAt > now + CRED_CACHE_TTL_MS);
214
- if (safeToCache) {
215
- credCache.set(credCacheKey(providerId, accountId), { at: now, value });
245
+ const lifeBound =
246
+ isOAuth && dueAt !== undefined ? dueAt - CRED_CACHE_EXPIRY_MARGIN_MS : undefined;
247
+ // A token already inside the margin must not be cached at all — its bound
248
+ // would be in the past, so the entry would be dead on arrival.
249
+ if (lifeBound === undefined || lifeBound > now) {
250
+ credCache.set(credCacheKey(providerId, accountId), {
251
+ at: now,
252
+ value,
253
+ ...(lifeBound === undefined ? {} : { expiresAt: lifeBound }),
254
+ });
216
255
  evictCredCacheOldest();
217
256
  }
218
257
  return value;
@@ -5,6 +5,7 @@
5
5
  import type { CanonicalEvent, CanonicalRequest, WireFamily } from "../protocol/canonical-model";
6
6
  import { GatewayError } from "../protocol/gateway-error";
7
7
  import { BUNDLED_PROVIDER_METADATA, type BundledProviderId } from "./provider-metadata";
8
+ import type { ValidatedOutboundHttp2Bidi } from "../network/http2-bidi";
8
9
 
9
10
  /** Closed union of every shipped provider ID. */
10
11
  export const BUNDLED_PROVIDER_IDS = BUNDLED_PROVIDER_METADATA.map((definition) => definition.id) as readonly BundledProviderId[];
@@ -67,14 +68,76 @@ export type ValidatedOutboundWebSocket = (
67
68
  signal: AbortSignal,
68
69
  ) => Promise<ProviderWebSocketSession>;
69
70
 
71
+ /**
72
+ * Validated bidirectional HTTP/2 egress, bound to the same selected network
73
+ * pool as `outbound_fetch`.
74
+ *
75
+ * A Connect-RPC bidi Run is neither a `fetch` (the request body never ends)
76
+ * nor a WebSocket (different protocol), so it gets its own capability — and it
77
+ * goes through the same binding factory as every other egress, which is what
78
+ * keeps SSRF validation, DNS pinning, and pool byte attribution intact. A
79
+ * provider adapter receives this closure and never dials a socket itself.
80
+ */
81
+ /**
82
+ * The dialer contract handed to a provider adapter.
83
+ *
84
+ * Declared in `network/http2-bidi.ts` — the transport owns the contract — and
85
+ * re-exported here so provider code keeps one import site.
86
+ */
87
+ export type { ValidatedOutboundHttp2Bidi };
88
+
89
+ /**
90
+ * A provider session that outlives the HTTP request that created it.
91
+ *
92
+ * Cursor's Run stays open between the client's separate HTTP requests: the
93
+ * first request returns a tool call and the next submits that tool's result on
94
+ * the *same* upstream Run. The account and network-pool slot that Run holds
95
+ * must therefore stay held too, while the request-scoped accounting for the
96
+ * first HTTP request is already settled. Ownership is transferred here so the
97
+ * release is idempotent and belongs to exactly one owner.
98
+ */
99
+ export interface AttemptLeaseOwnership {
100
+ /**
101
+ * Hands the selected account/network-pool lease to a provider session that
102
+ * outlives this request. Returns the release to call exactly once when the
103
+ * session terminates; returns `undefined` when there was nothing to transfer
104
+ * (a credential-less or pool-less route), which is not an error.
105
+ *
106
+ * Idempotent: a second call returns `undefined` and releases nothing.
107
+ */
108
+ transferToProviderSession(): (() => void) | undefined;
109
+ }
110
+
111
+ /**
112
+ * Identity that authorizes a later request to continue a provider session the
113
+ * gateway is already holding.
114
+ *
115
+ * Cursor's tool loop spans two client HTTP requests on one upstream Run, so the
116
+ * Run — and the account lease it holds — outlives the request that opened it. A
117
+ * later request may only claim the Run by presenting the same tenant, API key,
118
+ * provider, and account, all recorded server-side when the Run was created. A
119
+ * call id alone is never sufficient: it is unguessable but it is still just a
120
+ * string a client could carry into another key's request.
121
+ */
122
+ export interface ProviderContinuationScope {
123
+ readonly tenantId: string;
124
+ readonly apiKeyId: string;
125
+ readonly providerId: string;
126
+ readonly providerAccountId: string;
127
+ }
128
+
70
129
  /** Context passed from routing to a provider adapter. */
71
130
  export interface ProviderDispatchContext {
72
131
  readonly credential: ResolvedCredential;
73
132
  readonly deadline: number;
74
133
  readonly abort_signal: AbortSignal;
75
134
  readonly outbound_fetch?: ValidatedOutboundFetch | undefined;
135
+ /** Validated image/attachment egress; deliberately excluded from payload capture. */
136
+ readonly outbound_attachment_fetch?: ValidatedOutboundFetch | undefined;
76
137
  /** Validated WSS egress bound to the same selected network pool as HTTP. */
77
138
  readonly outbound_websocket?: ValidatedOutboundWebSocket | undefined;
139
+ /** Validated bidirectional HTTP/2 egress bound to the same selected network pool. */
140
+ readonly outbound_http2?: ValidatedOutboundHttp2Bidi | undefined;
78
141
  /** Built-in API-key route-selected fallback User-Agent; custom providers leave this unset. */
79
142
  readonly user_agent?: string | undefined;
80
143
  /** Gateway request id, stable across provider retries for one logical request. */
@@ -85,6 +148,19 @@ export interface ProviderDispatchContext {
85
148
  readonly conversation_affinity?: string | undefined;
86
149
  /** Opaque identity shared by retries of one logical gateway request. */
87
150
  readonly request_identity?: object | undefined;
151
+ /**
152
+ * Ownership of this attempt's account/network-pool lease. Present only when
153
+ * the route holds a lease; a provider session that outlives the request
154
+ * (Cursor's Run) takes it over so the slot is not released while the Run is
155
+ * live, and not released twice when it ends.
156
+ */
157
+ readonly lease_ownership?: AttemptLeaseOwnership | undefined;
158
+ /**
159
+ * Identity of the caller, for a provider session that outlives this request.
160
+ * Absent when the route cannot scope a continuation, in which case a session
161
+ * must not be held open across requests at all.
162
+ */
163
+ readonly continuation_scope?: ProviderContinuationScope | undefined;
88
164
  }
89
165
 
90
166
  const USER_AGENT_HEADER_NAME = /^user-agent$/i;