cartethyia 2.7.8 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/.env.example +3 -3
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  46. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
  48. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
  50. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  51. package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
  52. package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
  53. package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
  54. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  55. package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
  56. package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
  57. package/dist/dashboard/assets/brand/favicon.webp +0 -0
  58. package/dist/dashboard/assets/brand/login_register.webp +0 -0
  59. package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
  60. package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
  61. package/dist/dashboard/assets/brand/og_image.webp +0 -0
  62. package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
  63. package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
  64. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
  70. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
  72. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  74. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  75. package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
  76. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
  78. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
  80. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
  82. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
  84. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
  86. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
  88. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
  90. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
  92. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
  94. package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
  95. package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
  96. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
  98. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
  100. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  101. package/dist/dashboard/assets/providers/devin.svg +9 -0
  102. package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
  103. package/dist/dashboard/assets/providers/windsurf.svg +10 -0
  104. package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
  105. package/dist/dashboard/assets/providers/zro.svg +3 -0
  106. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  107. package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  108. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  109. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  110. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
  112. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
  114. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
  116. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  117. package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
  118. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  119. package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
  120. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  121. package/dist/dashboard/assets/story/25817331.webp +0 -0
  122. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  123. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  124. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  125. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  126. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  127. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  128. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  129. package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
  130. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  131. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
  132. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  133. package/dist/dashboard/console-backgrounds.webp +0 -0
  134. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  135. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  136. package/dist/dashboard/index.html +3 -3
  137. package/dist/dashboard/index.html.gz +0 -0
  138. package/dist/dashboard/precompressed-assets.json +59 -56
  139. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  140. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  141. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  142. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  143. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  144. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  145. package/package.json +1 -1
  146. package/src/boot/app.ts +10 -13
  147. package/src/boot/dependencies.ts +12 -4
  148. package/src/boot/flush.ts +20 -0
  149. package/src/boot/lifecycle.ts +7 -5
  150. package/src/config/cooldowns.ts +1 -1
  151. package/src/config/routing.ts +26 -0
  152. package/src/config/spec-metadata.ts +72 -0
  153. package/src/config/spec.ts +20 -6
  154. package/src/console/api-keys/contracts.ts +12 -1
  155. package/src/console/api-keys/routes.ts +50 -31
  156. package/src/console/auth/account-reset.ts +7 -9
  157. package/src/console/backup/contracts.ts +4 -0
  158. package/src/console/backup/export-endpoint.ts +7 -5
  159. package/src/console/cli-tools/service.ts +6 -12
  160. package/src/console/console-router-guards.ts +23 -16
  161. package/src/console/console-router.ts +7 -0
  162. package/src/console/dashboard-assets.ts +5 -5
  163. package/src/console/domain-registration.ts +27 -2
  164. package/src/console/inferhub/normalize.ts +24 -28
  165. package/src/console/inferhub/types.ts +3 -0
  166. package/src/console/ip-bans/routes.ts +93 -0
  167. package/src/console/observability/store.ts +0 -53
  168. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  169. package/src/console/providers/catalog/contracts.ts +123 -0
  170. package/src/console/providers/catalog/discover-constants.ts +6 -0
  171. package/src/console/providers/catalog/model-operations.ts +122 -10
  172. package/src/console/providers/catalog/provider-operations.ts +116 -8
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/routes.ts +65 -4
  175. package/src/console/providers/catalog/store-helpers.ts +30 -1
  176. package/src/console/providers/catalog/store.ts +249 -4
  177. package/src/console/quota/account-quota-global.ts +97 -19
  178. package/src/console/quota/account-quota-shared.ts +49 -0
  179. package/src/console/quota/account-quota-tenant.ts +6 -18
  180. package/src/console/quota/native-identity.ts +2 -0
  181. package/src/console/quota/quota-view.ts +19 -3
  182. package/src/console/routing/pools/operations.ts +31 -46
  183. package/src/console/settings/contracts.ts +18 -0
  184. package/src/console/settings/store.ts +32 -0
  185. package/src/console/shared/errors.ts +33 -6
  186. package/src/console/system-prompt/contracts.ts +0 -1
  187. package/src/main.ts +3 -3
  188. package/src/network/pool/byte-accounting.ts +3 -2
  189. package/src/network/pool/selector.ts +182 -39
  190. package/src/network/pool-cooldown-store.ts +89 -14
  191. package/src/network/pool-health.ts +49 -61
  192. package/src/network/rate-limit-verdict.ts +1 -1
  193. package/src/network/ssrf.ts +6 -6
  194. package/src/observability/event-log.ts +7 -361
  195. package/src/observability/event-query.ts +18 -4
  196. package/src/observability/performance-metrics.ts +9 -0
  197. package/src/observability/runtime-metrics.ts +14 -9
  198. package/src/observability/telemetry-buffer.ts +15 -25
  199. package/src/observability/telemetry-reconciliation.ts +9 -37
  200. package/src/observability/telemetry-row.ts +125 -0
  201. package/src/persistence/db-mode.ts +4 -1
  202. package/src/persistence/dialect.ts +30 -0
  203. package/src/persistence/migrate.ts +6 -3
  204. package/src/persistence/schema.ts +28 -2
  205. package/src/persistence/ttl-cache.ts +20 -2
  206. package/src/protocol/codecs.ts +1 -1
  207. package/src/protocol/dsml.ts +35 -87
  208. package/src/protocol/gateway-error.ts +1 -0
  209. package/src/protocol/markup-stream-parser.ts +195 -0
  210. package/src/protocol/reasoning-text.ts +19 -0
  211. package/src/protocol/reasoning.ts +1 -1
  212. package/src/protocol/request/chat.ts +14 -14
  213. package/src/protocol/request/gemini.ts +8 -2
  214. package/src/protocol/request/messages.ts +4 -5
  215. package/src/protocol/response/chat.ts +12 -20
  216. package/src/protocol/response/gemini.ts +6 -1
  217. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  218. package/src/protocol/response/responses.ts +18 -2
  219. package/src/protocol/sse-decoder.ts +25 -8
  220. package/src/protocol/stop-reason.ts +23 -1
  221. package/src/protocol/stream-error-frames.ts +13 -3
  222. package/src/protocol/think-split.ts +112 -0
  223. package/src/protocol/translation/dialect/qwen.ts +26 -58
  224. package/src/protocol/translation/quirks.ts +1 -1
  225. package/src/protocol/translation/tool-repair.ts +2 -2
  226. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  227. package/src/protocol/upstream-parsing.ts +1 -1
  228. package/src/providers/adapters/compatible-adapter.ts +78 -17
  229. package/src/providers/authentication/oauth-client.ts +1 -1
  230. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  231. package/src/providers/authentication/oauth-refresh-service.ts +153 -59
  232. package/src/providers/configured-provider.ts +148 -0
  233. package/src/providers/default-registry.ts +117 -27
  234. package/src/providers/discovery/discovery-types.ts +12 -0
  235. package/src/providers/discovery/probe-phases.ts +74 -33
  236. package/src/providers/discovery/probing-service.ts +293 -165
  237. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  238. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  239. package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
  240. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  241. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  242. package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
  243. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  244. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  245. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  246. package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
  247. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  248. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  249. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  250. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  251. package/src/providers/integrations/buddy/capabilities.ts +6 -43
  252. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  253. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
  254. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  255. package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
  256. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  257. package/src/providers/integrations/cline/cline.ts +2 -2
  258. package/src/providers/integrations/commandcode/quota.ts +22 -28
  259. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  260. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  261. package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
  262. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  263. package/src/providers/integrations/cursor/cursor-request.ts +35 -20
  264. package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
  265. package/src/providers/integrations/cursor/cursor.ts +139 -70
  266. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  267. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  268. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  269. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  270. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  271. package/src/providers/integrations/devin/devin-http.ts +91 -0
  272. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  273. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  274. package/src/providers/integrations/devin/devin.ts +539 -0
  275. package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
  276. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  277. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  278. package/src/providers/integrations/factory/factory.ts +2 -2
  279. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  280. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  281. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  282. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  283. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  284. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  285. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  286. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  287. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  288. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  289. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  290. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  291. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  292. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  293. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  294. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  295. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  296. package/src/providers/integrations/freepi/freepi.ts +292 -0
  297. package/src/providers/integrations/gemini.ts +2 -2
  298. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  299. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  300. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  301. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  302. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
  303. package/src/providers/integrations/hermes/index.ts +2 -0
  304. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  305. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  306. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  307. package/src/providers/integrations/kiro/kiro.ts +15 -19
  308. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  309. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  310. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  311. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  312. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  313. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  314. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  315. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  316. package/src/providers/integrations/perch/perch-protocol.ts +208 -0
  317. package/src/providers/integrations/perch/perch-quota.ts +123 -0
  318. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  319. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  320. package/src/providers/integrations/perch/perch.ts +305 -0
  321. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  322. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  323. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  324. package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
  325. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  326. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  327. package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
  328. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  329. package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
  330. package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
  331. package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
  332. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  333. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  334. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  335. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  336. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  337. package/src/providers/integrations/qoder/qoder.ts +3 -7
  338. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  339. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  340. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  341. package/src/providers/integrations/trae/trae-protocol.ts +12 -11
  342. package/src/providers/integrations/trae/trae-quota.ts +96 -36
  343. package/src/providers/integrations/trae/trae-realm.ts +74 -15
  344. package/src/providers/integrations/trae/trae.ts +99 -26
  345. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  346. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  347. package/src/providers/integrations/zro/zro.ts +204 -0
  348. package/src/providers/operations/account-error-classification.ts +235 -278
  349. package/src/providers/operations/account-health-service.ts +273 -26
  350. package/src/providers/operations/account-refresh-status.ts +269 -0
  351. package/src/providers/operations/account-tier.ts +66 -0
  352. package/src/providers/operations/client-version-resolver.ts +1 -1
  353. package/src/providers/operations/client-versions.ts +42 -150
  354. package/src/providers/operations/cooldown-rules.ts +56 -0
  355. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  356. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  357. package/src/providers/operations/provider-catalog-service.ts +1 -1
  358. package/src/providers/operations/provider-credential-service.ts +102 -32
  359. package/src/providers/operations/provider-rules/github.ts +48 -0
  360. package/src/providers/operations/provider-rules/grok.ts +33 -0
  361. package/src/providers/operations/provider-rules/inception.ts +49 -0
  362. package/src/providers/operations/provider-rules/index.ts +32 -0
  363. package/src/providers/operations/provider-rules/perch.ts +37 -0
  364. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  365. package/src/providers/operations/provider-rules/trae.ts +81 -0
  366. package/src/providers/provider-contracts.ts +33 -2
  367. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  368. package/src/providers/provider-metadata.ts +99 -14
  369. package/src/providers/provider-model-definition.ts +36 -0
  370. package/src/providers/provider-probe.ts +15 -1
  371. package/src/providers/provider-registry.ts +42 -33
  372. package/src/providers/quota/quota-limit-windows.ts +16 -5
  373. package/src/providers/quota/quota-support.ts +1 -1
  374. package/src/providers/quota/quota-window-parser.ts +90 -18
  375. package/src/providers/quota/refresh.ts +96 -18
  376. package/src/security/abuse.ts +71 -0
  377. package/src/security/api-key-auth-cache.ts +1 -1
  378. package/src/security/api-key-auth.ts +6 -2
  379. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  380. package/src/transport/dispatch/attempt-loop.ts +71 -17
  381. package/src/transport/dispatch/generation.ts +2 -8
  382. package/src/transport/dispatch/native-shared.ts +25 -0
  383. package/src/transport/dispatch/proxy-request.ts +9 -46
  384. package/src/transport/dispatch/responses-compact.ts +2 -8
  385. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  386. package/src/transport/dispatch/streaming/stream.ts +9 -36
  387. package/src/transport/dispatch/systemone.ts +2 -8
  388. package/src/transport/dispatch/token-count.ts +2 -6
  389. package/src/transport/dispatch/upstream.ts +67 -1
  390. package/src/transport/drain-endpoint.ts +31 -4
  391. package/src/transport/failure-policy.ts +129 -37
  392. package/src/transport/guards/body-policy.ts +7 -1
  393. package/src/transport/guards/error-lifecycle.ts +13 -1
  394. package/src/transport/guards/request-context.ts +22 -14
  395. package/src/transport/request/preparer.ts +81 -71
  396. package/src/transport/routing/bounded-lru.ts +14 -0
  397. package/src/transport/routing/drain-reserve.ts +129 -0
  398. package/src/transport/routing/plan-gate.ts +7 -1
  399. package/src/transport/routing/round-robin.ts +60 -0
  400. package/src/transport/routing/route-catalog.ts +123 -111
  401. package/src/transport/routing/route-model.ts +51 -9
  402. package/src/transport/routing/router.ts +418 -33
  403. package/src/transport/surface/adapters.ts +11 -2
  404. package/src/transport/surface/chat/adapter.ts +2 -2
  405. package/src/transport/surface/chat/encode.ts +47 -4
  406. package/src/transport/surface/chat/parse.ts +2 -12
  407. package/src/transport/surface/completion.ts +44 -33
  408. package/src/transport/surface/content-parts.ts +21 -0
  409. package/src/transport/surface/messages/adapter.ts +9 -2
  410. package/src/transport/surface/messages/encode.ts +11 -3
  411. package/src/transport/surface/messages/parse.ts +2 -1
  412. package/src/transport/surface/messages/stream.ts +33 -17
  413. package/src/transport/surface/responses/encode.ts +34 -6
  414. package/src/transport/surface/responses/parse.ts +3 -11
  415. package/src/workers/daily-checkin.ts +1 -1
  416. package/src/workers/quota-refresh-sweep.ts +1 -1
  417. package/src/workers/scheduler.ts +22 -4
  418. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  419. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  420. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  421. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  422. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  423. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  424. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  425. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  426. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  427. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  428. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  429. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  430. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  431. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  432. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  433. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  434. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  435. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  436. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  437. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  438. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  439. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  440. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  441. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  442. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
  443. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  444. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  445. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  446. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  447. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  448. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  449. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  450. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  451. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  452. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  453. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  454. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  455. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  456. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  457. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  458. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  459. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  460. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  461. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  462. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  463. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  464. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  465. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  466. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  467. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  468. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  469. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  470. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  471. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  472. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  473. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  474. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  475. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  476. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  477. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  478. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  479. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  480. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  481. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  482. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  483. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  484. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  485. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  486. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  487. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  488. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  489. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  490. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  491. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  492. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  493. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  494. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  495. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  496. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  497. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  498. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  499. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  500. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  501. package/dist/dashboard/console-backgrounds.jpg +0 -0
  502. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  503. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  504. package/src/providers/bundled-discovery-providers.ts +0 -98
  505. package/src/providers/configured-openai-providers.ts +0 -120
  506. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  507. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  508. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  509. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  510. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  511. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  512. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  513. package/src/providers/operations/model-discovery-cache.ts +0 -34
  514. package/src/providers/operations/probe-fetch.ts +0 -15
  515. package/src/providers/operations/provider-version-cache.ts +0 -30
  516. package/src/providers/provider-capabilities.ts +0 -33
  517. package/src/providers/provider-credential-envelope.ts +0 -43
  518. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
  519. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -23,7 +23,7 @@ import type { AdmissionLease } from "../../../security/admission/contracts";
