cartethyia 2.6.7 → 2.7.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 (335) hide show
  1. package/.env.example +1 -7
  2. package/README.md +25 -2
  3. package/dist/dashboard/assets/{BannedPage-Dskhyp0l.js → BannedPage-D-J75hBM.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage--SJhk7qQ.js → CliToolDetailPage-CVe6RaBH.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-C203VT-c.js → CliToolsPage-Dd8ZSamp.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BcS7kGG1.js → CombosPage-QoJ6foxo.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-uujbBYwC.js → CustomizationPage-CKUFTcW4.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
  17. package/dist/dashboard/assets/LoginPage-DubdQcvX.js +1 -0
  18. package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-CA1kLs45.js → ModelPicker-BH8Tehdi.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +4 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
  26. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +9 -0
  30. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
  31. package/dist/dashboard/assets/{QuotaPage-BueSWX5D.js → QuotaPage-ATpaxrbl.js} +1 -1
  32. package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
  33. package/dist/dashboard/assets/SettingsPage-CETophpx.js +1 -0
  34. package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-BdgECjqq.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
  37. package/dist/dashboard/assets/{SortableList-DrKeMcGN.js → SortableList-CAK-EN9z.js} +1 -1
  38. package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-qjuYtccc.js → StudioPage-B7W2_RhT.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
  43. package/dist/dashboard/assets/{api-keys-C23S9sgp.js → api-keys-CUi6UXsR.js} +1 -1
  44. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  45. package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
  46. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-DU6qrMA_.js → assert-67xXH5rf.js} +1 -1
  48. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-Bkz_-RrM.js → badge-DC2L43Mj.js} +1 -1
  50. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-C-7y3VKV.js → banner-BBVcb0J3.js} +1 -1
  52. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-BOPBuBzD.js → card-Cca1XC6G.js} +1 -1
  54. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-BaFUuFFF.js → cli-tools-KvsWa1Jl.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-e-Mlk0FH.js → clipboard-button-DUzEbynq.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-Iec_ywbD.js → dialog-DFvWhwTa.js} +1 -1
  60. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-Bs8MIIHc.js → drawer-CFM2sMX1.js} +1 -1
  62. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-DOddwoc0.js → entry-BLBLxX0h.js} +3 -3
  64. package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
  65. package/dist/dashboard/assets/{entry-DcobaKFu.js → entry-Bqy8RFvM.js} +1 -1
  66. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  67. package/dist/dashboard/assets/{entry-BRszm4_Z.js → entry-_xHo5x6r.js} +1 -1
  68. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  69. package/dist/dashboard/assets/formatters-mpBPqAQ8.js +1 -0
  70. package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-BuDsrsPn.js → github-badge-C9e8Ts7e.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BeLRhsmt.js → index-D6QzEC_n.js} +2 -2
  74. package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-DToMICwl.js → inline-BFUcvllj.js} +1 -1
  76. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-WlyHWeG2.js → input-C_ILAvBb.js} +1 -1
  78. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  79. package/dist/dashboard/assets/{layout-DaJ5baJr.js → layout-CBIyT6Rt.js} +1 -1
  80. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-B3oSK13z.js → list-row-ebJ_-Nsb.js} +1 -1
  82. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-ta6GMb-8.js → live-D0mm--ME.js} +1 -1
  84. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  85. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +1 -0
  86. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-g_fduiNw.js → mutation-sMD965oO.js} +1 -1
  88. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  89. package/dist/dashboard/assets/{numbers-Dulqgys5.js → numbers-BO9dKaFr.js} +1 -1
  90. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  91. package/dist/dashboard/assets/providers/inception.svg +5 -0
  92. package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
  93. package/dist/dashboard/assets/{query-keys-UETqfytp.js → query-keys-CfL_Zdoh.js} +1 -1
  94. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  95. package/dist/dashboard/assets/{routing-BPUo-W8d.js → routing-BnIpi1IJ.js} +1 -1
  96. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  97. package/dist/dashboard/assets/{select-BfZ3SCTM.js → select-Cj5Nw43g.js} +1 -1
  98. package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
  99. package/dist/dashboard/assets/settings-BAFhDgX8.js +1 -0
  100. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  101. package/dist/dashboard/assets/{stack-B1w5BM0u.js → stack-EMrOMt4I.js} +1 -1
  102. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  103. package/dist/dashboard/assets/{state-DhZBfWgx.js → state-CWoo7ty5.js} +1 -1
  104. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  105. package/dist/dashboard/assets/{switch-yD7D99Lg.js → switch-CCjT6IkF.js} +1 -1
  106. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-EDFRkn1i.js → useMutation-DjkLHUcE.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +55 -54
  112. package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
  113. package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
  114. package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
  115. package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
  116. package/package.json +1 -1
  117. package/scripts/cli/console-menu.ts +5 -2
  118. package/scripts/cli/tray.ts +21 -6
  119. package/src/boot/app.ts +24 -62
  120. package/src/boot/dependencies.ts +732 -750
  121. package/src/config/cooldowns.ts +16 -0
  122. package/src/config/providers.ts +13 -1
  123. package/src/config/resources.ts +13 -80
  124. package/src/config/security.ts +2 -38
  125. package/src/config/spec.ts +16 -23
  126. package/src/console/console-router.ts +7 -7
  127. package/src/console/domain-registration.ts +19 -12
  128. package/src/console/providers/catalog/catalog-projections.ts +2 -6
  129. package/src/console/providers/catalog/contracts.ts +19 -0
  130. package/src/console/providers/catalog/provider-operations.ts +38 -0
  131. package/src/console/providers/catalog/routes.ts +14 -0
  132. package/src/console/providers/catalog/store-helpers.ts +326 -0
  133. package/src/console/providers/catalog/store.ts +1292 -1563
  134. package/src/console/quota/account-quota-global.ts +2 -2
  135. package/src/console/quota/account-quota-shared.ts +1 -1
  136. package/src/console/quota/account-quota-tenant.ts +35 -12
  137. package/src/console/quota/quota-view.ts +2 -2
  138. package/src/console/remote-support/contracts.ts +3 -1
  139. package/src/console/remote-support/service.ts +115 -6
  140. package/src/console/routing/pools/contracts.ts +41 -1
  141. package/src/console/routing/pools/operations.ts +183 -4
  142. package/src/console/routing/pools/relay-deploy.ts +17 -4
  143. package/src/console/routing/pools/routes.ts +30 -0
  144. package/src/console/routing/pools/store.ts +98 -8
  145. package/src/main.ts +16 -2
  146. package/src/network/outbound-fetch.ts +226 -365
  147. package/src/network/pool/agent.ts +8 -0
  148. package/src/network/pool/pool-state.ts +7 -0
  149. package/src/network/pool/selector.ts +110 -2
  150. package/src/network/pool-cooldown-store.ts +252 -0
  151. package/src/network/pool-health-machine.ts +5 -3
  152. package/src/network/pool-health.ts +36 -9
  153. package/src/observability/event-query.ts +57 -0
  154. package/src/observability/metrics.ts +15 -5
  155. package/src/observability/runtime-metrics.ts +30 -15
  156. package/src/persistence/health-event-flush.ts +2 -0
  157. package/src/persistence/schema.ts +19 -0
  158. package/src/persistence/usage-flush.ts +8 -3
  159. package/src/protocol/canonical-model.ts +1 -1
  160. package/src/protocol/dsml.ts +377 -0
  161. package/src/protocol/gateway-error.ts +0 -1
  162. package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
  163. package/src/protocol/request/chat.ts +7 -7
  164. package/src/protocol/request/codex.ts +2 -2
  165. package/src/protocol/request/gemini.ts +1 -1
  166. package/src/protocol/request/messages.ts +6 -6
  167. package/src/protocol/request/responses.ts +7 -7
  168. package/src/protocol/response/dsml.ts +12 -133
  169. package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
  170. package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
  171. package/src/{transport → protocol}/translation/capabilities.ts +2 -2
  172. package/src/{transport → protocol}/translation/normalize.ts +1 -1
  173. package/src/{transport → protocol}/translation/quirks.ts +1 -1
  174. package/src/{transport → protocol}/translation/search.ts +1 -1
  175. package/src/{transport → protocol}/translation/thinking.ts +23 -1
  176. package/src/{transport → protocol}/translation/tool-id.ts +1 -1
  177. package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
  178. package/src/protocol/upstream-parsing.ts +176 -0
  179. package/src/providers/adapters/compatible-adapter.ts +11 -2
  180. package/src/providers/authentication/oauth-refresh-service.ts +9 -5
  181. package/src/providers/default-registry.ts +21 -5
  182. package/src/providers/discovery/probe-phases.ts +16 -3
  183. package/src/providers/discovery/probing-service.ts +4 -1
  184. package/src/providers/integrations/anthropic.ts +1 -1
  185. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
  186. package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
  187. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
  188. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
  189. package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
  190. package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
  191. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
  192. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
  193. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
  194. package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
  195. package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
  196. package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
  197. package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
  198. package/src/providers/integrations/claude/claude.ts +1 -1
  199. package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
  200. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  201. package/src/providers/integrations/codex/codex.ts +1 -1
  202. package/src/providers/integrations/commandcode/commandcode.ts +134 -104
  203. package/src/providers/integrations/commandcode/discovery.ts +6 -5
  204. package/src/providers/integrations/factory/factory.ts +1 -1
  205. package/src/providers/integrations/github/github.ts +1 -1
  206. package/src/providers/integrations/grok/grok.ts +1 -1
  207. package/src/providers/integrations/inception/provider.ts +91 -0
  208. package/src/providers/integrations/inferhub.ts +1 -1
  209. package/src/providers/integrations/kiro/kiro-request.ts +1 -1
  210. package/src/providers/integrations/mistral.ts +193 -19
  211. package/src/providers/integrations/opencode/fingerprint.ts +19 -2
  212. package/src/providers/integrations/opencode/prompt.ts +111 -0
  213. package/src/providers/integrations/opencode/provider.ts +101 -23
  214. package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
  215. package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
  216. package/src/providers/integrations/qoder/qoder.ts +1641 -884
  217. package/src/providers/integrations/shared/install-id.ts +2 -1
  218. package/src/providers/integrations/trae/trae-cache.ts +110 -0
  219. package/src/providers/integrations/trae/trae-oauth.ts +29 -4
  220. package/src/providers/integrations/trae/trae-protocol.ts +2 -1
  221. package/src/providers/integrations/trae/trae.ts +106 -23
  222. package/src/providers/operations/account-error-classification.ts +1660 -0
  223. package/src/providers/operations/account-health-service.ts +621 -2097
  224. package/src/providers/operations/account-reset-service.ts +2 -0
  225. package/src/providers/provider-contracts.ts +0 -2
  226. package/src/providers/provider-metadata.ts +25 -4
  227. package/src/providers/provider-probe.ts +2 -2
  228. package/src/{console → providers}/quota/cache.ts +1 -1
  229. package/src/{console → providers}/quota/refresh.ts +5 -5
  230. package/src/security/abuse.ts +17 -0
  231. package/src/transport/dispatch/attempt-finalize.ts +114 -12
  232. package/src/transport/dispatch/attempt-loop.ts +3 -0
  233. package/src/transport/dispatch/empty-stream.ts +224 -217
  234. package/src/transport/dispatch/generation.ts +37 -55
  235. package/src/transport/dispatch/handler-deps.ts +32 -0
  236. package/src/transport/dispatch/leases.ts +9 -4
  237. package/src/transport/dispatch/proxy-request.ts +6 -30
  238. package/src/transport/dispatch/responses-compact.ts +22 -43
  239. package/src/transport/dispatch/streaming/finalize.ts +3 -0
  240. package/src/transport/dispatch/streaming/stream.ts +0 -8
  241. package/src/transport/dispatch/systemone.ts +22 -42
  242. package/src/transport/dispatch/token-count.ts +23 -39
  243. package/src/transport/dispatch/upstream.ts +1 -3
  244. package/src/transport/dispatch/websearch.ts +22 -41
  245. package/src/transport/failure-policy.ts +59 -178
  246. package/src/transport/guards/abuse-guard.ts +10 -0
  247. package/src/transport/guards/auth-guard.ts +0 -18
  248. package/src/transport/guards/error-lifecycle.ts +1 -1
  249. package/src/transport/guards/pipeline.ts +0 -6
  250. package/src/transport/guards/request-context.ts +18 -91
  251. package/src/transport/request/degradation.ts +20 -1
  252. package/src/transport/request/inflight.ts +4 -95
  253. package/src/transport/request/preparer.ts +6 -6
  254. package/src/transport/request/state.ts +66 -38
  255. package/src/transport/request/tenant-preferences.ts +1 -1
  256. package/src/transport/routing/route-catalog.ts +41 -31
  257. package/src/transport/routing/router.ts +2 -2
  258. package/src/transport/surface/messages/parse.ts +1 -1
  259. package/src/workers/buddy-growth-tasks.ts +346 -425
  260. package/src/workers/daily-checkin.ts +17 -6
  261. package/src/workers/quota-refresh-sweep.ts +2 -2
  262. package/src/workers/scheduler.ts +17 -1
  263. package/dist/dashboard/assets/BannedPage-Dskhyp0l.js.gz +0 -0
  264. package/dist/dashboard/assets/CliToolDetailPage--SJhk7qQ.js.gz +0 -0
  265. package/dist/dashboard/assets/CliToolsPage-C203VT-c.js.gz +0 -0
  266. package/dist/dashboard/assets/CombosPage-BcS7kGG1.js.gz +0 -0
  267. package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js +0 -1
  268. package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js.gz +0 -0
  269. package/dist/dashboard/assets/CustomizationPage-uujbBYwC.js.gz +0 -0
  270. package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js +0 -1
  271. package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js.gz +0 -0
  272. package/dist/dashboard/assets/LoginPage-DoOUthbG.js +0 -1
  273. package/dist/dashboard/assets/LoginPage-DoOUthbG.js.gz +0 -0
  274. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  275. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js +0 -1
  276. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js.gz +0 -0
  277. package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js +0 -3
  278. package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js.gz +0 -0
  279. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
  280. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  281. package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js +0 -1
  282. package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js.gz +0 -0
  283. package/dist/dashboard/assets/ProxyPage-B9mBTweh.js +0 -7
  284. package/dist/dashboard/assets/ProxyPage-B9mBTweh.js.gz +0 -0
  285. package/dist/dashboard/assets/QuotaPage-BueSWX5D.js.gz +0 -0
  286. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js +0 -1
  287. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js.gz +0 -0
  288. package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js +0 -1
  289. package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js.gz +0 -0
  290. package/dist/dashboard/assets/SortableList-DrKeMcGN.js.gz +0 -0
  291. package/dist/dashboard/assets/StudioPage-qjuYtccc.js.gz +0 -0
  292. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js +0 -56
  293. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js.gz +0 -0
  294. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  295. package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
  296. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  297. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  298. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  299. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  300. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  301. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  302. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  303. package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
  304. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  305. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  306. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  307. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  308. package/dist/dashboard/assets/entry-DOddwoc0.js.gz +0 -0
  309. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  310. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  311. package/dist/dashboard/assets/index-BeLRhsmt.js.gz +0 -0
  312. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  313. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  314. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  315. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  316. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  317. package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
  318. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  319. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  320. package/dist/dashboard/assets/numbers-Dulqgys5.js.gz +0 -0
  321. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  322. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  323. package/dist/dashboard/assets/select-BfZ3SCTM.js.gz +0 -0
  324. package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
  325. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  326. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  327. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  328. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  329. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  330. package/src/console/admin/model-abuse/contracts.ts +0 -38
  331. package/src/console/admin/model-abuse/routes.ts +0 -97
  332. package/src/security/model-abuse.ts +0 -704
  333. package/src/transport/guards/concurrency-guard.ts +0 -125
  334. /package/dist/dashboard/assets/{app-MdVfwraB.js → app-BaWd0puM.js} +0 -0
  335. /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-BaWd0puM.js.gz} +0 -0
