cartethyia 2.7.0 → 2.7.2

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 (330) hide show
  1. package/.env.example +107 -106
  2. package/README.md +25 -2
  3. package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-XaUmiBrC.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CqD7QiLM.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-0XUo4UMn.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-CXGS_Ct-.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-DAG5J9EY.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
  17. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js +1 -0
  18. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +4 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.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-B9uWu4Nz.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +9 -0
  30. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
  31. package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-7P2n--Fv.js} +1 -1
  32. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CxZ1fTkN.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
  37. package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-Dqf4zpS-.js} +1 -1
  38. package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-Byj2lAEm.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
  43. package/dist/dashboard/assets/{api-keys-DFY_fZU4.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-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
  48. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
  50. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
  52. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
  54. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CcB6mGh2.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-DGb2P9Yx.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-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
  60. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
  62. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  65. package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-ClCMCWWQ.js} +3 -3
  66. package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
  67. package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
  68. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  69. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js +1 -0
  70. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-BxpznFLt.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-CDzP3ovJ.js → index-DFVFrnWW.js} +2 -2
  74. package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
  76. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-D23fo7cp.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-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
  80. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-dmbwZMDY.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-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
  84. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
  86. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  89. package/dist/dashboard/assets/providers/inception.svg +5 -0
  90. package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
  94. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-KIT0rC6G.js → select-DBNVLdUu.js} +1 -1
  96. package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
  98. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
  100. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
  102. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  103. package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
  104. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  105. package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
  106. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  107. package/dist/dashboard/index.html +1 -1
  108. package/dist/dashboard/index.html.gz +0 -0
  109. package/dist/dashboard/precompressed-assets.json +54 -53
  110. package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
  111. package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
  112. package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
  113. package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
  114. package/package.json +96 -96
  115. package/scripts/cli/console-menu.ts +5 -2
  116. package/scripts/cli/tray.ts +21 -6
  117. package/src/boot/app.ts +684 -714
  118. package/src/boot/dependencies.ts +65 -62
  119. package/src/config/cooldowns.ts +16 -0
  120. package/src/config/providers.ts +13 -1
  121. package/src/config/resources.ts +70 -70
  122. package/src/config/security.ts +37 -37
  123. package/src/config/spec.ts +371 -358
  124. package/src/console/cli-tools/contracts.ts +10 -4
  125. package/src/console/console-router.ts +233 -225
  126. package/src/console/domain-registration.ts +398 -381
  127. package/src/console/providers/catalog/catalog-projections.ts +2 -6
  128. package/src/console/providers/catalog/contracts.ts +19 -0
  129. package/src/console/providers/catalog/provider-operations.ts +38 -0
  130. package/src/console/providers/catalog/routes.ts +14 -0
  131. package/src/console/providers/catalog/store-helpers.ts +326 -0
  132. package/src/console/providers/catalog/store.ts +1292 -1563
  133. package/src/console/quota/account-quota-global.ts +2 -2
  134. package/src/console/quota/account-quota-shared.ts +1 -1
  135. package/src/console/quota/account-quota-tenant.ts +35 -12
  136. package/src/console/quota/quota-view.ts +2 -2
  137. package/src/console/routing/pools/contracts.ts +41 -1
  138. package/src/console/routing/pools/operations.ts +183 -4
  139. package/src/console/routing/pools/relay-deploy.ts +17 -4
  140. package/src/console/routing/pools/routes.ts +30 -0
  141. package/src/console/routing/pools/store.ts +98 -8
  142. package/src/main.ts +183 -167
  143. package/src/network/outbound-fetch.ts +124 -208
  144. package/src/network/pool/agent.ts +15 -0
  145. package/src/network/pool/pool-state.ts +7 -0
  146. package/src/network/pool/selector.ts +110 -2
  147. package/src/network/pool-cooldown-store.ts +252 -0
  148. package/src/network/pool-health-machine.ts +5 -3
  149. package/src/network/pool-health.ts +36 -9
  150. package/src/observability/event-query.ts +57 -0
  151. package/src/observability/metrics.ts +467 -467
  152. package/src/observability/runtime-metrics.ts +269 -269
  153. package/src/persistence/health-event-flush.ts +2 -0
  154. package/src/persistence/schema.ts +19 -0
  155. package/src/persistence/usage-flush.ts +8 -3
  156. package/src/protocol/canonical-model.ts +1 -1
  157. package/src/protocol/dsml.ts +377 -0
  158. package/src/protocol/gateway-error.ts +232 -232
  159. package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
  160. package/src/protocol/request/chat.ts +7 -7
  161. package/src/protocol/request/codex.ts +2 -2
  162. package/src/protocol/request/gemini.ts +1 -1
  163. package/src/protocol/request/messages.ts +6 -6
  164. package/src/protocol/request/responses.ts +7 -7
  165. package/src/protocol/response/dsml.ts +12 -133
  166. package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
  167. package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
  168. package/src/{transport → protocol}/translation/capabilities.ts +2 -2
  169. package/src/{transport → protocol}/translation/normalize.ts +1 -1
  170. package/src/{transport → protocol}/translation/quirks.ts +1 -1
  171. package/src/{transport → protocol}/translation/search.ts +1 -1
  172. package/src/{transport → protocol}/translation/thinking.ts +23 -1
  173. package/src/{transport → protocol}/translation/tool-id.ts +1 -1
  174. package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
  175. package/src/protocol/upstream-parsing.ts +176 -0
  176. package/src/providers/adapters/compatible-adapter.ts +11 -2
  177. package/src/providers/authentication/oauth-refresh-service.ts +9 -5
  178. package/src/providers/default-registry.ts +22 -5
  179. package/src/providers/discovery/probe-phases.ts +16 -3
  180. package/src/providers/discovery/probing-service.ts +4 -1
  181. package/src/providers/integrations/anthropic.ts +1 -1
  182. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
  184. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
  185. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
  186. package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
  187. package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
  188. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
  189. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
  190. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
  191. package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
  192. package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
  193. package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
  194. package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
  195. package/src/providers/integrations/claude/claude.ts +1 -1
  196. package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
  197. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  198. package/src/providers/integrations/codex/codex.ts +1 -1
  199. package/src/providers/integrations/factory/factory.ts +1 -1
  200. package/src/providers/integrations/github/github.ts +1 -1
  201. package/src/providers/integrations/grok/grok.ts +1 -1
  202. package/src/providers/integrations/inception/provider.ts +91 -0
  203. package/src/providers/integrations/inferhub.ts +1 -1
  204. package/src/providers/integrations/kiro/kiro-request.ts +1 -1
  205. package/src/providers/integrations/mistral.ts +193 -19
  206. package/src/providers/integrations/ollamacloud-models.ts +50 -0
  207. package/src/providers/integrations/opencode/fingerprint.ts +19 -2
  208. package/src/providers/integrations/opencode/prompt.ts +111 -0
  209. package/src/providers/integrations/opencode/provider.ts +101 -23
  210. package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
  211. package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
  212. package/src/providers/integrations/qoder/qoder-oauth.ts +12 -1
  213. package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
  214. package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
  215. package/src/providers/integrations/qoder/qoder.ts +315 -400
  216. package/src/providers/integrations/shared/install-id.ts +2 -1
  217. package/src/providers/integrations/trae/trae-cache.ts +110 -0
  218. package/src/providers/integrations/trae/trae-oauth.ts +29 -4
  219. package/src/providers/integrations/trae/trae-protocol.ts +2 -1
  220. package/src/providers/integrations/trae/trae.ts +106 -23
  221. package/src/providers/operations/account-error-classification.ts +1684 -0
  222. package/src/providers/operations/account-health-service.ts +27 -1503
  223. package/src/providers/operations/account-reset-service.ts +2 -0
  224. package/src/providers/operations/provider-catalog-service.ts +11 -1
  225. package/src/providers/provider-contracts.ts +7 -4
  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/cli-client-fingerprint.ts +9 -2
  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 +9 -2
  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 +92 -178
  246. package/src/transport/guards/abuse-guard.ts +10 -0
  247. package/src/transport/guards/auth-guard.ts +109 -109
  248. package/src/transport/guards/error-lifecycle.ts +1 -1
  249. package/src/transport/guards/pipeline.ts +220 -220
  250. package/src/transport/guards/request-context.ts +245 -238
  251. package/src/transport/request/degradation.ts +20 -1
  252. package/src/transport/request/inflight.ts +4 -114
  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 +75 -43
  257. package/src/transport/routing/router.ts +30 -2
  258. package/src/transport/surface/messages/parse.ts +1 -1
  259. package/src/transport/surface/responses/encode.ts +2 -3
  260. package/src/transport/surface/responses/parse.ts +49 -7
  261. package/src/types/undici.d.ts +33 -0
  262. package/src/workers/buddy-growth-tasks.ts +346 -425
  263. package/src/workers/daily-checkin.ts +17 -6
  264. package/src/workers/quota-refresh-sweep.ts +2 -2
  265. package/src/workers/scheduler.ts +17 -1
  266. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  267. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  268. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  269. package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
  270. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
  271. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  272. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  273. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
  274. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  275. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
  276. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
  277. package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
  278. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
  279. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
  280. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
  281. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  282. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
  283. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  284. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
  285. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
  286. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
  287. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  288. package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
  289. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
  290. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
  291. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  292. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  293. package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
  294. package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
  295. package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
  296. package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
  297. package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
  298. package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
  299. package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
  300. package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
  301. package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
  302. package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
  303. package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
  304. package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
  305. package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
  306. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  307. package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
  308. package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
  309. package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
  310. package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
  311. package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
  312. package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
  313. package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
  314. package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
  315. package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
  316. package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
  317. package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
  318. package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
  319. package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
  320. package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
  321. package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
  322. package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
  323. package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
  324. package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
  325. package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
  326. package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
  327. package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
  328. package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
  329. /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
  330. /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
