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
@@ -1,238 +1,245 @@
1
- // Per-request context, client identity, canonical parse, and route preparation.
2
- import { Elysia } from "elysia";
3
- import { GatewayError } from "../../protocol/gateway-error";
4
- import type { TrustedProxyBoundary } from "../../config/network";
5
- import { resolveUpstreamTimeoutMs } from "../../config/timeouts";
6
- import type { ProxyRequestStateStore } from "../request/state";
7
- import { fastPathname } from "../pathname";
8
- import type { ProxyRequestPreparer } from "../request/preparer";
9
- import type { CanonicalAdapter } from "../contracts";
10
- import type { SurfaceAdapterRegistry } from "../surface/adapters";
11
- import { resolveClientIdentity } from "../../security/ip-boundary";
12
- import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
13
- import { pushStructuredConsoleLog } from "../../observability/log-ring";
14
- import { log } from "../../observability/logger";
15
- import { isJsonProxyRoutePath, isProxyDispatchRoute } from "./body-policy";
16
- import { isOpaqueBodyRoute } from "../contracts";
17
- import { peerAddressUnavailableError } from "./guard-errors";
18
-
19
-
20
-
21
- interface RequestApp {
22
- request(
23
- handler: (context: { request: Request; set?: { headers: Record<string, string> } }) => void,
24
- ): RequestApp;
25
- }
26
-
27
- export interface RequestContextDeps {
28
- readonly stateStore: ProxyRequestStateStore;
29
- readonly clock?: () => number;
30
- /** Explicit deadline override; defaults to `CARTETHYIA_UPSTREAM_TIMEOUT_MS` (120s). */
31
- readonly requestDeadlineMs?: number;
32
- readonly maxBodyBytes?: number;
33
- }
34
-
35
- /**
36
- * Initializes per-request `ProxyRequestState` before any other hook runs.
37
- * Only gateway (`/v1/*`) requests get state: this middleware is mounted at
38
- * the composition root so it also observes health checks, console traffic,
39
- * and dashboard assets, but none of those use proxy state and none of them
40
- * run the `/v1`-scoped cleanup hooks — initializing for them leaked a state,
41
- * a deadline timer, a live-controller entry, and an in-flight count per
42
- * request that was never reclaimed. Cleanup (abort + WeakMap eviction) is
43
- * intentionally NOT done here via `afterResponse`: Elysia's afterResponse
44
- * hook order across plugins is not something this composition can guarantee
45
- * ahead of `createTelemetryLifecycleMiddleware`'s afterResponse (now in
46
- * lifecycle.ts), which still needs to read the state. The WeakMap entry is
47
- * reclaimed by GC once `request` is unreferenced; nothing needs to run after
48
- * the response is sent.
49
- */
50
- export function createRequestContextMiddleware(deps: RequestContextDeps): Elysia {
51
- const app = new Elysia() as unknown as RequestApp;
52
- app.request(({ request, set }) => {
53
- const pathname = fastPathname(request.url);
54
- if (pathname !== "/v1" && !pathname.startsWith("/v1/")) return;
55
- const state = deps.stateStore.initialize(
56
- request,
57
- (deps.clock ?? (() => Date.now()))(),
58
- deps.requestDeadlineMs ?? resolveUpstreamTimeoutMs(),
59
- );
60
- if (set) {
61
- set.headers["x-request-id"] = state.requestId;
62
- Object.assign(set.headers, GATEWAY_SECURITY_HEADERS);
63
- }
64
- });
65
- return app as unknown as Elysia;
66
- }
67
-
68
- export function createClientIdentityMiddleware(deps: {
69
- readonly stateStore: ProxyRequestStateStore;
70
- readonly trustedProxyBoundary: TrustedProxyBoundary;
71
- readonly resolvePeerAddress?: (request: Request) => string | null;
72
- }): Elysia {
73
- // `beforeHandle`, not `onRequest`: plugin-scoped `onRequest` hooks do not
74
- // fire for these routes under Elysia 2 beta (same scoping gap that muted
75
- // plugin `afterResponse` before `registerTelemetryLifecycle`), which left
76
- // `clientIdentity` unset and every gateway row without a client IP.
77
- // `beforeHandle` hooks from `.use()` plugins run in registration order, so
78
- // pipeline position still guarantees this executes before IP-abuse checks.
79
- return new Elysia()
80
- .beforeHandle(({ request, server }) => {
81
- const peer = deps.resolvePeerAddress
82
- ? deps.resolvePeerAddress(request)
83
- : (server?.requestIP(request)?.address ?? null);
84
- if (!peer)
85
- throw peerAddressUnavailableError();
86
- const address = resolveClientIdentity(request, deps.trustedProxyBoundary, peer);
87
- const state = deps.stateStore.require(request);
88
- state.clientIdentity = {
89
- address,
90
- source: address === peer ? "tcp-peer" : "trusted-forwarded-header",
91
- };
92
- const path = state.ingressPath;
93
- if (!isProxyDispatchRoute(path)) return;
94
- // The ingress policy stage already decoded the body, so the client's
95
- // requested model is readable here — before canonical parsing runs. It is
96
- // read defensively and only for display: routing still resolves the model
97
- // through the surface adapter, and a body that is not an object simply
98
- // yields no model rather than failing the request.
99
- const body = state.ingressBody;
100
- const requestedModel =
101
- body !== null && typeof body === "object" && !Array.isArray(body)
102
- ? (body as Record<string, unknown>)["model"]
103
- : undefined;
104
- const userAgent = request.headers.get("user-agent");
105
- pushStructuredConsoleLog("info", "Incoming proxy request", {
106
- event: "request_start",
107
- requestId: state.requestId,
108
- method: request.method,
109
- endpoint: path,
110
- clientIp: address,
111
- ...(typeof requestedModel === "string" && requestedModel.length > 0
112
- ? { model: requestedModel }
113
- : {}),
114
- ...(userAgent === null || userAgent.length === 0
115
- ? {}
116
- : { userAgent: userAgent.slice(0, 512) }),
117
- });
118
- })
119
- .as("plugin");
120
- }
121
-
122
- export function createCanonicalRequestMiddleware(deps: {
123
- readonly stateStore: ProxyRequestStateStore;
124
- readonly surfaceRegistry: SurfaceAdapterRegistry;
125
- readonly adapters: ReadonlyMap<string, CanonicalAdapter>;
126
- /**
127
- * Whether this tenant's payload-capture mode keeps request bodies
128
- * (`full`). `none`/`metadata` never read the request body at finalize, so
129
- * the parsed ingress body can be released as soon as the canonical request
130
- * exists instead of being held — across the whole upstream stream. Injected
131
- * so this stage does not import the capture policy or the database.
132
- */
133
- readonly keepsIngressBody: (tenantId: string | null) => Promise<boolean>;
134
- }): Elysia {
135
- return new Elysia()
136
- .beforeHandle(async ({ request }) => {
137
- const path = fastPathname(request.url);
138
- if (!path.startsWith("/v1/")) return;
139
- if (request.method === "GET" || request.method === "HEAD") return;
140
- // `/v1/completions` needs no separate arm: it is already in
141
- // the JSON routes table, so the body-policy predicate covers it.
142
- // Native routes (compact, System One) read a body but are never parsed
143
- // into a canonical request — their bodies are opaque by contract.
144
- if (!isJsonProxyRoutePath(path) || path === "/v1/responses/compact" || isOpaqueBodyRoute(path))
145
- return;
146
- // Model discovery and other GET/multimodal routes must not go through canonical parsing.
147
- const state = deps.stateStore.require(request);
148
- // Single-read invariant: the ingress policy middleware already decoded the
149
- // body once into `state.ingressBody`. Never re-read `request.body` here —
150
- // it is already consumed/locked. `undefined` means the ingress stage did
151
- // not run (reduced composition); fail closed. An explicit `null` (JSON
152
- // literal `null` body) is a valid single-read value and flows to the
153
- // surface adapter's parse error rather than triggering a second read.
154
- if (state.ingressBody === undefined)
155
- throw new GatewayError("admission_unavailable", 503, "The request body was never read, so this route cannot be served. Resend the request.");
156
- const body = state.ingressBody;
157
- const headers: Record<string, string> = {};
158
- request.headers.forEach((value, key) => {
159
- headers[key] = value;
160
- });
161
- const detection = deps.surfaceRegistry.detectOnce({ body, headers, path });
162
- if (detection.disagreement) {
163
- // A path-vs-body disagreement is expected traffic, not a fault: a
164
- // standard chat body ({model, messages[], max_tokens}) also matches
165
- // the messages body shape, so nearly every chat request with
166
- // max_tokens "disagrees". Warn only when the explicit header is
167
- // involved — a marker contradicting the path means the client is
168
- // genuinely confused about which surface it wants.
169
- const markerInvolved = detection.signals.some((s) => s.source === "explicit_marker");
170
- if (markerInvolved) {
171
- log.warn("[surface] conflicting signals detected for surface routing", {
172
- resolved: detection.surface,
173
- winning_source: detection.winning_source,
174
- signals: detection.signals,
175
- });
176
- } else {
177
- log.debug("[surface] ambiguous body shape resolved by endpoint path", {
178
- resolved: detection.surface,
179
- signals: detection.signals,
180
- });
181
- }
182
- }
183
- const adapter = deps.adapters.get(detection.surface);
184
- if (!adapter)
185
- throw new GatewayError(
186
- "capability_unsupported",
187
- 400,
188
- `Surface adapter unavailable: ${detection.surface}`,
189
- { surface: detection.surface },
190
- );
191
- const ingress = deps.stateStore.require(request);
192
- const userAgent = headers["user-agent"];
193
- if (userAgent) ingress.clientUserAgent = userAgent.slice(0, 512);
194
- ingress.canonicalRequest = adapter.parse({ body, headers, path });
195
- // The parsed body is the largest per-request value, and once the
196
- // canonical request exists the only later reader is payload capture — and
197
- // only in `full` mode (see `captureTerminalPayload`). For `none`/`metadata`
198
- // drop the reference now: held until cleanup, it would otherwise stay
199
- // resident for the entire upstream stream.
200
- if (!(await deps.keepsIngressBody(ingress.authorization?.tenantId ?? null))) {
201
- ingress.releaseIngressBody();
202
- }
203
- })
204
- .as("plugin");
205
- }
206
-
207
- export function createProxyRoutePreparationMiddleware(deps: {
208
- readonly stateStore: ProxyRequestStateStore;
209
- readonly preparer: ProxyRequestPreparer;
210
- }): Elysia {
211
- return new Elysia()
212
- .beforeHandle(async ({ request }) => {
213
- const path = fastPathname(request.url);
214
- if (!path.startsWith("/v1/")) return;
215
- if (request.method === "GET" || request.method === "HEAD") return;
216
- const isJsonRoute = isJsonProxyRoutePath(path);
217
- if (!isJsonRoute || path === "/v1/responses/compact" || isOpaqueBodyRoute(path)) return;
218
- const state = deps.stateStore.require(request);
219
- if (!state.authorization || !state.canonicalRequest)
220
- throw new GatewayError("admission_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served. Resend the request.");
221
- // The key rides along so a strike is charged to the caller that made it.
222
- // Without it, one recipient of a shared link could escalate a ban onto
223
- try {
224
- state.preparedRequest = await deps.preparer.prepare({
225
- canonicalRequest: state.canonicalRequest,
226
- authorization: state.authorization,
227
- deadlineMs: state.deadlineMs,
228
- signal: state.abortController.signal,
229
- ...(state.clientUserAgent === undefined
230
- ? {}
231
- : { clientUserAgent: state.clientUserAgent }),
232
- });
233
- } catch (error) {
234
- throw error;
235
- }
236
- })
237
- .as("plugin");
238
- }
1
+ // Per-request context, client identity, canonical parse, and route preparation.
2
+ import { Elysia } from "elysia";
3
+ import { GatewayError } from "../../protocol/gateway-error";
4
+ import type { TrustedProxyBoundary } from "../../config/network";
5
+ import { resolveUpstreamTimeoutMs } from "../../config/timeouts";
6
+ import type { ProxyRequestStateStore } from "../request/state";
7
+ import { fastPathname } from "../pathname";
8
+ import type { ProxyRequestPreparer } from "../request/preparer";
9
+ import type { CanonicalAdapter } from "../contracts";
10
+ import type { SurfaceAdapterRegistry } from "../surface/adapters";
11
+ import { resolveClientIdentity } from "../../security/ip-boundary";
12
+ import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
13
+ import { pushStructuredConsoleLog } from "../../observability/log-ring";
14
+ import { log } from "../../observability/logger";
15
+ import { isJsonProxyRoutePath, isProxyDispatchRoute } from "./body-policy";
16
+ import { isOpaqueBodyRoute } from "../contracts";
17
+ import { peerAddressUnavailableError } from "./guard-errors";
18
+
19
+
20
+
21
+ interface RequestApp {
22
+ request(
23
+ handler: (context: { request: Request; set?: { headers: Record<string, string> } }) => void,
24
+ ): RequestApp;
25
+ }
26
+
27
+ export interface RequestContextDeps {
28
+ readonly stateStore: ProxyRequestStateStore;
29
+ readonly clock?: () => number;
30
+ /** Explicit deadline override; defaults to `CARTETHYIA_UPSTREAM_TIMEOUT_MS` (120s). */
31
+ readonly requestDeadlineMs?: number;
32
+ }
33
+
34
+ /**
35
+ * Initializes per-request `ProxyRequestState` before any other hook runs.
36
+ * Only gateway (`/v1/*`) requests get state: this middleware is mounted at
37
+ * the composition root so it also observes health checks, console traffic,
38
+ * and dashboard assets, but none of those use proxy state and none of them
39
+ * run the `/v1`-scoped cleanup hooks — initializing for them leaked a state,
40
+ * a deadline timer, a live-controller entry, and an in-flight count per
41
+ * request that was never reclaimed. Cleanup (abort + WeakMap eviction) is
42
+ * intentionally NOT done here via `afterResponse`: Elysia's afterResponse
43
+ * hook order across plugins is not something this composition can guarantee
44
+ * ahead of `createTelemetryLifecycleMiddleware`'s afterResponse (now in
45
+ * lifecycle.ts), which still needs to read the state. The WeakMap entry is
46
+ * reclaimed by GC once `request` is unreferenced; nothing needs to run after
47
+ * the response is sent.
48
+ */
49
+ export function createRequestContextMiddleware(deps: RequestContextDeps): Elysia {
50
+ const app = new Elysia() as unknown as RequestApp;
51
+ app.request(({ request, set }) => {
52
+ const pathname = fastPathname(request.url);
53
+ if (pathname !== "/v1" && !pathname.startsWith("/v1/")) return;
54
+ const state = deps.stateStore.initialize(
55
+ request,
56
+ (deps.clock ?? (() => Date.now()))(),
57
+ deps.requestDeadlineMs ?? resolveUpstreamTimeoutMs(),
58
+ );
59
+ if (set) {
60
+ set.headers["x-request-id"] = state.requestId;
61
+ Object.assign(set.headers, GATEWAY_SECURITY_HEADERS);
62
+ }
63
+ });
64
+ return app as unknown as Elysia;
65
+ }
66
+
67
+ export function createClientIdentityMiddleware(deps: {
68
+ readonly stateStore: ProxyRequestStateStore;
69
+ readonly trustedProxyBoundary: TrustedProxyBoundary;
70
+ readonly resolvePeerAddress?: (request: Request) => string | null;
71
+ }): Elysia {
72
+ // `beforeHandle`, not `onRequest`: plugin-scoped `onRequest` hooks do not
73
+ // fire for these routes under Elysia 2 beta (same scoping gap that muted
74
+ // plugin `afterResponse` before `registerTelemetryLifecycle`), which left
75
+ // `clientIdentity` unset and every gateway row without a client IP.
76
+ // `beforeHandle` hooks from `.use()` plugins run in registration order, so
77
+ // pipeline position still guarantees this executes before IP-abuse checks.
78
+ return new Elysia()
79
+ .beforeHandle(({ request, server }) => {
80
+ const state = deps.stateStore.require(request);
81
+ // The IP-abuse root hook already resolved the same identity from the
82
+ // same boundary (and only for /v1/*, exactly these routes); reuse it so
83
+ // a request pays one resolveClientIdentity instead of two. Fall back to
84
+ // resolving here when running in a reduced composition without the
85
+ // abuse guard.
86
+ if (!state.clientIdentity) {
87
+ const peer = deps.resolvePeerAddress
88
+ ? deps.resolvePeerAddress(request)
89
+ : (server?.requestIP(request)?.address ?? null);
90
+ if (!peer)
91
+ throw peerAddressUnavailableError();
92
+ const address = resolveClientIdentity(request, deps.trustedProxyBoundary, peer);
93
+ state.clientIdentity = {
94
+ address,
95
+ source: address === peer ? "tcp-peer" : "trusted-forwarded-header",
96
+ };
97
+ }
98
+ const address = state.clientIdentity.address;
99
+ const path = state.ingressPath;
100
+ if (!isProxyDispatchRoute(path)) return;
101
+ // The ingress policy stage already decoded the body, so the client's
102
+ // requested model is readable here — before canonical parsing runs. It is
103
+ // read defensively and only for display: routing still resolves the model
104
+ // through the surface adapter, and a body that is not an object simply
105
+ // yields no model rather than failing the request.
106
+ const body = state.ingressBody;
107
+ const requestedModel =
108
+ body !== null && typeof body === "object" && !Array.isArray(body)
109
+ ? (body as Record<string, unknown>)["model"]
110
+ : undefined;
111
+ const userAgent = request.headers.get("user-agent");
112
+ pushStructuredConsoleLog("info", "Incoming proxy request", {
113
+ event: "request_start",
114
+ requestId: state.requestId,
115
+ method: request.method,
116
+ endpoint: path,
117
+ clientIp: address,
118
+ ...(typeof requestedModel === "string" && requestedModel.length > 0
119
+ ? { model: requestedModel }
120
+ : {}),
121
+ ...(userAgent === null || userAgent.length === 0
122
+ ? {}
123
+ : { userAgent: userAgent.slice(0, 512) }),
124
+ });
125
+ })
126
+ .as("plugin");
127
+ }
128
+
129
+ export function createCanonicalRequestMiddleware(deps: {
130
+ readonly stateStore: ProxyRequestStateStore;
131
+ readonly surfaceRegistry: SurfaceAdapterRegistry;
132
+ readonly adapters: ReadonlyMap<string, CanonicalAdapter>;
133
+ /**
134
+ * Whether this tenant's payload-capture mode keeps request bodies
135
+ * (`full`). `none`/`metadata` never read the request body at finalize, so
136
+ * the parsed ingress body can be released as soon as the canonical request
137
+ * exists instead of being held — across the whole upstream stream. Injected
138
+ * so this stage does not import the capture policy or the database.
139
+ */
140
+ readonly keepsIngressBody: (tenantId: string | null) => Promise<boolean>;
141
+ }): Elysia {
142
+ return new Elysia()
143
+ .beforeHandle(async ({ request }) => {
144
+ const path = fastPathname(request.url);
145
+ if (!path.startsWith("/v1/")) return;
146
+ if (request.method === "GET" || request.method === "HEAD") return;
147
+ // `/v1/completions` needs no separate arm: it is already in
148
+ // the JSON routes table, so the body-policy predicate covers it.
149
+ // Native routes (compact, System One) read a body but are never parsed
150
+ // into a canonical request — their bodies are opaque by contract.
151
+ if (!isJsonProxyRoutePath(path) || path === "/v1/responses/compact" || isOpaqueBodyRoute(path))
152
+ return;
153
+ // Model discovery and other GET/multimodal routes must not go through canonical parsing.
154
+ const state = deps.stateStore.require(request);
155
+ // Single-read invariant: the ingress policy middleware already decoded the
156
+ // body once into `state.ingressBody`. Never re-read `request.body` here —
157
+ // it is already consumed/locked. `undefined` means the ingress stage did
158
+ // not run (reduced composition); fail closed. An explicit `null` (JSON
159
+ // literal `null` body) is a valid single-read value and flows to the
160
+ // surface adapter's parse error rather than triggering a second read.
161
+ if (state.ingressBody === undefined)
162
+ throw new GatewayError("admission_unavailable", 503, "The request body was never read, so this route cannot be served. Resend the request.");
163
+ const body = state.ingressBody;
164
+ const headers: Record<string, string> = {};
165
+ request.headers.forEach((value, key) => {
166
+ headers[key] = value;
167
+ });
168
+ const detection = deps.surfaceRegistry.detectOnce({ body, headers, path });
169
+ if (detection.disagreement) {
170
+ // A path-vs-body disagreement is expected traffic, not a fault: a
171
+ // standard chat body ({model, messages[], max_tokens}) also matches
172
+ // the messages body shape, so nearly every chat request with
173
+ // max_tokens "disagrees". Warn only when the explicit header is
174
+ // involved — a marker contradicting the path means the client is
175
+ // genuinely confused about which surface it wants.
176
+ const markerInvolved = detection.signals.some((s) => s.source === "explicit_marker");
177
+ if (markerInvolved) {
178
+ log.warn("[surface] conflicting signals detected for surface routing", {
179
+ resolved: detection.surface,
180
+ winning_source: detection.winning_source,
181
+ signals: detection.signals,
182
+ });
183
+ } else {
184
+ log.debug("[surface] ambiguous body shape resolved by endpoint path", {
185
+ resolved: detection.surface,
186
+ signals: detection.signals,
187
+ });
188
+ }
189
+ }
190
+ const adapter = deps.adapters.get(detection.surface);
191
+ if (!adapter)
192
+ throw new GatewayError(
193
+ "capability_unsupported",
194
+ 400,
195
+ `Surface adapter unavailable: ${detection.surface}`,
196
+ { surface: detection.surface },
197
+ );
198
+ const ingress = deps.stateStore.require(request);
199
+ const userAgent = headers["user-agent"];
200
+ if (userAgent) ingress.clientUserAgent = userAgent.slice(0, 512);
201
+ ingress.canonicalRequest = adapter.parse({ body, headers, path });
202
+ // The parsed body is the largest per-request value, and once the
203
+ // canonical request exists the only later reader is payload capture — and
204
+ // only in `full` mode (see `captureTerminalPayload`). For `none`/`metadata`
205
+ // drop the reference now: held until cleanup, it would otherwise stay
206
+ // resident for the entire upstream stream.
207
+ if (!(await deps.keepsIngressBody(ingress.authorization?.tenantId ?? null))) {
208
+ ingress.releaseIngressBody();
209
+ }
210
+ })
211
+ .as("plugin");
212
+ }
213
+
214
+ export function createProxyRoutePreparationMiddleware(deps: {
215
+ readonly stateStore: ProxyRequestStateStore;
216
+ readonly preparer: ProxyRequestPreparer;
217
+ }): Elysia {
218
+ return new Elysia()
219
+ .beforeHandle(async ({ request }) => {
220
+ const path = fastPathname(request.url);
221
+ if (!path.startsWith("/v1/")) return;
222
+ if (request.method === "GET" || request.method === "HEAD") return;
223
+ const isJsonRoute = isJsonProxyRoutePath(path);
224
+ if (!isJsonRoute || path === "/v1/responses/compact" || isOpaqueBodyRoute(path)) return;
225
+ const state = deps.stateStore.require(request);
226
+ if (!state.authorization || !state.canonicalRequest)
227
+ throw new GatewayError("admission_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served. Resend the request.");
228
+ // The key rides along so a strike is charged to the caller that made it.
229
+ // Without it, one recipient of a shared link could escalate a ban onto
230
+ try {
231
+ state.preparedRequest = await deps.preparer.prepare({
232
+ canonicalRequest: state.canonicalRequest,
233
+ authorization: state.authorization,
234
+ deadlineMs: state.deadlineMs,
235
+ signal: state.abortController.signal,
236
+ ...(state.clientUserAgent === undefined
237
+ ? {}
238
+ : { clientUserAgent: state.clientUserAgent }),
239
+ });
240
+ } catch (error) {
241
+ throw error;
242
+ }
243
+ })
244
+ .as("plugin");
245
+ }
@@ -7,7 +7,7 @@
7
7
  * the planner warns (throttled here) when a request was dispatched degraded.