23
23
  import type { Reservation } from "../../routing/route-model";
24
24
  import { repriceUsage } from "../../../providers/provider-usage";
25
25
  import { metrics } from "../../../observability/metrics";
26
- import { flagPoolBlocked, flagPoolCooldown } from "../../../network/pool-health";
26
+ import { flagPoolOnEgressFailure } from "../../../network/pool-health";
27
27
  import { classifyEgressFailure } from "../../../network/rate-limit-verdict";
28
28
  import { releaseAttemptLeases } from "../leases";
29
29
  import type { AttemptLeaseOwnership } from "../leases";
@@ -581,11 +581,14 @@ export class StreamFinalizer {
581
581
  this.streamSettled = true;
582
582
  if (candidate.networkPoolId && !cancelled && deps.poolSelector && err instanceof GatewayError) {
583
583
  const egressVerdict = classifyEgressFailure(err, candidate.providerId);
584
- if (egressVerdict.kind === "cooldown") {
585
- flagPoolCooldown(deps.poolSelector, deps.db, candidate.networkPoolId, candidate.providerId, err);
586
- } else if (egressVerdict.kind === "blocked") {
587
- flagPoolBlocked(deps.poolSelector, deps.db, candidate.networkPoolId, candidate.providerId, err);
588
- }
584
+ flagPoolOnEgressFailure(
585
+ deps.poolSelector,
586
+ deps.db,
587
+ candidate.networkPoolId,
588
+ candidate.providerId,
589
+ err,
590
+ egressVerdict,
591
+ );
589
592
  }
590
593
  if (!cancelled) metrics.proxy_requests_total.inc(1, { status: "failed" });
591
594
  else metrics.proxy_requests_total.inc(1, { status: "cancelled" });
@@ -43,17 +43,11 @@ import type { AdmissionLease } from "../../../security/admission/contracts";
43
43
  import type { NetworkPoolSelector } from "../../../network/pool/selector";
44
44
  import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
45
45
  import type { ProviderExchangeCapture } from "../attempt-finalize";
46
- import { captureProviderExchange } from "../attempt-finalize";
47
- import {
48
- providerCaptureMaxBytes,
49
- resolveCaptureDepthForRequest,
50
- resolveCaptureModeForRequest,
51
- shouldCaptureProviderBodyForRequest,
52
- } from "../capture-policy";
46
+ import { resolveCaptureModeForRequest } from "../capture-policy";
53
47
  import { resolveTenantImageClampMaxBytes, readTenantSystemPromptsForDispatch } from "../../request/tenant-preferences";
54
48
  import { resolveTenantSystemPrompt } from "../../request/system-prompt-injection";
55
49
  import { isWrappedInjectionProvider } from "../../../console/system-prompt/contracts";
56
- import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
50
+ import { proxySuccessHeaders, buildUpstreamDispatchContext, buildDispatchNetworkBindings } from "../upstream";
57
51
  import type { AttemptLeaseOwnership } from "../leases";
58
52
  import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
59
53
  import { resolveStreamFirstChunkTimeoutMs, resolveStreamStallTimeoutMs, resolveUpstreamTimeoutMs } from "../../../config/timeouts";
@@ -170,18 +164,13 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
170
164
  continuationScope,
171
165
  } = input;