@@ -642,6 +642,7 @@ async function recordResetActivity(
642
642
  })
643
643
  .where(eq(providerAccounts.id, accountId));
644
644
  await db.insert(healthEvents).values({
645
+ id: randomUUID(),
645
646
  entityKind: "account",
646
647
  accountId,
647
648
  fromStatus: currentStatus,
@@ -655,6 +656,7 @@ async function recordResetActivity(
655
656
  }
656
657
 
657
658
  await db.insert(healthEvents).values({
659
+ id: randomUUID(),
658
660
  entityKind: "account",
659
661
  accountId,
660
662
  fromStatus: currentStatus,
@@ -338,7 +338,17 @@ export async function registerByokProviders(
338
338
  provider_id: providerId,
339
339
  base_url: baseUrl,
340
340
  authentication_header_shape: wireProfile.authHeaderShape,
341
- buildExtraHeaders: (_context, _request, candidate) => {
341
+ buildExtraHeaders: (context, _request, candidate) => {
342
+ // An operator-set route identity wins over both auto modes: Routing
343
+ // Strategy now serves custom providers too, and its value (including
344
+ // the empty "None" choice) must be honored verbatim.
345
+ const routeAgent = context.user_agent;
346
+ if (routeAgent !== undefined) {
347
+ return {
348
+ ...(routeAgent.length > 0 ? { "user-agent": routeAgent } : {}),
349
+ ...(compatibilityProfile?.extra_headers ?? {}),
350
+ };
351
+ }
342
352
  // Explicit gateway identity wins over CLI cloaking, and Codex /
343
353
  // Claude-Code-shaped traffic can never take this path: this hook
344
354
  // only runs for BYOK custom rows, never bundled first-party
@@ -134,11 +134,13 @@ export interface ProviderDispatchContext {
134
134
  readonly outbound_fetch?: ValidatedOutboundFetch | undefined;
135
135
  /** Validated image/attachment egress; deliberately excluded from payload capture. */
136
136
  readonly outbound_attachment_fetch?: ValidatedOutboundFetch | undefined;
137
- /** Validated WSS egress bound to the same selected network pool as HTTP. */
138
- readonly outbound_websocket?: ValidatedOutboundWebSocket | undefined;
139
137
  /** Validated bidirectional HTTP/2 egress bound to the same selected network pool. */
140
138
  readonly outbound_http2?: ValidatedOutboundHttp2Bidi | undefined;
141
- /** Built-in API-key route-selected fallback User-Agent; custom providers leave this unset. */
139
+ /**
140
+ * Route-selected fallback User-Agent from Routing Strategy; set when the
141
+ * operator configured one (bundled API-key providers default to the Claude
142
+ * CLI identity). Empty means the operator chose "None".
143
+ */
142
144
  readonly user_agent?: string | undefined;
143
145
  /** Gateway request id, stable across provider retries for one logical request. */
144
146
  readonly request_id?: string | undefined;
@@ -190,7 +192,8 @@ export function applyRouteUserAgent(
190
192
  if (
191
193
  credential.credential_kind === "oauth" ||
192
194
  credential.credential_kind === "scoped_access_token" ||
193
- userAgent === undefined
195
+ userAgent === undefined ||
196
+ userAgent.trim().length === 0
194
197
  ) {
195
198
  return;
196
199
  }
@@ -15,6 +15,15 @@ export interface CompatibilityProfile {
15
15
  }>;
16
16
  /** Whether requests to OpenAI/Anthropic-compatible upstream should send official CLI headers. Defaults to true. */
17
17
  readonly cli_identity?: boolean;
18
+ /**
19
+ * Direct egress for this provider uses the runtime-native fetch instead of
20
+ * the Node-stack pinned agent. Some upstreams meter per client transport
21
+ * fingerprint — OpenCode's anonymous free tier refuses the Node agent with
22
+ * 429 FreeUsageLimitError while accepting the native fetch byte-for-byte
23
+ * (verified live). The SSRF resolve/validate pass still runs for every hop;
24
+ * only the socket-level address pin is traded away.
25
+ */
26
+ readonly preferNativeFetch?: boolean;
18
27
  /**
19
28
  * Opts this provider into the gateway identity (`user-agent:
20
29
  * `Cartethyia/<version>`) instead of official CLI cloaking. Explicit only:
@@ -322,6 +331,14 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
322
331
  hasAdapterUserAgent: true,
323
332
  },
324
333
  { id: "bai", displayName: "B.AI", baseUrl: "https://api.b.ai/v1", credentialUrl: "https://b.ai" },
334
+ {
335
+ id: "inception",
336
+ displayName: "Inception Labs",
337
+ // Version-rooted: the chat wire lives below the `/v1` root, so the
338
+ // declared path is `/chat/completions` (same rule as Groq/DeepInfra).
339
+ baseUrl: "https://api.inceptionlabs.ai/v1",
340
+ credentialUrl: "https://platform.inceptionlabs.ai/dashboard/api-keys",
341
+ },
325
342
  {
326
343
  id: "larprouter",
327
344
  displayName: "LarRouter",
@@ -630,16 +647,20 @@ export function providerBaseUrl(providerId: string): string {
630
647
  export const PROVIDER_COMPATIBILITY_PROFILES: Record<string, CompatibilityProfile> = {
631
648
  opencode: {
632
649
  endpoint_paths_by_wire_family: {
633
- chat: "/inference/v1/chat/completions",
634
- responses: "/inference/v1/responses",
650
+ chat: "/inference/openai/v1/chat/completions",
651
+ responses: "/inference/openai/v1/responses",
635
652
  },
653
+ // Direct egress must use the native fetch: the anonymous free tier refuses
654
+ // the Node-stack agent with 429 FreeUsageLimitError (verified live).
655
+ preferNativeFetch: true,
636
656
  model_wire_families: [{ pattern: "^muse-spark", wire_family: "responses" }],
637
657
  },
638
658
  "opencode-zen": {
639
659
  endpoint_paths_by_wire_family: {
640
- chat: "/inference/v1/chat/completions",
641
- responses: "/inference/v1/responses",
660
+ chat: "/inference/openai/v1/chat/completions",
661
+ responses: "/inference/openai/v1/responses",
642
662
  },
663
+ preferNativeFetch: true,
643
664
  model_wire_families: [{ pattern: "^muse-spark", wire_family: "responses" }],
644
665
  },
645
666
  "opencode-go": {
@@ -110,13 +110,13 @@ export async function buildProviderProbeRequest(
110
110
  provider_id: providerId,
111
111
  model_id: PROBE_MODEL,
112
112
  wire_family: "chat",
113
- endpoint_path: "/inference/v1/chat/completions",
113
+ endpoint_path: "/inference/openai/v1/chat/completions",
114
114
  capabilities: {},
115
115
  } as ProviderDispatchTarget,
116
116
  );
117
117
  const headers = (await spec.buildExtraHeaders?.(probeContext(providerId), source)) ?? {};
118
118
  return {
119
- url: `${providerBaseUrl(providerId).replace(/\/+$/, "")}/inference/v1/chat/completions`,
119
+ url: `${providerBaseUrl(providerId).replace(/\/+$/, "")}/inference/openai/v1/chat/completions`,
120
120
  headers: { "content-type": "application/json", ...headers },
121
121
  body: payload,
122
122
  };
@@ -6,7 +6,7 @@
6
6
  // Reads the pre-envelope bare result and v2-v4 envelopes during upgrade;
7
7
  // writes use v5, which records attempt time, retained-refresh errors, and
8
8
  // forces a one-time refresh for required quota accounting metadata.
9
- import type { ProviderQuotaResult } from "../../providers/quota/quota-contracts";
9
+ import type { ProviderQuotaResult } from "./quota-contracts";
10
10
  import type { RedisClient } from "../../persistence/redis";
11
11
  import { TtlCache } from "../../persistence/ttl-cache";
12
12
  import { log } from "../../observability/logger";
@@ -11,12 +11,12 @@ import type { CartethyiaDatabase } from "../../persistence/db-handle";
11
11
  import { providerAccounts } from "../../persistence/schema";
12
12
  import type { RedisClient } from "../../persistence/redis";
13
13
  import { log } from "../../observability/logger";
14
- import { fetchProviderQuota } from "../../providers/quota/quota-support";
15
- import { type FetchLike, type ProviderQuotaResult } from "../../providers/quota/quota-contracts";
16
- import type { ProviderId } from "../../providers/provider-contracts";
17
- import type { ProviderRegistry } from "../../providers/provider-registry";
14
+ import { fetchProviderQuota } from "./quota-support";
15
+ import { type FetchLike, type ProviderQuotaResult } from "./quota-contracts";
16
+ import type { ProviderId } from "../provider-contracts";
17
+ import type { ProviderRegistry } from "../provider-registry";
18
18
  import { GLOBAL_QUOTA_LENS, setCachedQuota } from "./cache";
19
- import { record } from "../../providers/authentication/oauth-flow-store";
19
+ import { record } from "../authentication/oauth-flow-store";
20
20
 
21
21
  /** Why an account quota refresh failed; `null` means the fetch succeeded. */
22
22
  export type QuotaRefreshFailureCode = "missing_credential" | "quota_fetch_failed";
@@ -50,13 +50,20 @@ const REMOTE_CLI_TOOL_UA: ReadonlyArray<{
50
50
  // filtering; MCP traffic uses `claude-code/<version>` instead.
51
51
  { toolId: "claude", pattern: /(^|[^a-z0-9])claude-cli\//i },
52
52
  { toolId: "claude", pattern: /(^|[^a-z0-9])claude-code\//i },
53
+ // Codex CLI (TUI/CLI builds) — the Guardian auto-review agent sends a
54
+ // `codex-auto-review` model that is only useful when the gateway can
55
+ // remap it to a serving provider. `codex-tui`, `codex-cli`, and the
56
+ // desktop agent's `opencode`-ish UAs all carry this prefix.
57
+ { toolId: "codex", pattern: /(^|[^a-z0-9])codex-(tui|cli)\//i },
53
58
  ];
54
59
 
55
60
  /**
56
61
  * Returns the remote CLI tool id recognised from the probe, or `null` when
57
62
  * the caller is not a remote-mapping client. Ordinary SDKs, curl, and local
58
- * mapping tools (Codex, …) stay unlabelled so their model ids resolve
59
- * through ordinary aliases / allowlists.
63
+ * mapping tools stay unlabelled so their model ids resolve through ordinary
64
+ * aliases / allowlists. Codex is labelled because its Guardian auto-review
65
+ * agent sends the hardcoded `codex-auto-review` model, which must be remapable
66
+ * to a serving provider for approval reviews to work at all.
60
67
  */
61
68
  export function detectRemoteCliToolId(probe: CliClientProbe): string | null {
62
69
  const userAgent = readUserAgent(probe);
@@ -13,7 +13,7 @@ import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer"
13
13
  import type { ProxyRequestOutcome, ProxyRequestState } from "../request/state";
14
14
  import type { PayloadCaptureState } from "../../observability/payload-capture";
15
15
  import { finalizeRequestTelemetry } from "../guards/error-lifecycle";
16
- import { debitCachedQuota, quotaLens } from "../../console/quota/cache";
16
+ import { debitCachedQuota, quotaLens } from "../../providers/quota/cache";
17
17
  import { BUDDY_PROVIDER_IDS } from "../../providers/provider-metadata";
18
18
  import { log } from "../../observability/logger";
19
19
  import {
@@ -195,6 +195,43 @@ export function providerRequestMetadataOnly(request: unknown): unknown {
195
195
  return Object.keys(metadata).length === 0 ? null : metadata;
196
196
  }
197
197
 
198
+ /**
199
+ * Captures one provider request body into the stored provider-request panel.
200
+ *
201
+ * Providers send bodies in several shapes: a plain string (the common case), a
202
+ * `Uint8Array`/Buffer (Qoder's custom cosy encoding), or another BodyInit
203
+ * type (URLSearchParams, Blob, FormData) that cannot be sliced safely here.
204
+ * Strings and byte arrays are decoded and stored; anything else is recorded as
205
+ * present-but-opaque, same as the metadata path.
206
+ */
207
+ function captureRequestBodyBody(
208
+ body: BodyInit,
209
+ maxBytes: number,
210
+ ): { body: unknown } | { body_present: true; body_bytes: number } {
211
+ if (typeof body === "string") {
212
+ return { body: parseCapturedBody(truncateUtf8(body, maxBytes)) };
213
+ }
214
+ if (body instanceof Uint8Array) {
215
+ return { body: parseCapturedBody(truncateUtf8(new TextDecoder().decode(body), maxBytes)) };
216
+ }
217
+ return { body_present: true, body_bytes: capturedBodyByteLength(body) };
218
+ }
219
+
220
+ /** Byte length of a request body in whatever shape the provider sent it. */
221
+ function capturedBodyByteLength(body: BodyInit): number {
222
+ if (typeof body === "string") return Buffer.byteLength(body, "utf8");
223
+ if (body instanceof Uint8Array) return body.byteLength;
224
+ if (typeof body === "object" && body !== null && "size" in body) {
225
+ const size = (body as { size?: unknown }).size;
226
+ if (typeof size === "number" && Number.isFinite(size)) return Math.round(size);
227
+ }
228
+ if (typeof body === "object" && body !== null && "byteLength" in body) {
229
+ const byteLength = (body as { byteLength?: unknown }).byteLength;
230
+ if (typeof byteLength === "number" && Number.isFinite(byteLength)) return Math.round(byteLength);
231
+ }
232
+ return 0;
233
+ }
234
+
198
235
  /**
199
236
  * Wraps a validated outbound fetch to tee the provider request/response.
200
237
  *
@@ -216,7 +253,7 @@ export function captureProviderExchange(
216
253
  : input instanceof URL
217
254
  ? input.toString()
218
255
  : input.url;
219
- const rawBody = typeof init?.body === "string" ? init.body : null;
256
+ const rawBody = init?.body;
220
257
  const headers = selectCapturedHeaders(init);
221
258
  sink.request = {
222
259
  method: init?.method ?? "GET",
@@ -227,11 +264,11 @@ export function captureProviderExchange(
227
264
  // shape sliced up to `maxBytes` of the translated body and ran it through
228
265
  // `JSON.parse` for every attempt — content a metadata-mode tenant had
229
266
  // explicitly declined to retain, paid on every dispatch.
230
- ...(rawBody === null
267
+ ...(rawBody === null || rawBody === undefined
231
268
  ? {}
232
269
  : captureBody
233
- ? { body: parseCapturedBody(truncateUtf8(rawBody, maxBytes)) }
234
- : { body_present: true, body_bytes: Buffer.byteLength(rawBody, "utf8") }),
270
+ ? captureRequestBodyBody(rawBody, maxBytes)
271
+ : { body_present: true, body_bytes: capturedBodyByteLength(rawBody) }),
235
272
  };
236
273
  const response = await inner(input, init);
237
274
  sink.response =
@@ -463,6 +500,13 @@ export function estimatedUsage(inputTokens: number, outputTokens: number): Usage
463
500
  export interface AttemptCompletion extends ProxyRequestOutcome {
464
501
  readonly modelId?: string;
465
502
  readonly error?: unknown;
503
+ /**
504
+ * The HTTP status the terminal classifier derived for a raw (non-GatewayError)
505
+ * failure — an upstream socket drop is 502, not the 500 the fallback implied.
506
+ * Only consulted when the error carries no status of its own and no wire
507
+ * response named one.
508
+ */
509
+ readonly terminalHttpStatus?: number;
466
510
  readonly lease?: AdmissionLease;
467
511
  /** Usage to reconcile, or undefined when this attempt consumed nothing billable. */
468
512
  readonly commitUsage?: UsageRecord;
@@ -538,6 +582,63 @@ export function completionContext(input: {
538
582
  };
539
583
  }
540
584
 
585
+ /**
586
+ * The terminal-completion base the native routes (generation, responses
587
+ * compact, system one, web search, token count) all assemble. The proxy and
588
+ * streaming paths use `completionContext`; these routes previously spelled
589
+ * the same twelve fields by hand in six places. Callers spread this and add
590
+ * only what differs: status, response body, error classification.
591
+ */
592
+ export function nativeCompletionBase(input: {
593
+ readonly providerId: string;
594
+ readonly modelId: string;
595
+ readonly tenantId: string | null;
596
+ readonly ingressBody: unknown;
597
+ readonly accountId?: string;
598
+ readonly accountLabel?: string;
599
+ readonly networkPoolId?: string;
600
+ readonly lease?: AdmissionLease;
601
+ readonly db: CartethyiaDatabase;
602
+ readonly telemetryBuffer?: TelemetryBatchBuffer;
603
+ readonly snapshotService?: { invalidate(): unknown };
604
+ readonly providerCapture?: ProviderExchangeCapture;
605
+ readonly usage?: AttemptCompletion["usage"];
606
+ readonly commitUsage?: AttemptCompletion["commitUsage"];
607
+ }): Omit<
608
+ AttemptCompletion,
609
+ | "status"
610
+ | "responseBody"
611
+ | "httpStatus"
612
+ | "terminalHttpStatus"
613
+ | "error"
614
+ | "clientResponseText"
615
+ | "firstContentDeltaAtMs"
616
+ | "lastEventAtMs"
617
+ | "reportedModel"
618
+ | "errorCategory"
619
+ | "errorOrigin"
620
+ | "ttfbMs"
621
+ | "tokensPerSec"
622
+ > {
623
+ return {
624
+ providerId: input.providerId,
625
+ modelId: input.modelId,
626
+ tenantId: input.tenantId,
627
+ ingressBody: input.ingressBody,
628
+ terminal: true,
629
+ ...(input.accountId === undefined ? {} : { accountId: input.accountId }),
630
+ ...(input.accountLabel === undefined ? {} : { accountLabel: input.accountLabel }),
631
+ ...(input.networkPoolId === undefined ? {} : { networkPoolId: input.networkPoolId }),
632
+ ...(input.lease === undefined ? {} : { lease: input.lease }),
633
+ db: input.db,
634
+ ...(input.telemetryBuffer === undefined ? {} : { telemetryBuffer: input.telemetryBuffer }),
635
+ ...(input.snapshotService === undefined ? {} : { snapshotService: input.snapshotService }),
636
+ ...(input.providerCapture === undefined ? {} : { providerCapture: input.providerCapture }),
637
+ ...(input.usage === undefined ? {} : { usage: input.usage }),
638
+ ...(input.commitUsage === undefined ? {} : { commitUsage: input.commitUsage }),
639
+ };
640
+ }
641
+
541
642
  /**
542
643
  * Decides whether a failed attempt proves *the pool* is unusable, and which
543
644
  * HTTP status that verdict rests on.
@@ -639,13 +740,14 @@ export async function completeAttempt(
639
740
  completion.httpStatus ??
640
741
  (completion.error instanceof GatewayError
641
742
  ? completion.error.status
642
- : completion.status === "completed"
643
- ? 200
644
- : completion.status === "cancelled"
645
- ? 499
646
- : completion.status === "truncated"
647
- ? 502
648
- : 500),
743
+ : completion.terminalHttpStatus ??
744
+ (completion.status === "completed"
745
+ ? 200
746
+ : completion.status === "cancelled"
747
+ ? 499
748
+ : completion.status === "truncated"
749
+ ? 502
750
+ : 500)),
649
751
  ...(completion.providerId === undefined ? {} : { providerId: completion.providerId }),
650
752
  ...(completion.accountId === undefined ? {} : { accountId: completion.accountId }),
651
753
  ...(completion.accountLabel === undefined ? {} : { accountLabel: completion.accountLabel }),
@@ -288,6 +288,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
288
288
  status: terminal.status,
289
289
  errorCategory: terminal.errorCategory,
290
290
  errorOrigin: terminal.errorOrigin,
291
+ ...(terminal.httpStatus === undefined ? {} : { terminalHttpStatus: terminal.httpStatus }),
291
292
  error: lastError,
292
293
  tenantId: input.tenantId,
293
294
  terminal: true,
@@ -419,6 +420,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
419
420
  status: terminal.status,
420
421
  errorCategory: terminal.errorCategory,
421
422
  errorOrigin: terminal.errorOrigin,
423
+ ...(terminal.httpStatus === undefined ? {} : { terminalHttpStatus: terminal.httpStatus }),
422
424
  error,
423
425
  tenantId: input.tenantId,
424
426
  terminal: terminalAttempt,
@@ -539,6 +541,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
539
541
  status: terminal.status,
540
542
  errorCategory: terminal.errorCategory,
541
543
  errorOrigin: terminal.errorOrigin,
544
+ ...(terminal.httpStatus === undefined ? {} : { terminalHttpStatus: terminal.httpStatus }),
542
545
  error: lastError,
543
546
  terminal: true,
544
547
  tenantId: input.tenantId,
@@ -87,7 +87,9 @@ export function isEmptyStream(tally: StreamTally): boolean {
87
87
  * failing over across ordered endpoints pre-start.
88
88
  *
89
89
  * Throws GatewayError (platform_unavailable, 502, upstream) on exhaustion,
90
- * GatewayError (transport_closed, 499) on abort.
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.
91
93
  */
92
94
  export async function fetchWithEmptyStreamRecovery(
93
95
  endpoints: readonly string[],
@@ -197,7 +199,12 @@ async function fetchWithTimeout(
197
199
  // Race the fetch against a first-event timeout
198
200
  const timeoutPromise = new Promise<never>((_, reject) => {
199
201
  const timer = setTimeout(() => {
200
- reject(new GatewayError("transport_closed", 499, "First event timeout", {}, "upstream"));
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"));
201
208
  }, timeoutMs);
202
209
  signal.addEventListener("abort", () => {
203
210
  clearTimeout(timer);
@@ -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) {