cartethyia 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -18,6 +18,11 @@
18
18
  * rather than surfacing an empty body to the client.
19
19
  */
20
20
  import { GatewayError } from "../../protocol/gateway-error";
21
+ import {
22
+ fallbackRetryDelayMs,
23
+ MIN_RETRY_BUDGET_MS,
24
+ sleep as abortableSleep,
25
+ } from "../failure-policy";
21
26
 
22
27
  // ─── Public types ──────────────────────────────────────────────────────────
23
28
 
@@ -38,9 +43,17 @@ export type EmptyStreamFetch = (
38
43
  init: { headers: Record<string, string>; body: string; signal: AbortSignal },
39
44
  ) => Promise<Response>;
40
45
 
46
+ /**
47
+ * Retry-delay policy.
48
+ *
49
+ * Delays come from `fallbackRetryDelayMs` (capped exponential with full
50
+ * jitter), whose base and cap are operator-tunable. There is deliberately no
51
+ * per-caller base: the local formula this replaced was uncapped and
52
+ * unjittered, so a provider returning 200-with-no-events could drive an
53
+ * unbounded wait, and concurrent clients could resynchronize their retries.
54
+ */
41
55
  export interface EmptyStreamPolicy {
42
56
  readonly maxEmptyReplays?: number;
43
- readonly baseDelayMs?: number;
44
57
  readonly firstEventTimeoutMs?: number;
45
58
  readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
46
59
  }
