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
@@ -11,12 +11,14 @@ import type { CartethyiaDatabase } from "../../persistence/db-handle";
11
11
  import { providerAccounts } from "../../persistence/schema";
12
12
  import type { RedisClient } from "../../persistence/redis";
13
13
  import { log } from "../../observability/logger";
14
+ import { pushStructuredConsoleLog } from "../../observability/log-ring";
14
15
  import { fetchProviderQuota } from "./quota-support";
15
16
  import { type FetchLike, type ProviderQuotaResult } from "./quota-contracts";
16
17
  import type { ProviderId } from "../provider-contracts";
17
18
  import type { ProviderRegistry } from "../provider-registry";
18
19
  import { quotaLens, setCachedQuota } from "./cache";
19
20
  import { persistQuotaSnapshot } from "./snapshot";
21
+ import { applyAccountVerdict } from "../operations/account-health-service";
20
22
  import { asJsonbRecord } from "../../shared/coerce";
21
23
  import { KIRO_DEFAULT_LABELS } from "../integrations/kiro/kiro-oauth";
22
24
 
@@ -149,12 +151,52 @@ export async function recordAccountCheck(
149
151
  db: CartethyiaDatabase,
150
152
  accountId: string,
151
153
  outcome: { ok: boolean; error?: string | null; category?: string | null },
154
+ snapshotInvalidator?: { invalidate(): unknown },
152
155
  ): Promise<void> {
153
- const activeOnly = and(
154
- eq(providerAccounts.id, accountId),
155
- eq(providerAccounts.status, "active"),
156
- );
157
156
  if (outcome.ok) {
157
+ // A successful quota refresh is the freshest evidence about the account's
158
+ // health: heal a cooldown that the refresh shows is no longer justified, so
159
+ // the account returns to rotation immediately instead of waiting out the
160
+ // remainder of a stale fallback window. Only healable scopes heal — an
161
+ // auth/policy cooldown must never be lifted by a quota report (the
162
+ // credential is still refused). `lastErrorCategory` carries the category
163
+ // that parked the account, so the heal is scoped by the same verdict the
164
+ // health machine wrote.
165
+ const healableCategories = new Set([
166
+ "quota_exhausted",
167
+ "rate_limit_transient",
168
+ "model_capacity",
169
+ "model_unreachable",
170
+ "unknown",
171
+ "check_failed",
172
+ ]);
173
+ const rows = await db
174
+ .select({ status: providerAccounts.status, lastErrorCategory: providerAccounts.lastErrorCategory })
175
+ .from(providerAccounts)
176
+ .where(eq(providerAccounts.id, accountId))
177
+ .limit(1);
178
+ const account = rows[0];
179
+ const healable =
180
+ account?.status === "cooldown" &&
181
+ (account.lastErrorCategory === null || healableCategories.has(account.lastErrorCategory));
182
+ if (healable) {
183
+ const healed = await db
184
+ .update(providerAccounts)
185
+ .set({
186
+ status: "active",
187
+ lastSuccessAt: new Date(),
188
+ lastError: null,
189
+ lastErrorCategory: null,
190
+ lastErrorAt: null,
191
+ consecutiveFailures: 0,
192
+ cooldownUntil: null,
193
+ lastRecoveredAt: new Date(),
194
+ })
195
+ .where(eq(providerAccounts.id, accountId))
196
+ .returning({ id: providerAccounts.id });
197
+ if (healed.length > 0) await snapshotInvalidator?.invalidate();
198
+ return;
199
+ }
158
200
  await db
159
201
  .update(providerAccounts)
160
202
  .set({
@@ -164,7 +206,7 @@ export async function recordAccountCheck(
164
206
  lastErrorAt: null,
165
207
  consecutiveFailures: 0,
166
208
  })
167
- .where(activeOnly);
209
+ .where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.status, "active")));
168
210
  return;
169
211
  }
170
212
  const transient = isTransientTimeout(outcome.error);