172
166
 
173
- const outboundFetch = deps.networkBindingFactory?.fetch(
167
+ const bindings = await buildDispatchNetworkBindings({
168
+ deps,
174
169
  networkPoolId,
175
- prepared.authorization.snapshot.tenant_id,
176
- );
177
- const captureBody = outboundFetch
178
- ? await shouldCaptureProviderBodyForRequest(deps.db, prepared.authorization.snapshot.tenant_id, state)
179
- : false;
180
- const captureDepth = await resolveCaptureDepthForRequest(
181
- deps.db,
182
- prepared.authorization.snapshot.tenant_id,
170
+ tenantId: prepared.authorization.snapshot.tenant_id,
171
+ providerCapture,
183
172
  state,
184
- );
173
+ });
185
174
  // Resolved once, before the pipeline exists: the pipeline's diagnostics are
186
175
  // gated on it from construction, so `none`/`metadata` tenants never pay the
187
176
  // transcript serialization or the client-response copy for any event.
@@ -190,14 +179,6 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
190
179
  prepared.authorization.snapshot.tenant_id,
191
180
  state,
192
181
  );
193
- const wrappedOutboundFetch = outboundFetch
194
- ? captureProviderExchange(
195
- outboundFetch,
196
- providerCapture,
197
- captureBody,
198
- providerCaptureMaxBytes(captureDepth),
199
- )
200
- : undefined;
201
182
  // Same memoized ceiling the non-streaming path resolves, read before the