@@ -55,9 +68,8 @@ export interface EmptyStreamResult {
55
68
  // ─── Defaults ──────────────────────────────────────────────────────────────
56
69
 
57
70
  const DEFAULT_MAX_REPLAYS = 2;
58
- const DEFAULT_BASE_DELAY_MS = 500;
59
71
  const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 200_000; // 200s, matches CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS default
60
- const MIN_RETRY_BUDGET_MS = 15_000; // attempt-loop floor
72
+
61
73
 
62
74
  // ─── Helpers ───────────────────────────────────────────────────────────────
63
75
 
@@ -66,27 +78,7 @@ export function isEmptyStream(tally: StreamTally): boolean {
66
78
  return tally.dataEvents === 0 || (!tally.meaningful && !tally.thoughtOnly);
67
79
  }
68
80
 
69
- function defaultSleep(ms: number, signal: AbortSignal): Promise<void> {
70
- return new Promise((resolve, reject) => {
71
- if (signal.aborted) {
72
- reject(new GatewayError("transport_closed", 499, "Aborted"));
73
- return;
74
- }
75
- const timer = setTimeout(() => {
76
- signal.removeEventListener("abort", onAbort);
77
- resolve();
78
- }, ms);
79
- const onAbort = () => {
80
- clearTimeout(timer);
81
- reject(new GatewayError("transport_closed", 499, "Aborted"));
82
- };
83
- signal.addEventListener("abort", onAbort, { once: true });
84
- });
85
- }
86
81
 
87
- function backoffMs(attempt: number, base: number): number {
88
- return base * 2 ** attempt;
89
- }
90
82
 
91
83
  // ─── Main function ─────────────────────────────────────────────────────────
92
84
 
@@ -109,9 +101,8 @@ export async function fetchWithEmptyStreamRecovery(
109
101
  },
110
102
  ): Promise<EmptyStreamResult> {
111
103
  const maxReplays = deps.policy?.maxEmptyReplays ?? DEFAULT_MAX_REPLAYS;
112
- const baseDelay = deps.policy?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
113
104
  const firstEventTimeout = deps.policy?.firstEventTimeoutMs ?? DEFAULT_FIRST_EVENT_TIMEOUT_MS;
114
- const sleep = deps.policy?.sleep ?? defaultSleep;
105
+ const sleep = deps.policy?.sleep ?? abortableSleep;
115
106
 
116
107
  if (endpoints.length === 0) {
117
108
  throw new GatewayError("platform_unavailable", 502, "No endpoints available", {}, "upstream");
@@ -173,7 +164,7 @@ export async function fetchWithEmptyStreamRecovery(
173
164
 
174
165
  // Empty — replay if we haven't exhausted this endpoint's budget
175
166
  if (replay < replaysAllowed) {
176
- const delay = backoffMs(replay, baseDelay);
167
+ const delay = fallbackRetryDelayMs(replay);
177
168
  try {
178
169
  await sleep(delay, deps.signal);
179
170
  } catch (error) {
@@ -52,6 +52,37 @@ export interface GenerationHandlerDeps {
52
52
  /** Which surface a handler serves. Drives candidate filtering and error copy. */
53
53
  export type GenerationKind = Extract<ServiceKind, "imageGeneration" | "videoGeneration">;
54
54
 
55
+ /**
56
+ * Surfaces the gateway routes but does not model.
57
+ *
58
+ * Embeddings and audio have no adapter method and no media shape — there is
59
+ * nothing for this repo to translate — so they exist purely so a caller can
60
+ * reach a compatible upstream through the normal pipeline. Opening the route
61
+ * is the whole feature: routing, admission, failover, and accounting still
62
+ * apply, and the upstream's own bytes come back untouched.
63
+ */
64
+ export type PassthroughKind = Extract<ServiceKind, "embedding" | "audioTranscription" | "audioSpeech">;
65
+
66
+ /** Error-copy noun per surface, so a refusal names what the caller asked for. */
67
+ const SURFACE_NOUNS: Record<GenerationKind | PassthroughKind, string> = {
68
+ imageGeneration: "image",
69
+ videoGeneration: "video",
70
+ embedding: "embedding",
71
+ audioTranscription: "transcription",
72
+ audioSpeech: "speech",
73
+ };
74
+
75
+ /**
76
+ * Only the two media surfaces carry `prompt`/`n`.
77
+ *
78
+ * An embedding body is `{model, input}` and a transcription is multipart —
79
+ * neither has a prompt, so validating one here would reject every legitimate
80
+ * request to a surface the gateway just opened.
81
+ */
82
+ function isMediaKind(kind: GenerationKind | PassthroughKind): kind is GenerationKind {
83
+ return kind === "imageGeneration" || kind === "videoGeneration";
84
+ }
85
+
55
86
  /**
56
87
  * Maximum items one request may ask for.
57
88
  *
@@ -126,7 +157,15 @@ async function passThrough(
126
157
 
127
158
  /** Builds the handler for one generation surface. */
128
159
  export function createGenerationHandler(kind: GenerationKind, deps: GenerationHandlerDeps) {
129
- const noun = kind === "videoGeneration" ? "video" : "image";
160
+ return createNativeSurfaceHandler(kind, deps);
161
+ }
162
+
163
+ /** Builds the handler for one routed surface, media or passthrough. */
164
+ export function createNativeSurfaceHandler(
165
+ kind: GenerationKind | PassthroughKind,
166
+ deps: GenerationHandlerDeps,
167
+ ) {
168
+ const noun = SURFACE_NOUNS[kind];
130
169
  return async ({ request }: { request: Request }): Promise<Response> => {
131
170
  const state = deps.stateStore.require(request);
132
171
  const authorization = state.authorization;
@@ -151,19 +190,21 @@ export function createGenerationHandler(kind: GenerationKind, deps: GenerationHa
151
190
  }
152
191
  // Trust boundary only. Keep accepting `messages` for chat-backed providers
153
192
  // while native image/video endpoints can use the simpler `prompt` field.
154
- const hasPrompt = typeof generation.prompt === "string" && generation.prompt.trim().length > 0;
155
- const hasMessages = Array.isArray(generation.messages) && generation.messages.length > 0;
156
- if (!hasPrompt && !hasMessages) {
157
- throw new GatewayError("invalid_request", 400, "prompt is required");
158
- }
159
- if (generation.n !== undefined) {
160
- const n = generation.n;
161
- if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > MAX_ITEMS_PER_REQUEST) {
162
- throw new GatewayError(
163
- "invalid_request",
164
- 400,
165
- `n must be an integer between 1 and ${MAX_ITEMS_PER_REQUEST}`,
166
- );
193
+ if (isMediaKind(kind)) {
194
+ const hasPrompt = typeof generation.prompt === "string" && generation.prompt.trim().length > 0;
195
+ const hasMessages = Array.isArray(generation.messages) && generation.messages.length > 0;
196
+ if (!hasPrompt && !hasMessages) {
197
+ throw new GatewayError("invalid_request", 400, "prompt is required");
198
+ }
199
+ if (generation.n !== undefined) {
200
+ const n = generation.n;
201
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > MAX_ITEMS_PER_REQUEST) {
202
+ throw new GatewayError(
203
+ "invalid_request",
204
+ 400,
205
+ `n must be an integer between 1 and ${MAX_ITEMS_PER_REQUEST}`,
206
+ );
207
+ }
167
208
  }
168
209
  }
169
210
 
@@ -293,14 +334,20 @@ export function createGenerationHandler(kind: GenerationKind, deps: GenerationHa
293
334
  // body to the candidate's endpoint through the adapter, which owns the
294
335
  // upstream base URL. Either way the gateway does not interpret the
295
336
  // media — it returns what the upstream produced.
337
+ //
338
+ // A passthrough surface has no media method to prefer: the whole point
339
+ // is that this repo does not model it, so it goes straight to the
340
+ // adapter's native dispatch, which is plain forwarding.
296
341
  const media =
297
- kind === "videoGeneration"
298
- ? typeof adapter.generateVideo === "function"
299
- ? adapter.generateVideo(upstreamBody, target, mediaContext).then((outcome) => finish(outcome))
300
- : undefined
301
- : typeof adapter.generateImage === "function"
302
- ? adapter.generateImage(upstreamBody, target, mediaContext).then((outcome) => finish(outcome))
303
- : undefined;
342
+ !isMediaKind(kind)
343
+ ? undefined
344
+ : kind === "videoGeneration"
345
+ ? typeof adapter.generateVideo === "function"
346
+ ? adapter.generateVideo(upstreamBody, target, mediaContext).then((outcome) => finish(outcome))
347
+ : undefined
348
+ : typeof adapter.generateImage === "function"
349
+ ? adapter.generateImage(upstreamBody, target, mediaContext).then((outcome) => finish(outcome))
350
+ : undefined;
304
351
  const upstream =
305
352
  media !== undefined
306
353
  ? await media
@@ -348,3 +395,23 @@ export function createImageGenerationHandler(deps: GenerationHandlerDeps) {
348
395
  export function createVideoGenerationHandler(deps: GenerationHandlerDeps) {
349
396
  return createGenerationHandler("videoGeneration", deps);
350
397
  }
398
+
399
+ /**
400
+ * `POST /v1/embeddings`.
401
+ *
402
+ * Routed with no provider implementation behind it: a model row carrying the
403
+ * `embedding` kind is enough, and the adapter forwards the body as-is.
404
+ */
405
+ export function createEmbeddingHandler(deps: GenerationHandlerDeps) {
406
+ return createNativeSurfaceHandler("embedding", deps);
407
+ }
408
+
409
+ /** `POST /v1/audio/transcriptions`. Same passthrough shape as embeddings. */
410
+ export function createAudioTranscriptionHandler(deps: GenerationHandlerDeps) {
411
+ return createNativeSurfaceHandler("audioTranscription", deps);
412
+ }
413
+
414
+ /** `POST /v1/audio/speech`. Same passthrough shape as embeddings. */
415
+ export function createAudioSpeechHandler(deps: GenerationHandlerDeps) {
416
+ return createNativeSurfaceHandler("audioSpeech", deps);
417
+ }
@@ -14,6 +14,63 @@ export interface AttemptLeases {
14
14
  readonly reservation: Reservation;
15
15
  readonly proxySlot?: { poolId: string; release: () => void };
16
16
  readonly networkPoolId?: string;
17
+ /**
18
+ * Ownership of this attempt's account/network-pool release.
19
+ *
20
+ * A provider session that outlives the HTTP request — Cursor's Run, held open
21
+ * for the client's next tool-result request — takes the release over instead
22
+ * of having the request release it when the response is flushed. Request-scoped
23
+ * accounting (admission, per-request telemetry) still settles per request;
24
+ * only the *slot and pool inflight* move to the session's lifetime.
25
+ */
26
+ readonly ownership?: AttemptLeaseOwnership;
27
+ }
28
+
29
+ /**
30
+ * Request-scoped accounting versus session-scoped ownership.
31
+ *
32
+ * One HTTP request = one admission lease, one reservation, one pool slot. One
33
+ * Cursor Run = one upstream turn that can span several HTTP requests, and it
34
+ * must keep the slot for the whole turn. Transferring ownership is the only way
35
+ * to satisfy both: the request settles its own accounting and stops owning the
36
+ * release; the session owns it until the Run terminates.
37
+ *
38
+ * Idempotent by construction — the release is handed over at most once, and a
39
+ * second transfer attempt gets `undefined` so nothing is released twice.
40
+ */
41
+ export class AttemptLeaseOwnership {
42
+ #transferred = false;
43
+
44
+ constructor(private readonly release: () => void) {}
45
+
46
+ /**
47
+ * Hands the release to a provider session. Returns the release to call
48
+ * exactly once when the session terminates, or `undefined` when this attempt
49
+ * already transferred it (or released it itself).
50
+ */
51
+ transferToProviderSession(): (() => void) | undefined {
52
+ if (this.#transferred) return undefined;
53
+ this.#transferred = true;
54
+ const release = this.release;
55
+ let released = false;
56
+ return () => {
57
+ if (released) return;
58
+ released = true;
59
+ release();
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Releases only when ownership was never transferred — the ordinary
65
+ * end-of-request path. Returns false when a session owns it, so the caller
66
+ * can see that the slot outlives this request.
67
+ */
68
+ releaseByResponse(): boolean {
69
+ if (this.#transferred) return false;
70
+ this.#transferred = true;
71
+ this.release();
72
+ return true;
73
+ }
17
74
  }
18
75
 
19
76
  export interface AttemptLeaseSource {
@@ -23,6 +80,21 @@ export interface AttemptLeaseSource {
23
80
  readonly estimatedInputTokens: number;
24
81
  readonly estimatedOutputTokens: number;
25
82
  readonly authorizationSnapshot: ApiKeyAuthorizationSnapshot;
83
+ /**
84
+ * True when this request continues a provider session the gateway already
85
+ * holds, so its account and network-pool slot are already occupied by that
86
+ * session.
87
+ *
88
+ * A Cursor tool-result request borrows the held Run: it talks to the socket
89
+ * that is already open and does not open another connection. Acquiring a
90
+ * second pool slot would charge one logical upstream turn two concurrency
91
+ * slots — and, with a pool at its limit, would make the continuation fail
92
+ * with `proxy_pool_capacity_exceeded` for a turn that is already inside it.
93
+ *
94
+ * Admission (API-key authorization) and the routing reservation still run:
95
+ * per-request accounting is per request.
96
+ */
97
+ readonly borrowsHeldSession?: boolean;
26
98
  }
27
99
 
28
100
  async function acquirePoolSlot(
@@ -196,7 +268,10 @@ export async function acquireAttemptLeases(
196
268
  // address per request, so storing it here would only pin a stale answer.
197
269
  if (input.networkBindingFactory && input.host)
198
270
  await input.networkBindingFactory.resolve(input.host.hostname, input.signal, input.host.port);
199
- const slot = await acquirePoolSlot(candidate, source, input.poolSelector, input.strictPoolSelection);
271
+ // A continuation borrows the session's slot; see `borrowsHeldSession`.
272
+ const slot = source.borrowsHeldSession === true
273
+ ? undefined
274
+ : await acquirePoolSlot(candidate, source, input.poolSelector, input.strictPoolSelection);
200
275
  if (slot) partial.proxySlot = slot;
201
276
  if (input.strictPoolSelection && input.signal.aborted)
202
277
  throw new GatewayError("transport_closed", 499, "request was cancelled");
@@ -209,6 +284,14 @@ export async function acquireAttemptLeases(
209
284
  reservation: partial.reservation,
210
285
  ...(partial.proxySlot ? { proxySlot: partial.proxySlot } : {}),
211
286
  ...(partial.proxySlot ? { networkPoolId: partial.proxySlot.poolId } : {}),
287
+ // The pool slot is the one resource a provider session can outlive, so it
288
+ // is the one wrapped in a transferable owner. The admission lease and the
289
+ // routing reservation stay request-scoped: the account's concurrency slot
290
+ // is held for the Run's duration, while per-request usage settles per
291
+ // request.
292
+ ...(partial.proxySlot
293
+ ? { ownership: new AttemptLeaseOwnership(partial.proxySlot.release) }
294
+ : {}),
212
295
  };
213
296
  } catch (error) {
214
297
  await releaseAttemptLeases(partial, source.routingEngine);
@@ -221,14 +304,28 @@ export async function releaseAttemptLeases(
221
304
  readonly lease?: AdmissionLease | undefined;
222
305
  readonly reservation?: Reservation | undefined;
223
306
  readonly proxySlot?: { release: () => void } | undefined;
307
+ /**
308
+ * When present, the pool slot's release is owned rather than called
309
+ * directly: `releaseByResponse()` releases only if a provider session did
310
+ * not take it over. A slot a live Cursor Run is holding must not be freed
311
+ * at response time — the Run's next request needs it, and releasing early
312
+ * is what lets another tenant's request occupy a slot that is still in use.
313
+ */
314
+ readonly ownership?: AttemptLeaseOwnership | undefined;
224
315
  },
225
316
  routingEngine: Pick<RoutingEngine, "release">,
226
317
  ): Promise<void> {
227
318
  // Every release runs inside the settled set: a throwing pool-slot release
228
319
  // must not skip the admission lease or routing reservation (slot + inflight
229
320
  // leak). Sync release is wrapped so it participates in the same guarantee.
321
+ // When ownership exists it supersedes the raw slot release: calling both
322
+ // would release twice, and `releaseByResponse` is the single-shot gate.
230
323
  await Promise.allSettled([
231
- ...(leases.proxySlot ? [Promise.resolve().then(() => leases.proxySlot?.release())] : []),
324
+ ...(leases.ownership
325
+ ? [Promise.resolve().then(() => leases.ownership?.releaseByResponse())]
326
+ : leases.proxySlot
327
+ ? [Promise.resolve().then(() => leases.proxySlot?.release())]
328
+ : []),
232
329
  ...(leases.lease ? [leases.lease.release()] : []),
233
330
  ...(leases.reservation ? [routingEngine.release(leases.reservation)] : []),
234
331
  ]);
@@ -28,12 +28,14 @@ import {
28
28
  completionContext,
29
29
  estimatedUsage,
30
30
  parseCapturedBody,
31
- shouldCaptureProviderBody,
31
+ shouldCaptureProviderBodyForRequest,
32
32
  terminalFailure,
33
33
  } from "./attempt-finalize";
34
34
  import { runAttemptLoop } from "./attempt-loop";
35
35
  import { projectForRoute, routeCapabilitiesFor } from "../translation/capabilities";
36
36
  import { dispatchStreamingAttempt } from "./streaming/stream";
37
+ import type { ProviderContinuationScope } from "../../providers/provider-contracts";
38
+ import type { PreparedProxyRequest } from "../request/preparer";
37
39
 
38
40
  export interface ProviderProxyHandlerDeps {
39
41
  readonly db: CartethyiaDatabase;
@@ -52,6 +54,24 @@ export interface ProviderProxyHandlerDeps {
52
54
  readonly oauthRefreshService?: OAuthRefreshService;
53
55
  }
54
56
 
57
+ /**
58
+ * The continuation scope one dispatch carries.
59
+ *
60
+ * Present only for a route that has an account: a provider session held across
61
+ * requests is authorized by tenant + API key + provider + account, and a route
62
+ * with no account cannot be scoped — so it must not hold a session open at all.
63
+ */
64
+ function continuationScopeFor(prepared: PreparedProxyRequest): ProviderContinuationScope | undefined {
65
+ const accountId = prepared.candidate.provider_account_id;
66
+ if (accountId === undefined) return undefined;
67
+ return {
68
+ tenantId: prepared.authorization.tenantId,
69
+ apiKeyId: prepared.authorization.id,
70
+ providerId: prepared.candidate.provider_id,
71
+ providerAccountId: accountId,
72
+ };
73
+ }
74
+
55
75
  /**
56
76
  * First-token latency for one attempt, in the spread shape `completeAttempt`
57
77
  * takes: `{}` when there is nothing to measure — no first byte arrived (a
@@ -127,6 +147,9 @@ export async function handleProviderProxyRequest(
127
147
  estimatedInputTokens: prepared.estimatedInputTokens,
128
148
  estimatedOutputTokens: prepared.estimatedOutputTokens,
129
149
  authorizationSnapshot: prepared.authorization.snapshot,
150
+ // A continuation borrows the held Cursor Run: one upstream turn must not
151
+ // occupy two account/pool concurrency slots.
152
+ ...(prepared.continuationAffinity === undefined ? {} : { borrowsHeldSession: true }),
130
153
  },
131
154
  candidates: attemptCandidates,
132
155
  tenantId: prepared.authorization.tenantId,
@@ -175,6 +198,8 @@ export async function handleProviderProxyRequest(
175
198
  const lease = context.leases.lease;
176
199
  const reservation = context.leases.reservation;
177
200
  const proxySlot = context.leases.proxySlot;
201
+ const leaseOwnership = context.leases.ownership;
202
+ const continuationScope = continuationScopeFor(prepared);
178
203
  const providerRouteCandidate = {
179
204
  provider_id: candidate.provider_id,
180
205
  model_id: candidate.model_id,
@@ -209,13 +234,35 @@ export async function handleProviderProxyRequest(
209
234
  prepared.authorization.snapshot.tenant_id,
210
235
  ),
211
236
  providerCapture,
212
- await shouldCaptureProviderBody(deps.db, prepared.authorization.snapshot.tenant_id),
237
+ await shouldCaptureProviderBodyForRequest(
238
+ deps.db,
239
+ prepared.authorization.snapshot.tenant_id,
240
+ state,
241
+ ),
242
+ ),
243
+ }
244
+ : {}),
245
+ ...(deps.networkBindingFactory
246
+ ? {
247
+ outboundAttachmentFetch: deps.networkBindingFactory.fetch(
248
+ networkPoolId,
249
+ prepared.authorization.snapshot.tenant_id,
213
250
  ),
214
251
  }
215
252
  : {}),
216
253
  ...(deps.networkBindingFactory
217
254
  ? { outboundWebSocket: deps.networkBindingFactory.webSocket(networkPoolId, prepared.authorization.snapshot.tenant_id) }
218
255
  : {}),
256
+ ...(deps.networkBindingFactory
257
+ ? {
258
+ outboundHttp2: deps.networkBindingFactory.http2Bidi(
259
+ networkPoolId,
260
+ prepared.authorization.snapshot.tenant_id,
261
+ ),
262
+ }
263
+ : {}),
264
+ ...(leaseOwnership === undefined ? {} : { leaseOwnership }),
265
+ ...(continuationScope === undefined ? {} : { continuationScope }),
219
266
  });
220
267
  if (dispatchRequest.stream) {
221
268
  return dispatchStreamingAttempt({
@@ -237,6 +284,8 @@ export async function handleProviderProxyRequest(
237
284
  providerCapture,
238
285
  retainLeases: context.retainLeases,
239
286
  ttfbFields,
287
+ ...(leaseOwnership === undefined ? {} : { leaseOwnership }),
288
+ ...(continuationScope === undefined ? {} : { continuationScope }),
240
289
  });
241
290
  }
242
291
  // Pool slot ownership belongs to the candidate attempt: the slot was
@@ -269,6 +318,11 @@ export async function handleProviderProxyRequest(
269
318
  if (terminal === undefined) throw terminalFailure(undefined);
270
319
  const terminalError = terminalFailure(terminal);
271
320
  if (terminalError !== undefined) throw terminalError;
321
+ // An interim `tool_use` terminal defers usage: the provider's turn is not
322
+ // over, so its authoritative usage arrives with the *next* request that
323
+ // continues the same upstream turn. Charging an estimate here would bill
324
+ // the turn twice — the estimate now and the real usage later.
325
+ const deferredUsage = terminal.usage_deferred === true;
272
326
  const usage =
273
327
  terminal.usage ??
274
328
  estimatedUsage(prepared.estimatedInputTokens, prepared.estimatedOutputTokens);
@@ -316,7 +370,10 @@ export async function handleProviderProxyRequest(
316
370
  return reported === undefined ? {} : { reportedModel: reported };
317
371
  })(),
318
372
  usage: pricedUsage,
319
- commitUsage: pricedUsage,
373
+ // Deferred: the request consumed upstream resources (it is mid-turn), so
374
+ // the *recorded* usage stays for the row, but nothing is committed —
375
+ // the Run's own `turn_ended` commits once, on the final response.
376
+ ...(deferredUsage ? {} : { commitUsage: pricedUsage }),
320
377
  // Decode once: `output.bytes` is a `Uint8Array`, which
321
378
  // `parseCapturedBody` returns untouched — the Server panel would store
322
379
  // a byte-index map (`{"0":123,…}`) after redaction instead of the
@@ -26,6 +26,7 @@ import { metrics } from "../../../observability/metrics";
26
26
  import { flagPoolBlocked, flagPoolCooldown } from "../../../network/pool-health";
27
27
  import { isFreeTierIpBlock, shouldCooldownPool } from "../retry-policy";
28
28
  import { releaseAttemptLeases } from "../leases";
29
+ import type { AttemptLeaseOwnership } from "../leases";
29
30
  import { finalizeRequestTelemetry } from "../../guards/error-lifecycle";
30
31
  import { drainAbortReason } from "../../shutdown-notice";
31
32
  import {
@@ -69,6 +70,12 @@ export interface StreamFinalizeInput {
69
70
  * single pre-content Responses retry rebinds it mid-stream.
70
71
  */
71
72
  readonly getIterator: () => AsyncIterator<CanonicalEvent>;
73
+ /**
74
+ * Ownership of this attempt's account/network-pool release. A provider
75
+ * session that outlived this request already took it over, so
76
+ * `releaseByResponse()` is a no-op and the slot stays held for the session.
77
+ */
78
+ readonly leaseOwnership?: AttemptLeaseOwnership | undefined;
72
79
  /** First-token latency in the spread shape `completeAttempt` accepts. */
73
80
  readonly ttfbFields: (
74
81
  firstByteAt: number | undefined,
@@ -338,6 +345,9 @@ export class StreamFinalizer {
338
345
  lease,
339
346
  reservation,
340
347
  proxySlot,
348
+ ...(this.input.leaseOwnership === undefined
349
+ ? {}
350
+ : { ownership: this.input.leaseOwnership }),
341
351
  },
342
352
  prepared.routingEngine,
343
353
  );
@@ -392,6 +402,11 @@ export class StreamFinalizer {
392
402
  candidate.providerId,
393
403
  candidate.modelId,
394
404
  );
405
+ // A deferred terminal means the provider's turn is not over: the stream
406
+ // ended because the model asked for a tool the client must answer, and the
407
+ // authoritative usage arrives with the request that continues the same
408
+ // upstream turn. Committing an estimate here would bill that turn twice.
409
+ const deferredUsage = pipeline.terminal.usage_deferred === true;
395
410
  await completeAttempt(state, {
396
411
  status: upstreamTruncated ? "truncated" : "completed",
397
412
  ...this.streamCompletionContext(),
@@ -404,7 +419,7 @@ export class StreamFinalizer {
404
419
  }
405
420
  : {}),
406
421
  usage: finalUsage,
407
- commitUsage: finalUsage,
422
+ ...(deferredUsage ? {} : { commitUsage: finalUsage }),
408
423
  responseBody: pipeline.telemetryEventTranscript,
409
424
  ...(pipeline.clientResponseText ? { clientResponseText: pipeline.clientResponseText } : {}),
410
425
  ...(pipeline.firstContentDeltaAtMs !== undefined ? { firstContentDeltaAtMs: pipeline.firstContentDeltaAtMs } : {}),
@@ -43,8 +43,10 @@ import type { AdmissionLease } from "../../../security/admission/contracts";
43
43
  import type { NetworkPoolSelector } from "../../../network/pool/selector";
44
44
  import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
45
45
  import type { ProviderExchangeCapture } from "../attempt-finalize";
46
- import { captureProviderExchange, shouldCaptureProviderBody } from "../attempt-finalize";
46
+ import { captureProviderExchange, shouldCaptureProviderBodyForRequest } from "../attempt-finalize";
47
47
  import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
48
+ import type { AttemptLeaseOwnership } from "../leases";
49
+ import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
48
50
  import { resolveStreamFirstChunkTimeoutMs, resolveStreamStallTimeoutMs, resolveUpstreamTimeoutMs } from "../../../config/timeouts";
49
51
  import type { ProxyRequestState } from "../../request/state";
50
52
  import type { PreparedProxyRequest } from "../../request/preparer";
@@ -81,6 +83,10 @@ export interface StreamingDispatchInput {
81
83
  readonly proxySlot: { poolId: string; release: () => void } | undefined;
82
84
  readonly networkPoolId: string | undefined;
83
85
  readonly providerCapture: ProviderExchangeCapture;
86
+ /** Ownership of this attempt's account/network-pool release, when one was acquired. */
87
+ readonly leaseOwnership?: AttemptLeaseOwnership | undefined;
88
+ /** Caller identity authorizing a provider session held across requests. */
89
+ readonly continuationScope?: ProviderContinuationScope | undefined;
84
90
  /** Hands lease ownership to the returned `Response`'s async lifetime. */
85
91
  retainLeases: () => void;
86
92
  /** First-token latency in the spread shape `completeAttempt` accepts. */
@@ -151,6 +157,8 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
151
157
  providerCapture,
152
158
  retainLeases,
153
159
  ttfbFields,
160
+ leaseOwnership,
161
+ continuationScope,
154
162
  } = input;
155
163
 
156
164
  const outboundFetch = deps.networkBindingFactory?.fetch(
@@ -161,7 +169,7 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
161
169
  ? captureProviderExchange(
162
170
  outboundFetch,
163
171
  providerCapture,
164
- await shouldCaptureProviderBody(deps.db, prepared.authorization.snapshot.tenant_id),
172
+ await shouldCaptureProviderBodyForRequest(deps.db, prepared.authorization.snapshot.tenant_id, state),
165
173
  )
166
174
  : undefined;
167
175
  const dispatchContext = buildUpstreamDispatchContext({
@@ -174,6 +182,7 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
174
182
  ...(conversationAffinity ? { conversationAffinity } : {}),
175
183
  ...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
176
184
  ...(wrappedOutboundFetch ? { outboundFetch: wrappedOutboundFetch } : {}),
185
+ ...(outboundFetch ? { outboundAttachmentFetch: outboundFetch } : {}),
177
186
  ...(deps.networkBindingFactory
178
187
  ? {
179
188
  outboundWebSocket: deps.networkBindingFactory.webSocket(
@@ -182,6 +191,16 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
182
191
  ),
183
192
  }
184
193
  : {}),
194
+ ...(deps.networkBindingFactory
195
+ ? {
196
+ outboundHttp2: deps.networkBindingFactory.http2Bidi(
197
+ networkPoolId,
198
+ prepared.authorization.snapshot.tenant_id,
199
+ ),
200
+ }
201
+ : {}),
202
+ ...(leaseOwnership === undefined ? {} : { leaseOwnership }),
203
+ ...(continuationScope === undefined ? {} : { continuationScope }),
185
204
  });
186
205
  // `ProviderAdapter.dispatch` is declared to return a non-nullable
187
206
  // `AsyncIterable`, so an adapter that returned `undefined` would be a
@@ -248,6 +267,7 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
248
267
  providerCapture,
249
268
  getIterator: () => iterator,
250
269
  ttfbFields,
270
+ ...(leaseOwnership === undefined ? {} : { leaseOwnership }),
251
271
  },
252
272
  pipeline,
253
273
  {
@@ -1,5 +1,6 @@
1
1
  import type { CanonicalEvent } from "../../protocol/canonical-model";
2
- import type { ValidatedOutboundFetch, ValidatedOutboundWebSocket } from "../../providers/provider-contracts";
2
+ import type { AttemptLeaseOwnership, ValidatedOutboundFetch, ValidatedOutboundHttp2Bidi, ValidatedOutboundWebSocket } from "../../providers/provider-contracts";
3
+ import type { ProviderContinuationScope } from "../../providers/provider-contracts";
3
4
  import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
4
5
 
5
6
  const FORWARDED_REQUEST_HEADERS = new Set([
@@ -41,7 +42,18 @@ export function buildUpstreamDispatchContext(input: {
41
42
  readonly requestId?: string;
42
43
  readonly userAgent?: string;
43
44
  readonly outboundFetch?: ValidatedOutboundFetch;
45
+ /** Validated attachment egress that must not be wrapped by provider payload capture. */
46
+ readonly outboundAttachmentFetch?: ValidatedOutboundFetch;
44
47
  readonly outboundWebSocket?: ValidatedOutboundWebSocket;
48
+ /** Validated bidirectional HTTP/2 egress bound to the same selected pool. */
49
+ readonly outboundHttp2?: ValidatedOutboundHttp2Bidi;
50
+ /**
51
+ * Ownership of this attempt's account/network-pool lease; a provider session
52
+ * that outlives the request takes it over.
53
+ */
54
+ readonly leaseOwnership?: AttemptLeaseOwnership;
55
+ /** Caller identity authorizing a provider session held across requests. */
56
+ readonly continuationScope?: ProviderContinuationScope;
45
57
  /** Stable per-conversation cache affinity (preferred over random ids). */
46
58
  readonly conversationAffinity?: string;
47
59
  /** Opaque identity shared by retries of one logical gateway request. */
@@ -74,7 +86,13 @@ export function buildUpstreamDispatchContext(input: {
74
86
  ...(userAgent ? { user_agent: userAgent } : {}),
75
87
  ...(Object.keys(input.headers).length > 0 ? { request_headers: input.headers } : {}),
76
88
  ...(outboundFetch ? { outbound_fetch: outboundFetch } : {}),
89
+ ...(input.outboundAttachmentFetch
90
+ ? { outbound_attachment_fetch: input.outboundAttachmentFetch }
91
+ : {}),
77
92
  ...(input.outboundWebSocket ? { outbound_websocket: input.outboundWebSocket } : {}),
93
+ ...(input.outboundHttp2 ? { outbound_http2: input.outboundHttp2 } : {}),
94
+ ...(input.leaseOwnership ? { lease_ownership: input.leaseOwnership } : {}),
95
+ ...(input.continuationScope ? { continuation_scope: input.continuationScope } : {}),
78
96
  ...(input.conversationAffinity ? { conversation_affinity: input.conversationAffinity } : {}),
79
97
  ...(input.requestIdentity ? { request_identity: input.requestIdentity } : {}),
80
98
  } as Record<string, unknown>;