@@ -1,217 +1,224 @@
1
- /**
2
- * Empty-stream replay + pre-start failover.
3
- *
4
- * When a provider returns HTTP 200 but the response body decodes to zero
5
- * meaningful events, the request is replayed byte-identical (same headers,
6
- * same body) up to `maxEmptyReplays` times with exponential backoff. If all
7
- * replays on one endpoint fail, the helper advances to the next endpoint in
8
- * the caller-supplied ordered list.
9
- *
10
- * The helper is transport-layer: it owns fetch, watchdog, backoff, and
11
- * endpoint rotation. The caller owns SSE decoding via the `DrainStream`
12
- * callback and supplies materialized headers + serialized body so replays
13
- * are byte-identical.
14
- *
15
- * Design: the replay count, backoff, and the ordered endpoint list are the
16
- * caller's contract; this helper owns fetch, watchdog, and rotation so a
17
- * provider that answers 200 with no events is retried on the next endpoint
18
- * rather than surfacing an empty body to the client.
19
- */
20
- import { GatewayError } from "../../protocol/gateway-error";
21
- import {
22
- fallbackRetryDelayMs,
23
- MIN_RETRY_BUDGET_MS,
24
- sleep as abortableSleep,
25
- } from "../failure-policy";
26
-
27
- // ─── Public types ──────────────────────────────────────────────────────────
28
-
29
- /** What the caller's drain observed. Counts only dispatched data events:
30
- * comments/keepalives, `[DONE]`, and whitespace-only data are excluded. */
31
- export interface StreamTally {
32
- readonly dataEvents: number;
33
- readonly meaningful: boolean;
34
- readonly thoughtOnly: boolean;
35
- }
36
-
37
- /** Drain one response body to a tally. Provider-supplied; uses decodeSseEvents. */
38
- export type DrainStream = (response: Response, signal: AbortSignal) => Promise<StreamTally>;
39
-
40
- /** Bound fetch for one endpoint. Receives materialized headers + serialized body. */
41
- export type EmptyStreamFetch = (
42
- url: string,
43
- init: { headers: Record<string, string>; body: string; signal: AbortSignal },
44
- ) => Promise<Response>;
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
- */
55
- export interface EmptyStreamPolicy {
56
- readonly maxEmptyReplays?: number;
57
- readonly firstEventTimeoutMs?: number;
58
- readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
59
- }
60
-
61
- export interface EmptyStreamResult {
62
- readonly response: Response;
63
- readonly tally: StreamTally;
64
- readonly endpoint: string;
65
- readonly replaysUsed: number;
66
- }
67
-
68
- // ─── Defaults ──────────────────────────────────────────────────────────────
69
-
70
- const DEFAULT_MAX_REPLAYS = 2;
71
- const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 200_000; // 200s, matches CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS default
72
-
73
-
74
- // ─── Helpers ───────────────────────────────────────────────────────────────
75
-
76
- /** Pure predicate: zero data events, or data events with nothing meaningful. */
77
- export function isEmptyStream(tally: StreamTally): boolean {
78
- return tally.dataEvents === 0 || (!tally.meaningful && !tally.thoughtOnly);
79
- }
80
-
81
-
82
-
83
- // ─── Main function ─────────────────────────────────────────────────────────
84
-
85
- /**
86
- * Replays the byte-identical request while responses drain empty,
87
- * failing over across ordered endpoints pre-start.
88
- *
89
- * Throws GatewayError (platform_unavailable, 502, upstream) on exhaustion,
90
- * GatewayError (transport_closed, 499) on abort.
91
- */
92
- export async function fetchWithEmptyStreamRecovery(
93
- endpoints: readonly string[],
94
- request: { headers: Record<string, string>; body: string },
95
- deps: {
96
- fetch: EmptyStreamFetch;
97
- drain: DrainStream;
98
- signal: AbortSignal;
99
- deadlineMs: number;
100
- policy?: EmptyStreamPolicy;
101
- },
102
- ): Promise<EmptyStreamResult> {
103
- const maxReplays = deps.policy?.maxEmptyReplays ?? DEFAULT_MAX_REPLAYS;
104
- const firstEventTimeout = deps.policy?.firstEventTimeoutMs ?? DEFAULT_FIRST_EVENT_TIMEOUT_MS;
105
- const sleep = deps.policy?.sleep ?? abortableSleep;
106
-
107
- if (endpoints.length === 0) {
108
- throw new GatewayError("platform_unavailable", 502, "No endpoints available", {}, "upstream");
109
- }
110
-
111
- for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex++) {
112
- const endpoint = endpoints[endpointIndex]!;
113
- const isLastEndpoint = endpointIndex === endpoints.length - 1;
114
- const replaysAllowed = isLastEndpoint ? maxReplays : maxReplays;
115
-
116
- for (let replay = 0; replay <= replaysAllowed; replay++) {
117
- // Check abort before each fetch
118
- if (deps.signal.aborted) {
119
- throw new GatewayError("transport_closed", 499, "Aborted");
120
- }
121
-
122
- // Check deadline floor
123
- const now = Date.now();
124
- const remaining = deps.deadlineMs - now;
125
- if (remaining < MIN_RETRY_BUDGET_MS) {
126
- throw new GatewayError(
127
- "platform_unavailable",
128
- 502,
129
- "Deadline exhausted before empty-stream replay",
130
- {},
131
- "upstream",
132
- );
133
- }
134
-
135
- const fetchTimeout = Math.min(firstEventTimeout, remaining);
136
-
137
- let response: Response;
138
- try {
139
- response = await fetchWithTimeout(deps.fetch, endpoint, request, deps.signal, fetchTimeout);
140
- } catch (error) {
141
- if (error instanceof GatewayError) throw error;
142
- break; // advance to next endpoint
143
- }
144
-
145
- // Drain the response
146
- let tally: StreamTally;
147
- try {
148
- tally = await deps.drain(response, deps.signal);
149
- } catch (error) {
150
- if (error instanceof GatewayError) throw error;
151
- break; // advance to next endpoint
152
- }
153
-
154
- // Check if empty
155
- if (!isEmptyStream(tally)) {
156
- // Success — return the response and tally
157
- return {
158
- response,
159
- tally,
160
- endpoint,
161
- replaysUsed: replay,
162
- };
163
- }
164
-
165
- // Empty — replay if we haven't exhausted this endpoint's budget
166
- if (replay < replaysAllowed) {
167
- const delay = fallbackRetryDelayMs(replay);
168
- try {
169
- await sleep(delay, deps.signal);
170
- } catch (error) {
171
- if (error instanceof GatewayError) throw error;
172
- throw error;
173
- }
174
- }
175
- }
176
- }
177
-
178
- // All endpoints exhausted
179
- throw new GatewayError(
180
- "platform_unavailable",
181
- 502,
182
- "All endpoints returned empty streams",
183
- {},
184
- "upstream",
185
- );
186
- }
187
-
188
- // ─── Internal helpers ──────────────────────────────────────────────────────
189
-
190
- async function fetchWithTimeout(
191
- fetch: EmptyStreamFetch,
192
- url: string,
193
- request: { headers: Record<string, string>; body: string },
194
- signal: AbortSignal,
195
- timeoutMs: number,
196
- ): Promise<Response> {
197
- // Race the fetch against a first-event timeout
198
- const timeoutPromise = new Promise<never>((_, reject) => {
199
- const timer = setTimeout(() => {
200
- reject(new GatewayError("transport_closed", 499, "First event timeout", {}, "upstream"));
201
- }, timeoutMs);
202
- signal.addEventListener("abort", () => {
203
- clearTimeout(timer);
204
- reject(new GatewayError("transport_closed", 499, "Aborted"));
205
- }, { once: true });
206
- });
207
-
208
- try {
209
- return await Promise.race([
210
- fetch(url, { headers: request.headers, body: request.body, signal }),
211
- timeoutPromise,
212
- ]);
213
- } catch (error) {
214
- if (error instanceof GatewayError) throw error;
215
- throw error;
216
- }
217
- }
1
+ /**
2
+ * Empty-stream replay + pre-start failover.
3
+ *
4
+ * When a provider returns HTTP 200 but the response body decodes to zero
5
+ * meaningful events, the request is replayed byte-identical (same headers,
6
+ * same body) up to `maxEmptyReplays` times with exponential backoff. If all
7
+ * replays on one endpoint fail, the helper advances to the next endpoint in
8
+ * the caller-supplied ordered list.
9
+ *
10
+ * The helper is transport-layer: it owns fetch, watchdog, backoff, and
11
+ * endpoint rotation. The caller owns SSE decoding via the `DrainStream`
12
+ * callback and supplies materialized headers + serialized body so replays
13
+ * are byte-identical.
14
+ *
15
+ * Design: the replay count, backoff, and the ordered endpoint list are the
16
+ * caller's contract; this helper owns fetch, watchdog, and rotation so a
17
+ * provider that answers 200 with no events is retried on the next endpoint
18
+ * rather than surfacing an empty body to the client.
19
+ */
20
+ import { GatewayError } from "../../protocol/gateway-error";
21
+ import {
22
+ fallbackRetryDelayMs,
23
+ MIN_RETRY_BUDGET_MS,
24
+ sleep as abortableSleep,
25
+ } from "../failure-policy";
26
+
27
+ // ─── Public types ──────────────────────────────────────────────────────────
28
+
29
+ /** What the caller's drain observed. Counts only dispatched data events:
30
+ * comments/keepalives, `[DONE]`, and whitespace-only data are excluded. */
31
+ export interface StreamTally {
32
+ readonly dataEvents: number;
33
+ readonly meaningful: boolean;
34
+ readonly thoughtOnly: boolean;
35
+ }
36
+
37
+ /** Drain one response body to a tally. Provider-supplied; uses decodeSseEvents. */
38
+ export type DrainStream = (response: Response, signal: AbortSignal) => Promise<StreamTally>;
39
+
40
+ /** Bound fetch for one endpoint. Receives materialized headers + serialized body. */
41
+ export type EmptyStreamFetch = (
42
+ url: string,
43
+ init: { headers: Record<string, string>; body: string; signal: AbortSignal },
44
+ ) => Promise<Response>;
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
+ */
55
+ export interface EmptyStreamPolicy {
56
+ readonly maxEmptyReplays?: number;
57
+ readonly firstEventTimeoutMs?: number;
58
+ readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
59
+ }
60
+
61
+ export interface EmptyStreamResult {
62
+ readonly response: Response;
63
+ readonly tally: StreamTally;
64
+ readonly endpoint: string;
65
+ readonly replaysUsed: number;
66
+ }
67
+
68
+ // ─── Defaults ──────────────────────────────────────────────────────────────
69
+
70
+ const DEFAULT_MAX_REPLAYS = 2;
71
+ const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 200_000; // 200s, matches CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS default
72
+
73
+
74
+ // ─── Helpers ───────────────────────────────────────────────────────────────
75
+
76
+ /** Pure predicate: zero data events, or data events with nothing meaningful. */
77
+ export function isEmptyStream(tally: StreamTally): boolean {
78
+ return tally.dataEvents === 0 || (!tally.meaningful && !tally.thoughtOnly);
79
+ }
80
+
81
+
82
+
83
+ // ─── Main function ─────────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Replays the byte-identical request while responses drain empty,
87
+ * failing over across ordered endpoints pre-start.
88
+ *
89
+ * Throws GatewayError (platform_unavailable, 502, upstream) on exhaustion,
90
+ * GatewayError (deadline_exceeded, 504, upstream) when a first event never
91
+ * arrives within the window, and GatewayError (transport_closed, 499) on
92
+ * client abort.
93
+ */
94
+ export async function fetchWithEmptyStreamRecovery(
95
+ endpoints: readonly string[],
96
+ request: { headers: Record<string, string>; body: string },
97
+ deps: {
98
+ fetch: EmptyStreamFetch;
99
+ drain: DrainStream;
100
+ signal: AbortSignal;
101
+ deadlineMs: number;
102
+ policy?: EmptyStreamPolicy;
103
+ },
104
+ ): Promise<EmptyStreamResult> {
105
+ const maxReplays = deps.policy?.maxEmptyReplays ?? DEFAULT_MAX_REPLAYS;
106
+ const firstEventTimeout = deps.policy?.firstEventTimeoutMs ?? DEFAULT_FIRST_EVENT_TIMEOUT_MS;
107
+ const sleep = deps.policy?.sleep ?? abortableSleep;
108
+
109
+ if (endpoints.length === 0) {
110
+ throw new GatewayError("platform_unavailable", 502, "No endpoints available", {}, "upstream");
111
+ }
112
+
113
+ for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex++) {
114
+ const endpoint = endpoints[endpointIndex]!;
115
+ const isLastEndpoint = endpointIndex === endpoints.length - 1;
116
+ const replaysAllowed = isLastEndpoint ? maxReplays : maxReplays;
117
+
118
+ for (let replay = 0; replay <= replaysAllowed; replay++) {
119
+ // Check abort before each fetch
120
+ if (deps.signal.aborted) {
121
+ throw new GatewayError("transport_closed", 499, "Aborted");
122
+ }
123
+
124
+ // Check deadline floor
125
+ const now = Date.now();
126
+ const remaining = deps.deadlineMs - now;
127
+ if (remaining < MIN_RETRY_BUDGET_MS) {
128
+ throw new GatewayError(
129
+ "platform_unavailable",
130
+ 502,
131
+ "Deadline exhausted before empty-stream replay",
132
+ {},
133
+ "upstream",
134
+ );
135
+ }
136
+
137
+ const fetchTimeout = Math.min(firstEventTimeout, remaining);
138
+
139
+ let response: Response;
140
+ try {
141
+ response = await fetchWithTimeout(deps.fetch, endpoint, request, deps.signal, fetchTimeout);
142
+ } catch (error) {
143
+ if (error instanceof GatewayError) throw error;
144
+ break; // advance to next endpoint
145
+ }
146
+
147
+ // Drain the response
148
+ let tally: StreamTally;
149
+ try {
150
+ tally = await deps.drain(response, deps.signal);
151
+ } catch (error) {
152
+ if (error instanceof GatewayError) throw error;
153
+ break; // advance to next endpoint
154
+ }
155
+
156
+ // Check if empty
157
+ if (!isEmptyStream(tally)) {
158
+ // Success — return the response and tally
159
+ return {
160
+ response,
161
+ tally,
162
+ endpoint,
163
+ replaysUsed: replay,
164
+ };
165
+ }
166
+
167
+ // Empty — replay if we haven't exhausted this endpoint's budget
168
+ if (replay < replaysAllowed) {
169
+ const delay = fallbackRetryDelayMs(replay);
170
+ try {
171
+ await sleep(delay, deps.signal);
172
+ } catch (error) {
173
+ if (error instanceof GatewayError) throw error;
174
+ throw error;
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // All endpoints exhausted
181
+ throw new GatewayError(
182
+ "platform_unavailable",
183
+ 502,
184
+ "All endpoints returned empty streams",
185
+ {},
186
+ "upstream",
187
+ );
188
+ }
189
+
190
+ // ─── Internal helpers ──────────────────────────────────────────────────────
191
+
192
+ async function fetchWithTimeout(
193
+ fetch: EmptyStreamFetch,
194
+ url: string,
195
+ request: { headers: Record<string, string>; body: string },
196
+ signal: AbortSignal,
197
+ timeoutMs: number,
198
+ ): Promise<Response> {
199
+ // Race the fetch against a first-event timeout
200
+ const timeoutPromise = new Promise<never>((_, reject) => {
201
+ const timer = setTimeout(() => {
202
+ // A first-event timeout is the gateway waiting on a silent upstream (e.g.
203
+ // a provider that holds the request in a queue), not the client leaving.
204
+ // It must surface as a deadline failure, never as a `transport_closed`
205
+ // 499 — that code is reserved for genuine client cancellations and would
206
+ // otherwise hide a real upstream wait behind "cancelled by client".
207
+ reject(new GatewayError("deadline_exceeded", 504, "First event timeout", {}, "upstream"));
208
+ }, timeoutMs);
209
+ signal.addEventListener("abort", () => {
210
+ clearTimeout(timer);
211
+ reject(new GatewayError("transport_closed", 499, "Aborted"));
212
+ }, { once: true });
213
+ });
214
+
215
+ try {
216
+ return await Promise.race([
217
+ fetch(url, { headers: request.headers, body: request.body, signal }),
218
+ timeoutPromise,
219
+ ]);
220
+ } catch (error) {
221
+ if (error instanceof GatewayError) throw error;
222
+ throw error;
223
+ }
224
+ }
@@ -21,34 +21,16 @@
21
21
  import type { ImageGenerationOutcome, ProviderAdapter, ProviderDispatchTarget } from "../../providers/provider-contracts";
22
22
  import { GatewayError } from "../../protocol/gateway-error";
23
23
  import { resolveCredentialForAccount } from "../../providers/operations/provider-credential-service";
24
- import type { OAuthTokenRefresher, OAuthRefreshService } from "../../providers/authentication/oauth-refresh-service";
25
- import type { ValidatedNetworkBindingFactory } from "../../network/pool/resolver";
26
- import type { CartethyiaDatabase } from "../../persistence/db-handle";
27
- import type { RouteSnapshotService } from "../routing/route-model";
28
24
  import type { ServiceKind } from "../../protocol/canonical-model";
29
25
  import { metrics } from "../../observability/metrics";
30
- import type { NetworkPoolSelector } from "../../network/pool/selector";
31
- import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
32
26
  import type { ProxyRequestState } from "../request/state";
33
- import { ProxyRequestStateStore } from "../request/state";
34
- import { ProxyRequestPreparer } from "../request/preparer";
35
- import { completeAttempt, estimatedUsage } from "./attempt-finalize";
27
+ import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt-finalize";
36
28
  import { runAttemptLoop } from "./attempt-loop";
37
29
  import { captureProviderRateLimit } from "./ratelimit-capture";
38
30
 
39
- export interface GenerationHandlerDeps {
40
- readonly db: CartethyiaDatabase;
41
- readonly providerAdapters: ReadonlyMap<string, ProviderAdapter>;
42
- readonly resolveProviderAdapter?: (providerId: string) => Promise<ProviderAdapter | undefined>;
43
- readonly proxyPreparer: ProxyRequestPreparer;
44
- readonly stateStore: ProxyRequestStateStore;
45
- readonly poolSelector?: NetworkPoolSelector;
46
- readonly networkBindingFactory?: ValidatedNetworkBindingFactory;
47
- readonly snapshotService?: RouteSnapshotService;
48
- readonly telemetryBuffer?: TelemetryBatchBuffer;
49
- readonly resolveOAuthRefresher?: (providerId: string) => Promise<OAuthTokenRefresher | undefined>;
50
- readonly oauthRefreshService?: OAuthRefreshService;
51
- }
31
+ import type { GatewayHandlerDeps } from "./handler-deps";
32
+
33
+ export type GenerationHandlerDeps = GatewayHandlerDeps;
52
34
 
53
35
  /** Which surface a handler serves. Drives candidate filtering and error copy. */
54
36
  export type GenerationKind = Extract<ServiceKind, "imageGeneration" | "videoGeneration">;
@@ -133,22 +115,23 @@ async function passThrough(
133
115
  captureProviderRateLimit(args.candidate.provider_id, args.candidate.provider_account_id, upstream.headers);
134
116
  await completeAttempt(state, {
135
117
  status: "completed",
136
- providerId: args.candidate.provider_id,
137
- ...(args.candidate.provider_account_id ? { accountId: args.candidate.provider_account_id } : {}),
138
- ...(args.candidate.provider_account_label ? { accountLabel: args.candidate.provider_account_label } : {}),
139
- ...(args.networkPoolId ? { networkPoolId: args.networkPoolId } : {}),
140
- usage,
141
- modelId: args.candidate.model_id,
142
- lease: args.lease as never,
143
- commitUsage: usage,
144
- terminal: true,
145
- tenantId: args.tenantId,
146
- ingressBody: state.ingressBody,
118
+ ...nativeCompletionBase({
119
+ providerId: args.candidate.provider_id,
120
+ ...(args.candidate.provider_account_id ? { accountId: args.candidate.provider_account_id } : {}),
121
+ ...(args.candidate.provider_account_label ? { accountLabel: args.candidate.provider_account_label } : {}),
122
+ ...(args.networkPoolId ? { networkPoolId: args.networkPoolId } : {}),
123
+ usage,
124
+ modelId: args.candidate.model_id,
125
+ lease: args.lease as never,
126
+ commitUsage: usage,
127
+ tenantId: args.tenantId,
128
+ ingressBody: state.ingressBody,
129
+ providerCapture: args.providerCapture as never,
130
+ db: args.deps.db,
131
+ ...(args.deps.telemetryBuffer ? { telemetryBuffer: args.deps.telemetryBuffer } : {}),
132
+ ...(args.deps.snapshotService ? { snapshotService: args.deps.snapshotService } : {}),
133
+ }),
147
134
  responseBody: null,
148
- providerCapture: args.providerCapture as never,
149
- db: args.deps.db,
150
- ...(args.deps.telemetryBuffer ? { telemetryBuffer: args.deps.telemetryBuffer } : {}),
151
- ...(args.deps.snapshotService ? { snapshotService: args.deps.snapshotService } : {}),
152
135
  });
153
136
  metrics.proxy_requests_total.inc(1, { status: "completed" });
154
137
  return new Response(upstream.body, {
@@ -251,9 +234,7 @@ export function createNativeSurfaceHandler(
251
234
  provider_id: candidate.provider_id as ProviderAdapter["provider_id"],
252
235
  credential_kind: "none" as const,
253
236
  };
254
- const adapter = deps.resolveProviderAdapter
255
- ? await deps.resolveProviderAdapter(candidate.provider_id)
256
- : deps.providerAdapters.get(candidate.provider_id);
237
+ const adapter = await deps.resolveProviderAdapter?.(candidate.provider_id);
257
238
  if (!adapter) {
258
239
  throw new GatewayError(
259
240
  "capability_unsupported",
@@ -292,22 +273,23 @@ export function createNativeSurfaceHandler(
292
273
  const usage = estimatedUsage(0, 0);
293
274
  await completeAttempt(state, {
294
275
  status: "completed",
295
- providerId: candidate.provider_id,
296
- ...(candidate.provider_account_id ? { accountId: candidate.provider_account_id } : {}),
297
- ...(candidate.provider_account_label ? { accountLabel: candidate.provider_account_label } : {}),
298
- ...(leases.proxySlot ? { networkPoolId: leases.proxySlot.poolId } : {}),
299
- usage,
300
- modelId: candidate.model_id,
301
- lease: leases.lease,
302
- commitUsage: usage,
303
- terminal: true,
304
- tenantId: prepared.authorization.tenantId,
305
- ingressBody: state.ingressBody,
276
+ ...nativeCompletionBase({
277
+ providerId: candidate.provider_id,
278
+ ...(candidate.provider_account_id ? { accountId: candidate.provider_account_id } : {}),
279
+ ...(candidate.provider_account_label ? { accountLabel: candidate.provider_account_label } : {}),
280
+ ...(leases.proxySlot ? { networkPoolId: leases.proxySlot.poolId } : {}),
281
+ usage,
282
+ modelId: candidate.model_id,
283
+ lease: leases.lease,
284
+ commitUsage: usage,
285
+ tenantId: prepared.authorization.tenantId,
286
+ ingressBody: state.ingressBody,
287
+ providerCapture,
288
+ db: deps.db,
289
+ ...(deps.telemetryBuffer ? { telemetryBuffer: deps.telemetryBuffer } : {}),
290
+ ...(deps.snapshotService ? { snapshotService: deps.snapshotService } : {}),
291
+ }),
306
292
  responseBody: payload,
307
- providerCapture,
308
- db: deps.db,
309
- ...(deps.telemetryBuffer ? { telemetryBuffer: deps.telemetryBuffer } : {}),
310
- ...(deps.snapshotService ? { snapshotService: deps.snapshotService } : {}),
311
293
  });
312
294
  metrics.proxy_requests_total.inc(1, { status: "completed" });
313
295
  return jsonResponse(payload, 200, state.requestId);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The one dependency shape every gateway route handler takes.
3
+ *
4
+ * Proxy, generation, responses-compact, system one, web search, and token
5
+ * count all receive the same wiring. Before this module each handler spelled
6
+ * its own near-identical interface and `app.ts` spelled the same object five
7
+ * times — a new handler dep had to be added to five interfaces and five
8
+ * literals to reach the runtime. One shape means one place to grow.
9
+ */
10
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
11
+ import type { ProviderAdapter } from "../../providers/provider-contracts";
12
+ import type { OAuthRefreshService, OAuthTokenRefresher } from "../../providers/authentication/oauth-refresh-service";
13
+ import type { NetworkPoolSelector } from "../../network/pool/selector";
14
+ import type { ValidatedNetworkBindingFactory } from "../../network/pool/resolver";
15
+ import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
16
+ import type { ProxyRequestPreparer } from "../request/preparer";
17
+ import type { ProxyRequestStateStore } from "../request/state";
18
+ import type { RouteSnapshotService } from "../routing/route-model";
19
+
20
+ export interface GatewayHandlerDeps {
21
+ readonly db: CartethyiaDatabase;
22
+ /** Preferred over a static map: resolves an adapter on demand and caches it. */
23
+ readonly resolveProviderAdapter?: (providerId: string) => Promise<ProviderAdapter | undefined>;
24
+ readonly proxyPreparer: ProxyRequestPreparer;
25
+ readonly stateStore: ProxyRequestStateStore;
26
+ readonly poolSelector?: NetworkPoolSelector;
27
+ readonly networkBindingFactory?: ValidatedNetworkBindingFactory;
28
+ readonly snapshotService?: RouteSnapshotService;
29
+ readonly telemetryBuffer?: TelemetryBatchBuffer;
30
+ readonly resolveOAuthRefresher?: (providerId: string) => Promise<OAuthTokenRefresher | undefined>;
31
+ readonly oauthRefreshService?: OAuthRefreshService;
32
+ }
@@ -124,11 +124,16 @@ async function acquirePoolSlot(
124
124
  }
125
125
  }
126
126
  // Round-robin rotation rides the strategy the snapshot attached to the
127
- // candidate (the pool-owning tenant scopes the cursor); absent strategy or
128
- // `least_loaded` keeps the default weighted least-loaded scan.
127
+ // candidate (the pool-owning tenant scopes the cursor); `random` scatters
128
+ // each selection across eligible pools. Absent strategy or `least_loaded`
129
+ // keeps the default weighted least-loaded scan.
129
130
  const rotation: PoolRotation | undefined =
130
- routing?.strategy === "round_robin"
131
- ? { key: routing.tenantId, rotateCount: routing.rotateCount }
131
+ routing?.strategy === "round_robin" || routing?.strategy === "random"
132
+ ? {
133
+ key: routing.tenantId,
134
+ rotateCount: routing.rotateCount,
135
+ strategy: routing.strategy,
136
+ }
132
137
  : undefined;
133
138
 
134
139
  if (!poolIds?.length || !poolSelector) {