202
183
  // dispatch context is built so a stream gets the same limit as a normal
203
184
  // request rather than silently falling back to the deployment default.
@@ -237,18 +218,10 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
237
218
  return text === undefined ? {} : { tenantSystemPrompt: text };
238
219
  })()),
239
220
  ...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
240
- ...(wrappedOutboundFetch ? { outboundFetch: wrappedOutboundFetch } : {}),
241
- ...(outboundFetch ? { outboundAttachmentFetch: outboundFetch } : {}),
242
- ...(deps.networkBindingFactory
243
- ? {
244
- outboundHttp2: deps.networkBindingFactory.http2Bidi(
245
- networkPoolId,
246
- prepared.authorization.snapshot.tenant_id,
247
- ),
248
- }
249
- : {}),
221
+ ...bindings,
250
222
  ...(leaseOwnership === undefined ? {} : { leaseOwnership }),
251
223
  ...(continuationScope === undefined ? {} : { continuationScope }),
224
+ ...(providerCapture ? { providerCapture } : {}),
252
225
  });
253
226
  // `ProviderAdapter.dispatch` is declared to return a non-nullable
254
227
  // `AsyncIterable`, so an adapter that returned `undefined` would be a
@@ -4,6 +4,7 @@ import { metrics } from "../../observability/metrics";
4
4
  import { estimatedUsage, nativeCompletionBase, completeAttempt } from "./attempt-finalize";
5
5
  import { runAttemptLoop } from "./attempt-loop";
6
6
  import {
7
+ forwardUpstreamResponse,
7
8
  networkFetchContext,
8
9
  requireRecordBody,
9
10
  resolveAttemptCredential,
@@ -164,14 +165,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
164
165
  responseBody: null,
165
166
  });
166
167
  metrics.proxy_requests_total.inc(1, { status: "completed" });
167
- return new Response(response.body, {
168
- status: response.status,
169
- headers: {
170
- "content-type": response.headers.get("content-type") ?? "application/json",
171
- "cache-control": "no-store",
172
- "x-request-id": state.requestId,
173
- },
174
- });
168
+ return forwardUpstreamResponse(response, state.requestId);
175
169
  },
176
170
  });
177
171
  };
@@ -43,7 +43,7 @@ import type { PreparedNativeRequest } from "../request/preparer";
43
43
  import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt-finalize";
44
44
  import { finalizeRequestTelemetry } from "../guards/error-lifecycle";
45
45
  import { runAttemptLoop } from "./attempt-loop";
46
- import { hasScope, networkFetchContext, requireRecordBody, resolveAttemptCredential } from "./native-shared";
46
+ import { forwardUpstreamResponse, hasScope, networkFetchContext, requireRecordBody, resolveAttemptCredential } from "./native-shared";
47
47
 
48
48
  import type { GatewayHandlerDeps } from "./handler-deps";
49
49
 
@@ -189,11 +189,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
189
189
 
190
190
  // Verbatim: the client reads the provider's own number, and the
191
191
  // provider's own error when it refuses to produce one.
192
- const contentType = upstream.headers.get("content-type") ?? "application/json";
193
- return new Response(upstream.body, {
194
- status: upstream.status,
195
- headers: { "content-type": contentType, "cache-control": "no-store", "x-request-id": state.requestId },
196
- });
192
+ return forwardUpstreamResponse(upstream, state.requestId);
197
193
  },
198
194
  });
199
195
  };
@@ -1,7 +1,16 @@
1
1
  import type { CanonicalEvent } from "../../protocol/canonical-model";
2
2
  import type { AttemptLeaseOwnership, ValidatedOutboundFetch, ValidatedOutboundHttp2Bidi } from "../../providers/provider-contracts";
3
- import type { ProviderContinuationScope } from "../../providers/provider-contracts";
3
+ import type { ProviderCaptureExchange, ProviderContinuationScope } from "../../providers/provider-contracts";
4
4
  import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
5
+ import type { ValidatedNetworkBindingFactory } from "../../network/pool/resolver";
6
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
7
+ import { captureProviderExchange, type ProviderExchangeCapture } from "./attempt-finalize";
8
+ import {
9
+ providerCaptureMaxBytes,
10
+ resolveCaptureDepthForRequest,
11
+ shouldCaptureProviderBodyForRequest,
12
+ type CapturePreferenceState,
13
+ } from "./capture-policy";
5
14
 
6
15
  const FORWARDED_REQUEST_HEADERS = new Set([
7
16
  "user-agent",
@@ -34,6 +43,60 @@ export function proxySuccessHeaders(state: { requestId: string }): Record<string
34
43
  };
35
44
  }
36
45
 