8
8
  */
9
9
  import type { CanonicalRequest, ContentPart } from "../../protocol/canonical-model";
10
- import { deriveRequiredCapabilities, type RequiredCapability } from "../translation/capabilities";
10
+ import { deriveRequiredCapabilities, type RequiredCapability } from "../../protocol/translation/capabilities";
11
11
 
12
12
  // Degradation is often sustained — a client that always asks for tools against
13
13
  // a tools-less route degrades every single request. An unthrottled warn would
@@ -261,6 +261,25 @@ export function* degradedRequestVariants(
261
261
  "audio",
262
262
  ] as RequiredCapability[];
263
263
  const ordered = priority.filter((c) => required.includes(c));
264
+ // Pre-pass: drop exactly one capability from the pristine request. The
265
+ // cumulative chain below can only ADD losses, so when the plan's real blocker
266
+ // is a late capability in this order (an image on a text-only model), every
267
+ // surviving chain variant has already lost `tools` as collateral — a
268
+ // tool-capable model then served a transcript of `[tool:<name>]` placeholders
269
+ // with no tool catalog at all. Trying each single loss first finds the
270
+ // smallest degradation that satisfies routing: `degraded: [cap]` alone is
271
+ // always lexicographically smaller than the chain's `[...earlier, cap]`, so
272
+ // a single-loss win must be preferred, never the reverse.
273
+ for (const cap of ordered) {
274
+ const singular = degradeRequestForCapability(original, cap);
275
+ if (singular) {
276
+ yield {
277
+ request: singular,
278
+ degraded: [cap],
279
+ required: required.filter((c) => c !== cap),
280
+ };
281
+ }
282
+ }
264
283
  let current: CanonicalRequest = original;
265
284
  const degraded: RequiredCapability[] = [];
266
285
  for (const cap of ordered) {