@@ -185,7 +227,7 @@ export async function recordAccountCheck(
185
227
  lastErrorAt: new Date(),
186
228
  ...(transient ? {} : { consecutiveFailures: sql`${providerAccounts.consecutiveFailures} + 1` }),
187
229
  })
188
- .where(activeOnly)
230
+ .where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.status, "active")))
189
231
  .returning({ consecutiveFailures: providerAccounts.consecutiveFailures });
190
232
  if (transient) return;
191
233
  const failures = failing[0]?.consecutiveFailures ?? 0;
@@ -195,13 +237,18 @@ export async function recordAccountCheck(
195
237
  // pass. The cooldown clears itself; a success before it elapses resets the
196
238
  // count above.
197
239
  if (failures >= QUOTA_FAILURE_PARK_THRESHOLD) {
198
- await db
199
- .update(providerAccounts)
200
- .set({
201
- status: "cooldown",
202
- cooldownUntil: new Date(Date.now() + QUOTA_FAILURE_COOLDOWN_MS),
203
- })
204
- .where(activeOnly);
240
+ // Converge on the health application seam (proposal §3.5): quota refresh
241
+ // parking is a producer into `applyAccountVerdict`, not a separate state
242
+ // machine.
243
+ const parked = await applyAccountVerdict(db, {
244
+ accountId,
245
+ status: "cooldown",
246
+ reason: outcome.error ?? "Account check failed",
247
+ category: outcome.category ?? "quota_fetch_failed",
248
+ cooldownUntil: new Date(Date.now() + QUOTA_FAILURE_COOLDOWN_MS),
249
+ fromStatus: "active",
250
+ });
251
+ if (parked) await snapshotInvalidator?.invalidate();
205
252
  }
206
253
  }
207
254
 