46
+ /** The three validated egress bindings shared by every dispatch attempt. */
47
+ export interface DispatchNetworkBindings {
48
+ readonly outboundFetch?: ValidatedOutboundFetch;
49
+ readonly outboundAttachmentFetch?: ValidatedOutboundFetch;
50
+ readonly outboundHttp2?: ValidatedOutboundHttp2Bidi;
51
+ }
52
+
53
+ /**
54
+ * Builds the outbound network bindings for one attempt's dispatch context,
55
+ * shared by the non-streaming proxy path and the streaming branch.
56
+ *
57
+ * Both previously assembled the same three objects by hand (wrapped fetch,
58
+ * attachment fetch, HTTP/2 bidi), which drifted in the capture wrap. The
59
+ * capture-body gating is preserved exactly: the DB-backed capture-preference
60
+ * read is skipped when no network binding exists (`outboundFetch ? ... :
61
+ * false`), so a request that can never tee a provider exchange does not pay
62
+ * for the preference read. The reads are memoized on the request state, so
63
+ * resolving them here costs no extra DB reads.
64
+ */
65
+ export async function buildDispatchNetworkBindings(input: {
66
+ readonly deps: {
67
+ readonly networkBindingFactory?: ValidatedNetworkBindingFactory | undefined;
68
+ readonly db: CartethyiaDatabase;
69
+ };
70
+ readonly networkPoolId: string | undefined;
71
+ readonly tenantId: string | null;
72
+ readonly providerCapture: ProviderExchangeCapture;
73
+ readonly state: CapturePreferenceState;
74
+ }): Promise<DispatchNetworkBindings> {
75
+ const factory = input.deps.networkBindingFactory;
76
+ if (factory === undefined) return {};
77
+ const outboundFetch = factory.fetch(input.networkPoolId, input.tenantId ?? undefined);
78
+ const captureBody = await shouldCaptureProviderBodyForRequest(
79
+ input.deps.db,
80
+ input.tenantId,
81
+ input.state,
82
+ );
83
+ const captureDepth = await resolveCaptureDepthForRequest(
84
+ input.deps.db,
85
+ input.tenantId,
86
+ input.state,
87
+ );
88
+ return {
89
+ outboundFetch: captureProviderExchange(
90
+ outboundFetch,
91
+ input.providerCapture,
92
+ captureBody,
93
+ providerCaptureMaxBytes(captureDepth),
94
+ ),
95
+ outboundAttachmentFetch: factory.fetch(input.networkPoolId, input.tenantId ?? undefined),
96
+ outboundHttp2: factory.http2Bidi(input.networkPoolId, input.tenantId ?? undefined),
97
+ };
98
+ }
99
+
37
100
  export function buildUpstreamDispatchContext(input: {
38
101
  readonly credential: unknown;
39
102
  readonly deadline: number;
@@ -42,6 +105,8 @@ export function buildUpstreamDispatchContext(input: {
42
105
  readonly requestId?: string;
43
106
  readonly userAgent?: string;
44
107
  readonly outboundFetch?: ValidatedOutboundFetch;
108
+ /** Capture sink for provider exchanges not visible to the HTTP fetch wrapper. */
109
+ readonly providerCapture?: ProviderCaptureExchange;
45
110
  /** Validated attachment egress that must not be wrapped by provider payload capture. */
46
111
  readonly outboundAttachmentFetch?: ValidatedOutboundFetch;
47
112
  /** Validated bidirectional HTTP/2 egress bound to the same selected pool. */
@@ -100,6 +165,7 @@ export function buildUpstreamDispatchContext(input: {
100
165
  ...(userAgent ? { user_agent: userAgent } : {}),
101
166
  ...(Object.keys(input.headers).length > 0 ? { request_headers: input.headers } : {}),
102
167
  ...(outboundFetch ? { outbound_fetch: outboundFetch } : {}),
168
+ ...(input.providerCapture ? { provider_capture: input.providerCapture } : {}),
103
169
  ...(input.outboundAttachmentFetch
104
170
  ? { outbound_attachment_fetch: input.outboundAttachmentFetch }
105
171
  : {}),
@@ -50,14 +50,41 @@ export function tokenMatches(provided: string | null, expected: string): boolean
50
50
  return a.length === b.length && timingSafeEqual(a, b);
51
51
  }
52
52
 
53
+ interface LoopbackAdminGate {
54
+ /** Peer address of the request, resolved by the composition root. */
55
+ readonly peer: string | null;
56
+ /** Shared secret; the route is registered only when this is set. */
57
+ readonly token: string;
58
+ /** `policy_rejected` (403) message, preserved per surface. */
59
+ readonly policyRejectedMessage: string;
60
+ /** `authentication_failed` (401) message, preserved per surface. */
61
+ readonly authenticationFailedMessage: string;
62
+ }
63
+
64
+ /**
65
+ * The shared security gate for host-local admin operations: the request must
66
+ * come from loopback, then it must carry the drain token compared in constant
67
+ * time. The peer check is independent of the secret, so a leaked token alone
68
+ * cannot reach a remote surface. The peer refusal comes first, then the token,
69
+ * with the codes and messages preserved verbatim per caller.
70
+ */
71
+ export function requireLoopbackAdmin(request: Request, gate: LoopbackAdminGate): void {
72
+ if (!isLoopbackPeer(gate.peer))
73
+ throw new GatewayError("policy_rejected", 403, gate.policyRejectedMessage);
74
+ if (!tokenMatches(request.headers.get("x-drain-token"), gate.token))
75
+ throw new GatewayError("authentication_failed", 401, gate.authenticationFailedMessage);
76
+ }
77
+
53
78
  export function createDrainHandler(
54
79
  deps: DrainEndpointDeps,
55
80
  ): (context: { request: Request }) => Promise<Response> {
56
81
  return async ({ request }) => {
57
- if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
58
- throw new GatewayError("policy_rejected", 403, "drain is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂");
59
- if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
60
- throw new GatewayError("authentication_failed", 401, "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑");
82
+ requireLoopbackAdmin(request, {
83
+ peer: deps.resolvePeerAddress(request),
84
+ token: deps.token,
85
+ policyRejectedMessage: "drain is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
86
+ authenticationFailedMessage: "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑",
87
+ });
61
88
  // Acknowledge first, then drain: the response must be on the wire before the
62
89
  // shutdown path starts closing sockets, or the caller sees a dropped
63
90
  // connection instead of a 202.
@@ -2,7 +2,6 @@ import { GatewayError, type GatewayErrorCode, type GatewayErrorOrigin } from "..
2
2
  import { isRecord } from "../protocol/codecs";
3
3
  import { resolveFallbackRetryBaseMs, resolveFallbackRetryCapMs } from "../config/timeouts";
4
4
  import { parseUpstreamBackoff } from "../protocol/upstream-parsing";
5
- import { resolveAccountQuotaCooldownMs } from "../config/cooldowns";
6
5
 
7
6
  /**
8
7
  * How much of the upstream's error body is kept and surfaced.
@@ -14,6 +13,7 @@ import { resolveAccountQuotaCooldownMs } from "../config/cooldowns";
14
13
  * runs a few KB; 4000 keeps the whole thing while bounding a hostile body.
15
14
  */
16
15
  const MAX_UPSTREAM_ERROR_BYTES = 4000;
16
+ export { MAX_UPSTREAM_ERROR_BYTES };
17
17
  export {
18
18
  parseRetryAfter,
19
19
  parseRateLimitReset,
@@ -22,6 +22,32 @@ export {
22
22
  parseProviderResetDuration,
23
23
  parseAbsoluteResetTimestamp,
24
24
  } from "../protocol/upstream-parsing";
25
+ /**
26
+ * Extracts only the human-readable text from an upstream error body —
27
+ * `message`/`msg`/`error` fields — deliberately skipping `code`/identifier
28
+ * values. Used by `upstreamMessageAsIs` to decide whether the upstream
29
+ * actually supplied a message: a symbolic code like `"permission_error"` is
30
+ * an identifier, not prose, so it must not count as passthrough text.
31
+ */
32
+ export function extractUpstreamMessageText(body: unknown): string {
33
+ let raw = "";
34
+ if (typeof body === "string") raw = body;
35
+ else if (isRecord(body)) {
36
+ if (typeof body.error === "string" && body.error.trim()) raw = body.error;
37
+ else {
38
+ const envelope = isRecord(body.error) ? body.error : body;
39
+ const message = (envelope as Record<string, unknown>).message;
40
+ if (typeof message === "string" && message.trim()) raw = message;
41
+ else if (typeof body.msg === "string" && body.msg.trim()) raw = body.msg;
42
+ else if (typeof (envelope as Record<string, unknown>).msg === "string" && String((envelope as Record<string, unknown>).msg).trim())
43
+ raw = String((envelope as Record<string, unknown>).msg);
44
+ else if (typeof (envelope as Record<string, unknown>).error === "string" && String((envelope as Record<string, unknown>).error).trim())
45
+ raw = String((envelope as Record<string, unknown>).error);
46
+ }
47
+ }
48
+ return raw.replace(/[\r\n]+/g, " ").trim();
49
+ }
50
+
25
51
  /** Extracts a bounded, newline-free provider message from any error envelope. */
26
52
  export function extractUpstreamMessage(body: unknown): string {
27
53
  let raw = "";
@@ -233,31 +259,6 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
233
259
  // DNS failure. Failing the client request instead of failing over is the wrong
234
260
  // outcome for a whole class the table already names as platform-wide.
235
261
  const statusCode = error.status;
236
- // Qoder answers an exhausted quota by closing the stream with an upstream 499
237
- // (nginx client-close style). That is a refusal from the upstream — the
238
- // gateway's own aborts are `transport_closed`, produced locally, and never
239
- // carry a qoder providerId — so a 499 scoped to qoder is quota, not a
240
- // malformed request. Without this it fell through to `invalid_request`
241
- // (non-retryable, no cooldown) and the account was re-dialled on every
242
- // request inside the exhausted window until a late 429 finally classified it.
243
- if (
244
- error.origin === "upstream" &&
245
- statusCode === 499 &&
246
- (providerId === "qoder" || providerId === "qodercn")
247
- ) {
248
- const cooldownMs = resolveAccountQuotaCooldownMs();
249
- return {
250
- retryable: true,
251
- mutatesAccount: true,
252
- category: "quota_exceeded",
253
- scope: "account",
254
- origin: error.origin,
255
- ...(providerCode ? { providerCode } : {}),
256
- ...(providerId ? { providerId } : {}),
257
- cooldownMs,
258
- statusCode,
259
- };
260
- }
261
262
  const retryableByStatus =
262
263
  statusCode === 401 ||
263
264
  statusCode === 403 ||
@@ -657,10 +658,8 @@ const QUOTA_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
657
658
  // quota (a cooldown) instead of `auth_invalidated`.
658
659
  "110",
659
660
  // Qoder also reports quota exhaustion as `{"code":116,"error":"quota
660
- // exceeded"}` (seen on the IDE model-server surface with a 499/stream
661
- // close). The 499 special-case below already treats qoder 499 as quota;
662
- // naming the code here makes any status that carries it classify as quota
663
- // instead of `invalid_request`/`authentication_failed`.
661
+ // exceeded"}`. Naming the code here makes any status that carries it classify
662
+ // as quota instead of `invalid_request`/`authentication_failed`.
664
663
  "116",
665
664
  // InferHub publishes its own vocabulary in `error.code`. The two 403s below
666
665
  // are deliberate absences: `account_blocked` means the account was blocked
@@ -696,6 +695,25 @@ const AUTH_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
696
695
  "insufficient_key_scope",
697
696
  "model_not_allowed_for_key",
698
697
  ]);
698
+
699
+ /**
700
+ * Qoder job-token verdicts that are NOT credential evidence.
701
+ *
702
+ * A PAT (`pt-…`) is exchanged for a short-lived job token (`jt-…`). When the
703
+ * job token expires mid-flight the upstream answers `{"code":"105","message":
704
+ * "Login expired"}` (403). The PAT is still valid — re-exchanging it mints a
705
+ * fresh job token — so treating this as `auth_invalidated` parked a healthy
706
+ * account as "Re-login required". It stays `authentication_failed` (retryable;
707
+ * the next attempt re-runs the PAT exchange) but without `credentialEvidence`,
708
+ * so the account is never disabled or cooled on it.
709
+ */
710
+ const QODER_JOB_TOKEN_EXPIRED: ReadonlySet<string> = new Set([
711
+ "105",
712
+ "login expired",
713
+ "login_expired",
714
+ "job token expired",
715
+ "jobtoken expired",
716
+ ]);
699
717
  const PLATFORM_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
700
718
  "server_error",
701
719
  "internal_error",
@@ -800,6 +818,20 @@ export function classifyUpstreamError(
800
818
  return { code: "proxy_auth_required", status: 407, origin: "network" };
801
819
  if (normalized === "11140")
802
820
  return { code: "policy_rejected", status: 403, origin: "upstream" };
821
+ // ZCode plan-lane envelope codes (verified live 2026-10-30 by the donor and
822
+ // mirrored by the zcode-start provider). `3009` is model concurrency
823
+ // (transient, retried in place), `3008` user concurrency (the desktop client
824
+ // is busy on that account — switchable), `3012` is the identity/prompt-shape
825
+ // gate, `1113` the paid-lane "insufficient balance or no resource package"
826
+ // (a request that was not recognized as start-plan traffic).
827
+ if (normalized === "3009")
828
+ return { code: "rate_limited", status: 429, origin: "upstream" };
829
+ if (normalized === "3008")
830
+ return { code: "accounts_rate_limited", status: 429, origin: "upstream" };
831
+ if (normalized === "3012")
832
+ return { code: "policy_rejected", status: 405, origin: "upstream" };
833
+ if (normalized === "1113")
834
+ return { code: "quota_exceeded", status: 429, origin: "upstream", rateLimitScope: "provider" };
803
835
  if (normalized !== undefined && CONTEXT_LENGTH_CODES.has(normalized))
804
836
  return { code: "context_length_exceeded", status: 413, origin: "upstream" };
805
837
  if (normalized !== undefined && QUOTA_ERROR_IDENTIFIERS.has(normalized))
@@ -809,6 +841,16 @@ export function classifyUpstreamError(
809
841
  origin: "upstream",
810
842
  rateLimitScope: "provider",
811
843
  };
844
+ // Qoder job-token expiry (`105` / "Login expired") is recoverable by
845
+ // re-exchanging the PAT — never credential evidence (see
846
+ // `QODER_JOB_TOKEN_EXPIRED`).
847
+ if (normalized !== undefined && QODER_JOB_TOKEN_EXPIRED.has(normalized)) {
848
+ return {
849
+ code: "authentication_failed",
850
+ status: status === 403 || status === 401 ? status : 403,
851
+ origin: "upstream",
852
+ };
853
+ }
812
854
  if (normalized !== undefined && AUTH_ERROR_IDENTIFIERS.has(normalized)) {
813
855
  let authStatus: number;
814
856
  if (status === 401 || status === 403) authStatus = status;
@@ -919,17 +961,15 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
919
961
  : isContextLengthFailure(parsedBody)
920
962
  ? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
921
963
  : classifyUpstreamError(response.status, identifier);
922
- const extracted = extractUpstreamMessage(parsedBody ?? text);
923
- let message =
924
- extracted.length > 0
925
- ? extracted
926
- : text.trim() || `Provider returned HTTP ${response.status}.`;
964
+ const upstreamText = upstreamMessageAsIs(parsedBody ?? text, "");
965
+ let message = upstreamText.message || `Provider returned HTTP ${response.status}.`;
927
966
  // A bare "Provider returned HTTP <5xx>" tells the operator nothing actionable;
928
967
  // platform-unavailable fallbacks get the same human sign-off every other
929
- // gateway error carries. The raw upstream body stays in `details.raw` for
930
- // diagnostics either way.
968
+ // gateway error carries — but only when the upstream supplied no text of its
969
+ // own. The raw upstream body stays in `details.raw` for diagnostics either way.
931
970
  if (
932
971
  classification.code === "platform_unavailable" &&
972
+ !upstreamText.hadUpstreamText &&
933
973
  message === `Provider returned HTTP ${response.status}.`
934
974
  ) {
935
975
  message = `The provider's service is temporarily unavailable (HTTP ${response.status}) (request {requestId}). Your request did not go through — wait a moment and try again. Hold tight ☁️`;
@@ -949,6 +989,7 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
949
989
  ...(reportedProviderCode ? { providerCode: reportedProviderCode } : {}),
950
990
  ...(requestId ? { upstreamRequestId: requestId } : {}),
951
991
  raw: text.slice(0, MAX_UPSTREAM_ERROR_BYTES),
992
+ ...(upstreamText.hadUpstreamText ? { upstreamMessageAsIs: true } : {}),
952
993
  ...(retryAfterMs === null || retryAfterMs === undefined ? {} : { retryAfterMs }),
953
994
  ...(queuedWaitMs !== undefined
954
995
  ? { reason: "upstream-queued", cooldownMs: queuedWaitMs }
@@ -971,6 +1012,57 @@ export function extractHtmlTitle(html: string): string {
971
1012
  return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
972
1013
  }
973
1014
 
1015
+ /** The public bound on an upstream message surface. */
1016
+ export const MAX_UPSTREAM_MESSAGE_LENGTH = 500;
1017
+
1018
+ /**
1019
+ * Extracts the upstream's own text from an error body, "as-is".
1020
+ *
1021
+ * The upstream error passthrough contract: when a failure is unknown or
1022
+ * unparseable, the client should see whatever the upstream actually said,
1023
+ * not a generalized 502 sentence. Structured JSON envelopes already flow
1024
+ * through `extractUpstreamMessage`; this helper extends the same idea to the
1025
+ * shapes that used to lose the text entirely — HTML error pages (their
1026
+ * `<title>` is often the only readable thing) and plain-text bodies.
1027
+ *
1028
+ * The returned message is bounded to 500 chars (D44) and newline-collapsed,
1029
+ * matching `sanitizePublicDetail` per-string bound; `details.raw` keeps the
1030
+ * full 4000. `hadUpstreamText` distinguishes "the upstream said something"
1031
+ * from "we synthesized a fallback", so callers can keep their human sign-off
1032
+ * only when there is genuinely no upstream text.
1033
+ */
1034
+ export function upstreamMessageAsIs(
1035
+ body: unknown,
1036
+ fallback: string,
1037
+ ): { message: string; hadUpstreamText: boolean } {
1038
+ const raw = typeof body === "string" ? body : "";
1039
+ const looksLikeHtml = raw.includes("<") && raw.includes(">");
1040
+ // HTML error pages first: the structured extractor passes a string through
1041
+ // verbatim, which would surface raw markup instead of the readable `<title>`
1042
+ // (or a tag-stripped excerpt when the page has no title).
1043
+ if (looksLikeHtml) {
1044
+ const title = extractHtmlTitle(raw);
1045
+ if (title.length > 0) {
1046
+ return {
1047
+ message: title.replace(/[\r\n]+/g, " ").trim().slice(0, MAX_UPSTREAM_MESSAGE_LENGTH),
1048
+ hadUpstreamText: true,
1049
+ };
1050
+ }
1051
+ }
1052
+ const structured = extractUpstreamMessageText(body);
1053
+ if (structured.length > 0) {
1054
+ return { message: structured.slice(0, MAX_UPSTREAM_MESSAGE_LENGTH), hadUpstreamText: true };
1055
+ }
1056
+ const text = raw.trim();
1057
+ if (text.length > 0) {
1058
+ return {
1059
+ message: text.replace(/[\r\n]+/g, " ").trim().slice(0, MAX_UPSTREAM_MESSAGE_LENGTH),
1060
+ hadUpstreamText: true,
1061
+ };
1062
+ }
1063
+ return { message: fallback, hadUpstreamText: false };
1064
+ }
1065
+
974
1066
  /** Throws a typed transport error when an upstream returns an HTML error page. */
975
1067
  export async function throwIfHtmlResponse(response: Response): Promise<void> {
976
1068
  const contentType = response.headers.get("content-type") ?? "";
@@ -58,8 +58,14 @@ export function isProxyDispatchRoute(path: string | undefined): path is string {
58
58
  return path !== undefined && isJsonProxyRoute(path);
59
59
  }
60
60
 
61
+ /**
62
+ * The same canonical JSON-route membership as `isProxyDispatchRoute`, kept as
63
+ * a non-narrowing alias for callers that already hold a string path. One
64
+ * predicate, two spellings, so a route-table change cannot diverge between
65
+ * the body-read policy and the canonical-parse gate.
66
+ */
61
67
  export function isJsonProxyRoutePath(path: string): boolean {
62
- return isJsonProxyRoute(path);
68
+ return isProxyDispatchRoute(path);
63
69
  }
64
70
 
65
71
  interface IngressPolicyOptions {
@@ -77,8 +77,20 @@ export function createErrorNormalizationMiddleware(deps: {
77
77
  .error(({ request, error, set }) => {
78
78
  const state = deps.stateStore.get(request as Request);
79
79
  const gateway = error instanceof GatewayError ? error : undefined;
80
+ // Elysia's built-in errors (NotFound, ValidationError, ParseError,
81
+ // InternalServerError) always carry a numeric `status` **and** a code.
82
+ // A raw runtime error can carry a `.code` on its own — Bun's fetch
83
+ // surfaces `TypeError` with `code: "ECONNRESET"` when an upstream socket
84
+ // dies mid-response — so a `code` alone is not evidence of an Elysia
85
+ // built-in. Requiring the numeric status keeps that raw error from
86
+ // leaking into the public envelope as a fake code/400; the dispatch loop
87
+ // already normalizes socket drops to `transport_unavailable`/502 before
88
+ // they reach here.
80
89
  const builtin =
81
- !gateway && typeof error === "object" && error !== null
90
+ !gateway &&
91
+ typeof error === "object" &&
92
+ error !== null &&
93
+ typeof (error as ElysiaBuiltinError).status === "number"
82
94
  ? (error as ElysiaBuiltinError)
83
95
  : undefined;
84
96
  const builtinStatus = typeof builtin?.status === "number" ? builtin.status : undefined;
@@ -12,7 +12,7 @@ import { resolveClientIdentity } from "../../security/ip-boundary";
12
12
  import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
13
13
  import { pushStructuredConsoleLog } from "../../observability/log-ring";
14
14
  import { log } from "../../observability/logger";
15
- import { isJsonProxyRoutePath, isProxyDispatchRoute } from "./body-policy";
15
+ import { isProxyDispatchRoute } from "./body-policy";
16
16
  import { isOpaqueBodyRoute } from "../contracts";
17
17
  import { peerAddressUnavailableError } from "./guard-errors";
18
18
 
@@ -24,6 +24,24 @@ interface RequestApp {
24
24
  ): RequestApp;
25
25
  }
26
26
 
27
+ /**
28
+ * Whether this request falls outside the canonical JSON-proxy pipeline the
29
+ * canonical-parse and route-preparation stages serve.
30
+ *
31
+ * Shared gate, applied in the exact order both middlewares historically
32
+ * checked: non-`/v1/` paths, GET/HEAD (which carry no body to parse), then
33
+ * the JSON route table with opaque-body routes excluded — `/v1/completions`
34
+ * needs no separate arm because it is already in the JSON routes table, and
35
+ * native routes (compact, System One, token count) read a body but are never
36
+ * parsed into a canonical request, their bodies being opaque by contract.
37
+ */
38
+ function skipNonCanonicalProxyRequest(request: Request): boolean {
39
+ const path = fastPathname(request.url);
40
+ if (!path.startsWith("/v1/")) return true;
41
+ if (request.method === "GET" || request.method === "HEAD") return true;
42
+ return !isProxyDispatchRoute(path) || isOpaqueBodyRoute(path);
43
+ }
44
+
27
45
  interface RequestContextDeps {
28
46
  readonly stateStore: ProxyRequestStateStore;
29
47
  readonly clock?: () => number;
@@ -141,15 +159,9 @@ export function createCanonicalRequestMiddleware(deps: {
141
159
  }): Elysia {
142
160
  return new Elysia()
143
161
  .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) || isOpaqueBodyRoute(path)) return;
152
162
  // Model discovery and other GET/multimodal routes must not go through canonical parsing.
163
+ if (skipNonCanonicalProxyRequest(request)) return;
164
+ const path = fastPathname(request.url);
153
165
  const state = deps.stateStore.require(request);
154
166
  // Single-read invariant: the ingress policy middleware already decoded the
155
167
  // body once into `state.ingressBody`. Never re-read `request.body` here —
@@ -216,11 +228,7 @@ export function createProxyRoutePreparationMiddleware(deps: {
216
228
  }): Elysia {
217
229
  return new Elysia()
218
230
  .beforeHandle(async ({ request }) => {
219
- const path = fastPathname(request.url);
220
- if (!path.startsWith("/v1/")) return;
221
- if (request.method === "GET" || request.method === "HEAD") return;
222
- const isJsonRoute = isJsonProxyRoutePath(path);
223
- if (!isJsonRoute || isOpaqueBodyRoute(path)) return;
231
+ if (skipNonCanonicalProxyRequest(request)) return;
224
232
  const state = deps.stateStore.require(request);
225
233
  if (!state.authorization || !state.canonicalRequest)
226
234
  throw new GatewayError("route_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served (request {requestId}). Resend the request. The bouncer stepped out — resend shortly 🚪");