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
@@ -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>;
@@ -1,3 +1,4 @@
1
+ import { log } from "../../observability/logger";
1
2
  // API-key authentication guard for `/v1/*`: token resolution, scope check,
2
3
  // client-router denylist, and the model-abuse ban gate.
3
4
  import { Elysia } from "elysia";
@@ -39,7 +40,31 @@ export function createApiKeyAuthenticationMiddleware(deps: {
39
40
  const pathname = fastPathname(request.url);
40
41
  if (!pathname.startsWith("/v1/")) return;
41
42
  const token = requestToken(request.headers);
42
- const authorization = await resolveApiKeyAuthorization(deps.db, token);
43
+ // A database failure here is not "the key is bad". Left to propagate it
44
+ // surfaced as a 500 whose log line carried the whole statement and its
45
+ // bound parameters — including the key hash — once per request. Under an
46
+ // overloaded database that is a credential leak written at request rate.
47
+ //
48
+ // It is also the wrong answer: the caller cannot fix it by sending a
49
+ // different key, so it is a capacity signal, not an auth one.
50
+ const authorization = await resolveApiKeyAuthorization(deps.db, token).catch((error: unknown) => {
51
+ // Only the error's identity is logged, never its message: a drizzle
52
+ // query error carries the full statement and its bound parameters, and
53
+ // on this path one of those parameters is the key hash.
54
+ // A sanitized stand-in, not the original: a drizzle query error's
55
+ // message is the statement plus its bound parameters, and one of those
56
+ // parameters is the key hash.
57
+ const safe = new Error("[redacted: credential-store query failed]");
58
+ safe.name = error instanceof Error ? error.name : "Error";
59
+ log.error("[auth] key lookup failed; treating as unavailable", safe);
60
+ throw new GatewayError(
61
+ "accounts_unavailable",
62
+ 503,
63
+ "The gateway could not verify this API key because its credential store is unavailable. Retry shortly.",
64
+ {},
65
+ "cartethyia",
66
+ );
67
+ });
43
68
  if (!authorization)
44
69
  throw new GatewayError("invalid_request", 401, "This API key is unknown, revoked, or expired. Issue a new key and send it as: Authorization: Bearer <key>.");
45
70
  const decision = createAccessDecision({
@@ -17,10 +17,11 @@ export function isProxyRequest(request: Request): boolean {
17
17
  * This list must mirror the routes `app.ts` actually mounts. It drives
18
18
  * body-read policy — a JSON route rejects a missing or non-JSON content-type
19
19
  * with 415, while an unrouted path is not read at all — so an entry with no
20
- * handler behind it describes a route the gateway does not serve. Two such
21
- * entries (`/v1/embeddings`, `/v1/audio/speech`) sit outside this list: they
22
- * advertise a surface that 404s, and the route-table test pins them absent so
23
- * re-adding one is a deliberate act.
20
+ * handler behind it describes a route the gateway does not serve. Three such
21
+ * entries (`/v1/embeddings`, `/v1/audio/transcriptions`, `/v1/audio/speech`)
22
+ * were listed for a while with nothing mounted — they advertised a surface
23
+ * that 404d — and the route-table test pinned them absent so re-adding one was
24
+ * a deliberate act. They are mounted now, as passthrough surfaces.
24
25
  *
25
26
  * The native service routes (`NATIVE_SERVICE_PATHS` — System One, web search,
26
27
  * image and video generation) are included: they read a JSON body through the