@@ -261,6 +308,12 @@ const TARGET_COLUMNS = {
261
308
  * disabled account is not necessarily revoked — it may have been parked for a
262
309
  * reason unrelated to its credential — and skipping it would let a credential
263
310
  * die silently while the row still reads healthy.
311
+ *
312
+ * Invariant (area 04, D21): this sweep **never flips status**. The refresh it
313
+ * kicks off rewrites only credential/expiry columns; `recordAccountCheck` is
314
+ * active-only, so `disabled` rows are identical after the pass to before. The
315
+ * `origin: "sweep:quota"` tag on the token-refresh log lines (D26) is what
316
+ * lets an operator verify that in the Console Log.
264
317
  */
265
318
  export async function listQuotaRefreshTargets(
266
319
  db: CartethyiaDatabase,
@@ -343,6 +396,20 @@ async function runQuotaRefresh(
343
396
  let resolveError: string | undefined;
344
397
  try {
345
398
  credential = await deps.resolveCredential(target.providerId as ProviderId, target.accountId);
399
+ // D26: every OAuth credential this sweep resolves may have been refreshed
400
+ // inside the refresh-aware resolver. Label that refresh with its origin so
401
+ // the Console Log can prove the sweep touches disabled accounts without
402
+ // ever flipping their status. The refresh service itself also logs the
403
+ // actual token refresh under the `sweep` origin family; this line names
404
+ // the sweep here, at the source of the trigger.
405
+ if (credential !== "" && target.credentialKind === "oauth") {
406
+ pushStructuredConsoleLog("debug", "Quota sweep resolved OAuth credential; token may have been refreshed", {
407
+ event: "token_refresh",
408
+ accountId: target.accountId,
409
+ providerId: target.providerId,
410
+ details: { origin: "sweep:quota" },
411
+ });
412
+ }
346
413
  } catch (error) {
347
414
  resolveError = error instanceof Error ? error.message : "Failed to resolve credential";
348
415
  }
@@ -406,7 +473,13 @@ async function runQuotaRefresh(
406
473
  try {
407
474
  await deps.db
408
475
  .update(providerAccounts)
409
- .set({ quotaPlan: quota.plan })
476
+ .set({
477
+ quotaPlan: quota.plan,
478
+ // Freshness stamp for the plan column: routing uses it to downgrade
479
+ // a plan older than the freshness horizon to `unknown` instead of
480
+ // trusting a weeks-old string.
481
+ quotaPlanFetchedAt: new Date(),
482
+ })
410
483
  .where(eq(providerAccounts.id, target.accountId));
411
484
  } catch (error) {
412
485
  log.warn(`[quota] failed to record plan for account=${target.accountId}`, error as Error);
@@ -448,10 +521,15 @@ async function runQuotaRefresh(
448
521
  }
449
522
  }
450
523
  try {
451
- await recordAccountCheck(deps.db, target.accountId, {
452
- ok: quota.error === null,
453
- ...(quota.error === null ? {} : { error: quota.error, category: failure ?? "quota_fetch_failed" }),
454
- });
524
+ await recordAccountCheck(
525
+ deps.db,
526
+ target.accountId,
527
+ {
528
+ ok: quota.error === null,
529
+ ...(quota.error === null ? {} : { error: quota.error, category: failure ?? "quota_fetch_failed" }),
530
+ },
531
+ deps.snapshotInvalidator,
532
+ );
455
533
  } catch (error) {
456
534
  // A stamp failure must not lose the freshly fetched quota.
457
535
  log.warn(`[quota] failed to stamp check outcome for account=${target.accountId}`, error as Error);
@@ -113,6 +113,15 @@ export interface IpAbuseStore {
113
113
  * bounded `admission_unavailable` with zero dispatches.
114
114
  */
115
115
  checkAndRecord(attempt: IpAbuseAttempt): Promise<IpAbuseOutcome>;
116
+ /**
117
+ * Manually bans an identity for `durationMs` (0/negative = permanent).
118
+ * Throws when the store is unreachable.
119
+ */
120
+ ban(identity: string, durationMs: number): Promise<void>;
121
+ /** Lifts a manual (or any) ban on an identity. No-op when not banned. */
122
+ unban(identity: string): Promise<void>;
123
+ /** Lists currently active bans with their expiry (`null` = permanent). */
124
+ listBans(): Promise<readonly { identity: string; bannedUntil: number | null }[]>;
116
125
  }
117
126
 
118
127
  interface IpAbuseProtectionServiceOptions {
@@ -293,6 +302,34 @@ export class InMemoryIpAbuseStore implements IpAbuseStore {
293
302
  return true;
294
303
  }
295
304
 
305
+ async ban(identity: string, durationMs: number): Promise<void> {
306
+ this.assertAvailable();
307
+ this.evictOldest(this.bans);
308
+ this.bans.set(identity, durationMs > 0 ? this.clock() + durationMs : Number.POSITIVE_INFINITY);
309
+ }
310
+
311
+ async unban(identity: string): Promise<void> {
312
+ this.assertAvailable();
313
+ this.bans.delete(identity);
314
+ }
315
+
316
+ async listBans(): Promise<readonly { identity: string; bannedUntil: number | null }[]> {
317
+ this.assertAvailable();
318
+ const now = this.clock();
319
+ const out: { identity: string; bannedUntil: number | null }[] = [];
320
+ for (const [identity, until] of this.bans) {
321
+ if (until <= now) {
322
+ this.bans.delete(identity);
323
+ continue;
324
+ }
325
+ out.push({
326
+ identity,
327
+ bannedUntil: Number.isFinite(until) ? until : null,
328
+ });
329
+ }
330
+ return out;
331
+ }
332
+
296
333
  async checkAndRecord(attempt: IpAbuseAttempt): Promise<IpAbuseOutcome> {
297
334
  this.assertAvailable();
298
335
  const { identity, route, now, limit, banThreshold, banDurationMs } = attempt;
@@ -582,5 +619,39 @@ export class RedisIpAbuseStore implements IpAbuseStore {
582
619
  );
583
620
  return decodeAbuseOutcome(raw);
584
621
  }
622
+
623
+ async ban(identity: string, durationMs: number): Promise<void> {
624
+ const marker = this.banMarkerKey(identity);
625
+ const now = Date.now();
626
+ if (durationMs > 0) {
627
+ await this.redis.set(marker, String(now + durationMs), "PX", durationMs);
628
+ } else {
629
+ // Permanent: no TTL; a large sentinel far in the future.
630
+ await this.redis.set(marker, String(Number.MAX_SAFE_INTEGER));
631
+ }
632
+ }
633
+
634
+ async unban(identity: string): Promise<void> {
635
+ await this.redis.del(this.banMarkerKey(identity));
636
+ }
637
+
638
+ async listBans(): Promise<readonly { identity: string; bannedUntil: number | null }[]> {
639
+ const keys = await this.redis.keys("cartethyia:ip:ban:*");
640
+ const now = Date.now();
641
+ const out: { identity: string; bannedUntil: number | null }[] = [];
642
+ for (const key of keys) {
643
+ const value = await this.redis.get(key);
644
+ const until = Number(value ?? 0);
645
+ if (!Number.isFinite(until) || until <= now) {
646
+ await this.redis.del(key);
647
+ continue;
648
+ }
649
+ out.push({
650
+ identity: key.slice("cartethyia:ip:ban:".length),
651
+ bannedUntil: until >= Number.MAX_SAFE_INTEGER - 1 ? null : until,
652
+ });
653
+ }
654
+ return out;
655
+ }
585
656
  }
586
657
 
@@ -1,7 +1,7 @@
1
1
  /** Bounded process-local authorization cache; shared-cache I/O stays in the auth service. */
2
2
  export const AUTH_CACHE_TTL_MS = 3_000;
3
3
  const AUTH_CACHE_MAX_ENTRIES = 10_000;
4
- const AUTH_NEGATIVE_CACHE_TTL_MS = 1_500;
4
+ export const AUTH_NEGATIVE_CACHE_TTL_MS = 1_500;
5
5
  const AUTH_FAILURE_COOLDOWN_MS = 1_000;
6
6
 
7
7
  interface CacheEntry<T> {
@@ -21,8 +21,12 @@ import type { ApiKeyModelAccessMode } from "../persistence/schema";
21
21
  * snapshot's consumed value is a reference point, so a brief stale window
22
22
  * cannot meaningfully over-consume a budget.
23
23
  *
24
- * Negative results (unknown/revoked tokens) are deliberately NOT cached, so
25
- * a newly minted key is usable immediately.
24
+ * Unknown or revoked tokens are negative-cached for
25
+ * `AUTH_NEGATIVE_CACHE_TTL_MS` (1.5s): a token rejected within that window
26
+ * stays refused even if its key is minted or un-revoked in between. A newly
27
+ * minted key is usable immediately only because `invalidateApiKeyCache`
28
+ * clears the negative entries when a mutation lands — not because negatives
29
+ * are absent.
26
30
  */
27
31
  const authorizationCache = createApiKeyAuthorizationCache<ResolvedApiKey>();
28
32
 
@@ -622,19 +622,20 @@ export function nativeCompletionBase(input: {
622
622
  | "tokensPerSec"
623
623
  > {
624
624
  return {
625
- providerId: input.providerId,
626
- modelId: input.modelId,
627
- tenantId: input.tenantId,
628
- ingressBody: input.ingressBody,
629
- terminal: true,
630
- ...(input.accountId === undefined ? {} : { accountId: input.accountId }),
631
- ...(input.accountLabel === undefined ? {} : { accountLabel: input.accountLabel }),
632
- ...(input.networkPoolId === undefined ? {} : { networkPoolId: input.networkPoolId }),
633
- ...(input.lease === undefined ? {} : { lease: input.lease }),
634
- db: input.db,
635
- ...(input.telemetryBuffer === undefined ? {} : { telemetryBuffer: input.telemetryBuffer }),
636
- ...(input.snapshotService === undefined ? {} : { snapshotService: input.snapshotService }),
637
- ...(input.providerCapture === undefined ? {} : { providerCapture: input.providerCapture }),
625
+ ...completionContext({
626
+ providerId: input.providerId,
627
+ modelId: input.modelId,
628
+ tenantId: input.tenantId,
629
+ ingressBody: input.ingressBody,
630
+ ...(input.providerCapture === undefined ? {} : { providerCapture: input.providerCapture }),
631
+ db: input.db,
632
+ ...(input.accountId === undefined ? {} : { accountId: input.accountId }),
633
+ ...(input.accountLabel === undefined ? {} : { accountLabel: input.accountLabel }),
634
+ ...(input.networkPoolId === undefined ? {} : { networkPoolId: input.networkPoolId }),
635
+ ...(input.lease === undefined ? {} : { lease: input.lease }),
636
+ ...(input.telemetryBuffer === undefined ? {} : { telemetryBuffer: input.telemetryBuffer }),
637
+ ...(input.snapshotService === undefined ? {} : { snapshotService: input.snapshotService }),
638
+ }),
638
639
  ...(input.usage === undefined ? {} : { usage: input.usage }),
639
640
  ...(input.commitUsage === undefined ? {} : { commitUsage: input.commitUsage }),
640
641
  };
@@ -32,7 +32,7 @@ import { metrics } from "../../observability/metrics";
32
32
  import type { NetworkPoolSelector } from "../../network/pool/selector";
33
33
  import type { ProxyRequestState } from "../request/state";
34
34
  import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
35
- import { flagPoolBlocked, flagPoolCooldown } from "../../network/pool-health";
35
+ import { flagPoolOnEgressFailure } from "../../network/pool-health";
36
36
  import {
37
37
  completeAttempt,
38
38
  errorClientResponseBody,
@@ -61,6 +61,41 @@ function comboCandidateKey(candidate: RouteCandidate): string {
61
61
  ].join(":");
62
62
  }
63
63
 
64
+ /**
65
+ * Extra dials allowed for a mid-response upstream socket drop when the
66
+ * candidate list is exhausted (single-account providers). One extra dial
67
+ * recovers the transient reset; more would stack latency on a dying origin.
68
+ */
69
+ const MAX_SOCKET_DROP_RETRIES = 1;
70
+
71
+ /**
72
+ * Normalizes a raw (non-GatewayError) failure before it escapes the loop.
73
+ *
74
+ * A transport-level throw — e.g. Bun's `TypeError` with
75
+ * `code: "ECONNRESET"` when the upstream socket dies mid-response — carries a
76
+ * `.code` of its own. The error middleware reads that `.code` for Elysia
77
+ * built-in errors, so a raw socket drop leaked to the client as
78
+ * `{"code":"ECONNRESET","status":400}` with origin `cartethyia`: the wrong
79
+ * code, the wrong status, and the wrong blame. The classifier already knows
80
+ * what this is (`transport_unavailable`, 502, upstream) — materialize it as a
81
+ * real GatewayError so the client and the captured Client Response see the
82
+ * same stable envelope every other upstream transport failure gets.
83
+ */
84
+ function normalizeEscapeError(error: unknown, signal: AbortSignal): unknown {
85
+ if (error instanceof GatewayError) return error;
86
+ const terminal = classifyTerminalOutcome(error, signal);
87
+ if (terminal.errorCategory === "transport_unavailable") {
88
+ return new GatewayError(
89
+ "transport_unavailable",
90
+ 502,
91
+ "The upstream closed the connection mid-response (request {requestId}). Retry; if it keeps happening, the account or pool is unstable. The connection ghosted us — come back in a moment 👻",
92
+ {},
93
+ "upstream",
94
+ );
95
+ }
96
+ return error;
97
+ }
98
+
64
99
  /**
65
100
  * True when the rejection is about the API key or the gateway's own policy,
66
101
  * not about the provider account that was dialled.
@@ -321,7 +356,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
321
356
  status: terminal.status === "cancelled" ? "cancelled" : "failed",
322
357
  });
323
358
  }
324
- throw lastError;
359
+ throw normalizeEscapeError(lastError, state.abortController.signal);
325
360
  }
326
361
  }
327
362
  let lease: AdmissionLease | undefined;
@@ -381,6 +416,13 @@ export async function runAttemptLoop<TResult, TAdapter>(
381
416
  // names the real cause.
382
417
  const drain = drainAbortReason(state.abortController.signal.reason);
383
418
  if (drain !== undefined) error = drain;
419
+ // Normalize a raw transport throw (e.g. Bun's `TypeError` with
420
+ // `code: "ECONNRESET"` when the upstream socket dies mid-response) into
421
+ // the stable `transport_unavailable`/502/upstream GatewayError **before**
422
+ // classification, so the telemetry row, the captured Client Response, and
423
+ // the escaped error all agree — and the raw `.code` never leaks to the
424
+ // client as a fake Elysia built-in code (see `normalizeEscapeError`).
425
+ error = normalizeEscapeError(error, state.abortController.signal);
384
426
  lastError = error;
385
427
  // A drain is a *server* close, not a client cancel: it must be recorded
386
428
  // as a failure with the shutdown code, and it is never retryable.
@@ -411,21 +453,33 @@ export async function runAttemptLoop<TResult, TAdapter>(
411
453
  // the pool excluded. Both flag helpers write the local entry
412
454
  // synchronously, so the ordering holds even though the Redis mirror is
413
455
  // fire-and-forget.
414
- if (networkPoolId && deps.poolSelector && error instanceof GatewayError) {
415
- if (rateLimitedPool) {
416
- flagPoolCooldown(deps.poolSelector, deps.db, networkPoolId, candidate.provider_id, error);
417
- } else if (blockedPool) {
418
- // The address itself is refused, not the request: retire the egress
419
- // so the next attempt leaves by a different one instead of retrying
420
- // a proxy the upstream has already flagged.
421
- flagPoolBlocked(deps.poolSelector, deps.db, networkPoolId, candidate.provider_id, error);
422
- }
423
- }
456
+ flagPoolOnEgressFailure(
457
+ deps.poolSelector,
458
+ deps.db,
459
+ networkPoolId,
460
+ candidate.provider_id,
461
+ error,
462
+ egressVerdict,
463
+ );
464
+ // A mid-response upstream socket drop (`transport_unavailable`) is the one
465
+ // retryable failure that receives an extra dial even when the candidate
466
+ // list is exhausted: an origin that closes the connection before the
467
+ // response is often transient (Trae's edge resets under concurrency),
468
+ // and the fresh dial recovers it — verified live 2026-11-09, 7/7
469
+ // recovered on the first retry. Without this, a single-account provider
470
+ // (the common case) tripped `index === candidates.length - 1` on the very
471
+ // first attempt and the client saw the raw socket error. The retry is
472
+ // bounded to one extra dial per request, never unbounded.
473
+ const memberAttempts = attemptsByMember.get(memberKey) ?? 0;
474
+ const socketDropRetryLeft =
475
+ terminal.errorCategory === "transport_unavailable" &&
476
+ !cancelled &&
477
+ memberAttempts < MAX_SOCKET_DROP_RETRIES;
424
478
  const terminalAttempt =
425
479
  cancelled ||
426
480
  isClientScopedRejection(error) ||
427
481
  !isRetryableFailure(error) ||
428
- (index === candidates.length - 1 && !poolScopedFailure);
482
+ ((index === candidates.length - 1 && !poolScopedFailure) && !socketDropRetryLeft);
429
483
  // Track only the most recent fired exchange; the array this replaced
430
484
  // accumulated every attempt's capture until the loop returned.
431
485
  if (providerCapture.request !== null) lastFiredCapture = providerCapture;
@@ -474,7 +528,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
474
528
  // A 403 can be a deterministic provider policy rejection, not an
475
529
  // invalid OAuth credential. Only the evidence-based classifier above
476
530
  // may trigger a refresh.
477
- const candidateKey = `${candidate.provider_id}:${candidate.provider_account_id ?? "global"}:${candidate.model_id}:${candidate.endpoint}`;
531
+ const candidateKey = comboCandidateKey(candidate);
478
532
  const authInvalidated = isOAuthCredentialInvalidated(error);
479
533
  if (
480
534
  !cancelled &&
@@ -499,7 +553,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
499
553
  }
500
554
  if (terminalAttempt) {
501
555
  metrics.proxy_requests_total.inc(1, { status: cancelled ? "cancelled" : "failed" });
502
- throw error;
556
+ throw normalizeEscapeError(error, state.abortController.signal);
503
557
  }
504
558
  // A failed combo member stays flagged for this request. Move it behind
505
559
  // every candidate that has not failed yet, so the next attempt cannot
@@ -542,7 +596,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
542
596
  // as the sleep's own error would leave `proxy_requests_total` missing
543
597
  // the request.
544
598
  metrics.proxy_requests_total.inc(1, { status: "cancelled" });
545
- throw lastError;
599
+ throw normalizeEscapeError(lastError, state.abortController.signal);
546
600
  }
547
601
  }
548
602
  }
@@ -577,5 +631,5 @@ export async function runAttemptLoop<TResult, TAdapter>(
577
631
  }));
578
632
  }
579
633
  metrics.proxy_requests_total.inc(1, { status: "failed" });
580
- throw lastError;
634
+ throw normalizeEscapeError(lastError, state.abortController.signal);
581
635
  }
@@ -27,6 +27,7 @@ import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt
27
27
  import { runAttemptLoop } from "./attempt-loop";
28
28
  import { captureProviderRateLimit } from "./ratelimit-capture";
29
29
  import {
30
+ forwardUpstreamResponse,
30
31
  hasScope,
31
32
  jsonResponse,
32
33
  networkFetchContext,
@@ -133,14 +134,7 @@ async function passThrough(
133
134
  responseBody: null,
134
135
  });
135
136
  metrics.proxy_requests_total.inc(1, { status: "completed" });
136
- return new Response(upstream.body, {
137
- status: upstream.status,
138
- headers: {
139
- "content-type": upstream.headers.get("content-type") ?? "text/event-stream",
140
- "cache-control": "no-store",
141
- "x-request-id": state.requestId,
142
- },
143
- });
137
+ return forwardUpstreamResponse(upstream, state.requestId, "text/event-stream");
144
138
  }
145
139
 
146
140
  /** Builds the handler for one generation surface. */
@@ -71,6 +71,31 @@ export function jsonResponse(payload: unknown, status: number, requestId: string
71
71
  });
72
72
  }
73
73
 
74
+ /**
75
+ * Forwards an upstream response body verbatim with the shared native-response
76
+ * header set.
77
+ *
78
+ * The native handlers (System One, responses-compact, token-count,
79
+ * generation's passThrough) each spelled the same three headers by hand; the
80
+ * copies drifted at least once (generation's JSON builder lost `cache-control`).
81
+ * Status and body come from the upstream untouched; only the content-type
82
+ * default differs per route (SSE vs JSON) and is passed per call.
83
+ */
84
+ export function forwardUpstreamResponse(
85
+ response: Response,
86
+ requestId: string,
87
+ defaultContentType = "application/json",
88
+ ): Response {
89
+ return new Response(response.body, {
90
+ status: response.status,
91
+ headers: {
92
+ "content-type": response.headers.get("content-type") ?? defaultContentType,
93
+ "cache-control": "no-store",
94
+ "x-request-id": requestId,
95
+ },
96
+ });
97
+ }
98
+
74
99
  /**
75
100
  * Strips a thinking suffix (`model(high)`) from the model name and rebuilds
76
101
  * the body with the bare model when it changed. Returns the bare model and
@@ -11,7 +11,7 @@ import { chatAdapter } from "../surface/chat/adapter";
11
11
  import { responsesAdapter } from "../surface/responses/adapter";
12
12
  import { messagesAdapter } from "../surface/messages/adapter";
13
13
  import { completionAdapter } from "../surface/completion";
14
- import { forwardedRequestHeaders, proxySuccessHeaders, buildUpstreamDispatchContext, reportedModelFrom } from "./upstream";
14
+ import { forwardedRequestHeaders, proxySuccessHeaders, buildUpstreamDispatchContext, buildDispatchNetworkBindings, reportedModelFrom } from "./upstream";
15
15
  import { resolvePromptCacheKey } from "../../providers/operations/session-resolution";
16
16
  import {
17
17
  applyTenantPreferences,
@@ -21,18 +21,12 @@ import {
21
21
  import { metrics } from "../../observability/metrics";
22
22
  import { repriceUsage } from "../../providers/provider-usage";
23
23
  import {
24
- captureProviderExchange,
25
24
  completeAttempt,
26
25
  completionContext,
27
26
  estimatedUsage,
28
27
  parseCapturedBody,
29
28
  terminalFailure,
30
29
  } from "./attempt-finalize";
31
- import {
32
- providerCaptureMaxBytes,
33
- resolveCaptureDepthForRequest,
34
- shouldCaptureProviderBodyForRequest,
35
- } from "./capture-policy";
36
30
  import { runAttemptLoop } from "./attempt-loop";
37
31
  import { projectForRoute, routeCapabilitiesFor } from "../../protocol/translation/capabilities";
38
32
  import { dispatchStreamingAttempt } from "./streaming/stream";
@@ -263,45 +257,14 @@ export async function handleProviderProxyRequest(
263
257
  ...(conversationAffinity ? { conversationAffinity } : {}),
264
258
  ...(tenantSystemPromptText === undefined ? {} : { tenantSystemPrompt: tenantSystemPromptText }),
265
259
  ...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
266
- ...(deps.networkBindingFactory
267
- ? {
268
- outboundFetch: captureProviderExchange(
269
- deps.networkBindingFactory.fetch(
270
- networkPoolId,
271
- prepared.authorization.snapshot.tenant_id,
272
- ),
273
- providerCapture,
274
- await shouldCaptureProviderBodyForRequest(
275
- deps.db,
276
- prepared.authorization.snapshot.tenant_id,
277
- state,
278
- ),
279
- providerCaptureMaxBytes(
280
- await resolveCaptureDepthForRequest(
281
- deps.db,
282
- prepared.authorization.snapshot.tenant_id,
283
- state,
284
- ),
285
- ),
286
- ),
287
- }
288
- : {}),
289
- ...(deps.networkBindingFactory
290
- ? {
291
- outboundAttachmentFetch: deps.networkBindingFactory.fetch(
292
- networkPoolId,
293
- prepared.authorization.snapshot.tenant_id,
294
- ),
295
- }
296
- : {}),
297
- ...(deps.networkBindingFactory
298
- ? {
299
- outboundHttp2: deps.networkBindingFactory.http2Bidi(
300
- networkPoolId,
301
- prepared.authorization.snapshot.tenant_id,
302
- ),
303
- }
304
- : {}),
260
+ ...(providerCapture ? { providerCapture } : {}),
261
+ ...(await buildDispatchNetworkBindings({
262
+ deps,
263
+ networkPoolId,
264
+ tenantId: prepared.authorization.snapshot.tenant_id,
265
+ providerCapture,
266
+ state,
267
+ })),
305
268
  ...(leaseOwnership === undefined ? {} : { leaseOwnership }),
306
269
  ...(continuationScope === undefined ? {} : { continuationScope }),
307
270
  });
@@ -4,6 +4,7 @@ import type { CodexCompactAdapter } from "../../providers/integrations/codex/cod
4
4
  import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt-finalize";
5
5
  import { runAttemptLoop } from "./attempt-loop";
6
6
  import {
7
+ forwardUpstreamResponse,
7
8
  networkFetchContext,
8
9
  requireRecordBody,
9
10
  resolveAttemptCredential,
@@ -143,14 +144,7 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
143
144
  responseBody: null,
144
145
  });
145
146
  metrics.proxy_requests_total.inc(1, { status: "completed" });
146
- return new Response(response.body, {
147
- status: response.status,
148
- headers: {
149
- "content-type": response.headers.get("content-type") ?? "application/json",
150
- "cache-control": "no-store",
151
- "x-request-id": state.requestId,
152
- },
153
- });
147
+ return forwardUpstreamResponse(response, state.requestId);
154
148
  },
155
149
  });
156
150
  };