@remodex/rmx 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (588) hide show
  1. package/AGENTS_INSTALL.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +584 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CZqebSPQ.css +1 -0
  11. package/gui/dist/assets/index-CkETtt7P.js +71 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
  14. package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
  15. package/gui/dist/icons.svg +24 -0
  16. package/gui/dist/index.html +25 -0
  17. package/gui/dist/logo.png +0 -0
  18. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  19. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude-color.svg +1 -0
  21. package/gui/dist/provider-icons/cline-color.svg +16 -0
  22. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  23. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  24. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  25. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok.svg +1 -0
  34. package/gui/dist/provider-icons/groq-color.svg +1 -0
  35. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  36. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  37. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  38. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  39. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  40. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  41. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  42. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  43. package/gui/dist/provider-icons/openai.svg +1 -0
  44. package/gui/dist/provider-icons/opencode.svg +2 -0
  45. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  46. package/gui/dist/provider-icons/pi.svg +21 -0
  47. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  48. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  49. package/gui/dist/provider-icons/telegram.svg +1 -0
  50. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  51. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  52. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  53. package/package.json +118 -0
  54. package/src/AGENTS.md +28 -0
  55. package/src/adapters/anthropic-image-guard.ts +251 -0
  56. package/src/adapters/anthropic-image-normalize.ts +518 -0
  57. package/src/adapters/anthropic.ts +1205 -0
  58. package/src/adapters/azure.ts +36 -0
  59. package/src/adapters/base.ts +83 -0
  60. package/src/adapters/client-fingerprint.ts +59 -0
  61. package/src/adapters/command-code.ts +453 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/cursor-errors.ts +165 -0
  65. package/src/adapters/cursor/discovery.ts +276 -0
  66. package/src/adapters/cursor/effort-map.ts +139 -0
  67. package/src/adapters/cursor/exec-policy.ts +88 -0
  68. package/src/adapters/cursor/framing.ts +250 -0
  69. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  70. package/src/adapters/cursor/kv-store.ts +52 -0
  71. package/src/adapters/cursor/live-models.ts +153 -0
  72. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  73. package/src/adapters/cursor/live-transport.ts +1235 -0
  74. package/src/adapters/cursor/mcp-config.ts +42 -0
  75. package/src/adapters/cursor/mcp-manager.ts +333 -0
  76. package/src/adapters/cursor/message-mapper.ts +49 -0
  77. package/src/adapters/cursor/native-exec-common.ts +59 -0
  78. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  79. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  80. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  81. package/src/adapters/cursor/native-exec-network.ts +43 -0
  82. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  83. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  84. package/src/adapters/cursor/native-exec.ts +604 -0
  85. package/src/adapters/cursor/protobuf-events.ts +735 -0
  86. package/src/adapters/cursor/protobuf-request.ts +719 -0
  87. package/src/adapters/cursor/request-builder.ts +280 -0
  88. package/src/adapters/cursor/thread-continuity.ts +67 -0
  89. package/src/adapters/cursor/tool-definitions.ts +621 -0
  90. package/src/adapters/cursor/transport-retry.ts +132 -0
  91. package/src/adapters/cursor/transport.ts +57 -0
  92. package/src/adapters/cursor/types.ts +59 -0
  93. package/src/adapters/cursor.ts +196 -0
  94. package/src/adapters/google-antigravity-replay.ts +520 -0
  95. package/src/adapters/google-antigravity-wire.ts +140 -0
  96. package/src/adapters/google-errors.ts +85 -0
  97. package/src/adapters/google-http.ts +100 -0
  98. package/src/adapters/google-tool-schema.ts +173 -0
  99. package/src/adapters/google-truncation.ts +24 -0
  100. package/src/adapters/google-wire-compiler.ts +232 -0
  101. package/src/adapters/google.ts +859 -0
  102. package/src/adapters/identity.ts +77 -0
  103. package/src/adapters/image.ts +23 -0
  104. package/src/adapters/kiro-constants.ts +16 -0
  105. package/src/adapters/kiro-errors.ts +208 -0
  106. package/src/adapters/kiro-events.ts +197 -0
  107. package/src/adapters/kiro-images.ts +129 -0
  108. package/src/adapters/kiro-retry.ts +312 -0
  109. package/src/adapters/kiro-thinking.ts +104 -0
  110. package/src/adapters/kiro-tool-fallback.ts +36 -0
  111. package/src/adapters/kiro-tools.ts +224 -0
  112. package/src/adapters/kiro-truncation.ts +33 -0
  113. package/src/adapters/kiro-wire.ts +129 -0
  114. package/src/adapters/kiro.ts +1924 -0
  115. package/src/adapters/mimo-free.ts +263 -0
  116. package/src/adapters/openai-chat.ts +1265 -0
  117. package/src/adapters/openai-responses.ts +1309 -0
  118. package/src/adapters/run-turn-queue.ts +114 -0
  119. package/src/adapters/tool-catalog-nudge.ts +71 -0
  120. package/src/adapters/upstream-http-error.ts +48 -0
  121. package/src/android-remote/assets.ts +218 -0
  122. package/src/android-remote/attachments.ts +168 -0
  123. package/src/android-remote/auth.ts +237 -0
  124. package/src/android-remote/cloudflare-provisioning.ts +409 -0
  125. package/src/android-remote/cloudflare-secret.ts +106 -0
  126. package/src/android-remote/cloudflare-tunnel.ts +488 -0
  127. package/src/android-remote/cloudflared.ts +286 -0
  128. package/src/android-remote/codex-app-server.ts +565 -0
  129. package/src/android-remote/desktop-history-page.ts +936 -0
  130. package/src/android-remote/desktop-ipc.ts +3643 -0
  131. package/src/android-remote/desktop-ownership-store.ts +98 -0
  132. package/src/android-remote/desktop-project-registration.ts +129 -0
  133. package/src/android-remote/desktop-session-stream.ts +1110 -0
  134. package/src/android-remote/desktop-workspace-state.ts +443 -0
  135. package/src/android-remote/file-change-parser.ts +112 -0
  136. package/src/android-remote/gateway.ts +9108 -0
  137. package/src/android-remote/mutation-store.ts +299 -0
  138. package/src/android-remote/projection.ts +1780 -0
  139. package/src/android-remote/queued-turn-store.ts +248 -0
  140. package/src/android-remote/session-command-recovery.ts +1384 -0
  141. package/src/android-remote/store.ts +466 -0
  142. package/src/android-remote/thread-reconciliation.ts +133 -0
  143. package/src/android-remote/thread-source-paths.ts +307 -0
  144. package/src/android-remote/thread-stream.ts +546 -0
  145. package/src/android-remote/turn-activity.ts +235 -0
  146. package/src/android-remote/user-message-identity.ts +94 -0
  147. package/src/bridge.ts +1793 -0
  148. package/src/chat/inbound.ts +295 -0
  149. package/src/chat/outbound.ts +821 -0
  150. package/src/claude/agents-inject.ts +267 -0
  151. package/src/claude/alias.ts +149 -0
  152. package/src/claude/auth-detect.ts +229 -0
  153. package/src/claude/auth-mode-migration.ts +32 -0
  154. package/src/claude/auth-mode.ts +62 -0
  155. package/src/claude/context-windows.ts +189 -0
  156. package/src/claude/desktop-3p-guard.ts +35 -0
  157. package/src/claude/desktop-3p-paths.ts +84 -0
  158. package/src/claude/desktop-3p.ts +601 -0
  159. package/src/claude/desktop-health.ts +26 -0
  160. package/src/claude/desktop-profile.ts +263 -0
  161. package/src/claude/gateway-cache.ts +70 -0
  162. package/src/claude/inbound-debug.ts +163 -0
  163. package/src/claude/inbound.ts +519 -0
  164. package/src/claude/model-info.ts +154 -0
  165. package/src/claude/outbound.ts +898 -0
  166. package/src/cli/access.ts +108 -0
  167. package/src/cli/account-api.ts +296 -0
  168. package/src/cli/account-auth.ts +250 -0
  169. package/src/cli/account-catalog-refresh.ts +14 -0
  170. package/src/cli/account-extended.ts +476 -0
  171. package/src/cli/account-main.ts +317 -0
  172. package/src/cli/account.ts +297 -0
  173. package/src/cli/agent-driven.ts +70 -0
  174. package/src/cli/agent.ts +184 -0
  175. package/src/cli/catalog-prewarm.ts +27 -0
  176. package/src/cli/claude-desktop.ts +211 -0
  177. package/src/cli/claude.ts +302 -0
  178. package/src/cli/codex-shim-autorestore.ts +45 -0
  179. package/src/cli/codex-shim-readiness.ts +69 -0
  180. package/src/cli/combo.ts +124 -0
  181. package/src/cli/config-command.ts +183 -0
  182. package/src/cli/debug.ts +228 -0
  183. package/src/cli/desktop-first-run.ts +25 -0
  184. package/src/cli/doctor.ts +1022 -0
  185. package/src/cli/export-command.ts +201 -0
  186. package/src/cli/help.ts +370 -0
  187. package/src/cli/index.ts +1565 -0
  188. package/src/cli/init.ts +224 -0
  189. package/src/cli/integrations.ts +225 -0
  190. package/src/cli/interactive-confirm.ts +133 -0
  191. package/src/cli/internal-dispatch.ts +35 -0
  192. package/src/cli/launcher-context.ts +77 -0
  193. package/src/cli/models-runtime.ts +224 -0
  194. package/src/cli/models.ts +340 -0
  195. package/src/cli/observe.ts +170 -0
  196. package/src/cli/opencode.ts +587 -0
  197. package/src/cli/provider-runtime.ts +179 -0
  198. package/src/cli/provider.ts +476 -0
  199. package/src/cli/ready.ts +301 -0
  200. package/src/cli/route-policy.ts +92 -0
  201. package/src/cli/runtime-api.ts +328 -0
  202. package/src/cli/star-prompt.ts +211 -0
  203. package/src/cli/status-oauth.ts +78 -0
  204. package/src/cli/status.ts +321 -0
  205. package/src/cli/system-command.ts +196 -0
  206. package/src/cli/system-restart-client.ts +146 -0
  207. package/src/cli/tray-proxy.ts +205 -0
  208. package/src/cli/v2.ts +200 -0
  209. package/src/cli.ts +10 -0
  210. package/src/clients/config-export.ts +1109 -0
  211. package/src/codex/account-id.ts +34 -0
  212. package/src/codex/account-label.ts +34 -0
  213. package/src/codex/account-lifecycle.ts +172 -0
  214. package/src/codex/account-namespace-match.ts +63 -0
  215. package/src/codex/account-namespaces.ts +195 -0
  216. package/src/codex/account-pause.ts +20 -0
  217. package/src/codex/account-priority.ts +83 -0
  218. package/src/codex/account-runtime-state.ts +31 -0
  219. package/src/codex/account-store.ts +517 -0
  220. package/src/codex/account-usability.ts +40 -0
  221. package/src/codex/admission.ts +263 -0
  222. package/src/codex/app-server-processes.ts +799 -0
  223. package/src/codex/auth-api.ts +2098 -0
  224. package/src/codex/auth-collision.ts +107 -0
  225. package/src/codex/auth-context.ts +480 -0
  226. package/src/codex/autostart-health.ts +156 -0
  227. package/src/codex/catalog/account-models.ts +67 -0
  228. package/src/codex/catalog/aggregation.ts +471 -0
  229. package/src/codex/catalog/bundled.ts +533 -0
  230. package/src/codex/catalog/effort.ts +432 -0
  231. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  232. package/src/codex/catalog/kinds.ts +2 -0
  233. package/src/codex/catalog/metadata.ts +287 -0
  234. package/src/codex/catalog/native-models.ts +7 -0
  235. package/src/codex/catalog/parsing.ts +503 -0
  236. package/src/codex/catalog/provider-fetch.ts +2267 -0
  237. package/src/codex/catalog/sync.ts +1606 -0
  238. package/src/codex/catalog-admission.ts +197 -0
  239. package/src/codex/catalog-refresh-status.ts +87 -0
  240. package/src/codex/catalog-write-serialization.ts +242 -0
  241. package/src/codex/catalog.ts +15 -0
  242. package/src/codex/codex-write-lock.ts +384 -0
  243. package/src/codex/convergence-types.ts +593 -0
  244. package/src/codex/convergence.ts +580 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/desktop-client-processes.ts +521 -0
  249. package/src/codex/exec-invocation.ts +22 -0
  250. package/src/codex/features.ts +1091 -0
  251. package/src/codex/generation.ts +202 -0
  252. package/src/codex/history-job.ts +347 -0
  253. package/src/codex/history-lock.ts +242 -0
  254. package/src/codex/history-migration-guardian.ts +115 -0
  255. package/src/codex/history-provider.ts +1075 -0
  256. package/src/codex/history-transition.ts +105 -0
  257. package/src/codex/history-worker.ts +204 -0
  258. package/src/codex/home.ts +206 -0
  259. package/src/codex/inject-coordination.ts +257 -0
  260. package/src/codex/inject.ts +1857 -0
  261. package/src/codex/injected-marker.ts +79 -0
  262. package/src/codex/integration-record.ts +266 -0
  263. package/src/codex/internal/catalog-writer.ts +203 -0
  264. package/src/codex/internal/history-writer.ts +105 -0
  265. package/src/codex/journal.ts +172 -0
  266. package/src/codex/main-account-cache.ts +56 -0
  267. package/src/codex/main-account.ts +40 -0
  268. package/src/codex/management-convergence.ts +114 -0
  269. package/src/codex/model-cache.ts +267 -0
  270. package/src/codex/native-main-admission.ts +47 -0
  271. package/src/codex/native-main-auth-temp.ts +187 -0
  272. package/src/codex/native-main-claim.ts +167 -0
  273. package/src/codex/native-main-lock-file.ts +162 -0
  274. package/src/codex/native-main-owner.ts +329 -0
  275. package/src/codex/native-profile-api.ts +247 -0
  276. package/src/codex/native-profile-manager.ts +1531 -0
  277. package/src/codex/native-profile-processes.ts +121 -0
  278. package/src/codex/native-profile-recovery.ts +99 -0
  279. package/src/codex/native-profile-stage-store.ts +387 -0
  280. package/src/codex/native-profile-startup.ts +348 -0
  281. package/src/codex/native-profile-store.ts +855 -0
  282. package/src/codex/native-profile-types.ts +120 -0
  283. package/src/codex/native-residue.ts +691 -0
  284. package/src/codex/paths.ts +78 -0
  285. package/src/codex/plugins-doctor.ts +242 -0
  286. package/src/codex/pool-rotation.ts +295 -0
  287. package/src/codex/project-config-warnings.ts +426 -0
  288. package/src/codex/prompt-journal.ts +311 -0
  289. package/src/codex/prompt-layers.ts +967 -0
  290. package/src/codex/prompt-lock.ts +143 -0
  291. package/src/codex/provider-adoption.ts +242 -0
  292. package/src/codex/quota-rejection.ts +224 -0
  293. package/src/codex/quota.ts +494 -0
  294. package/src/codex/refresh.ts +60 -0
  295. package/src/codex/routing.ts +1855 -0
  296. package/src/codex/runtime.ts +659 -0
  297. package/src/codex/shim.ts +1215 -0
  298. package/src/codex/subagent-defaults.ts +557 -0
  299. package/src/codex/subagent-model-fallback.ts +560 -0
  300. package/src/codex/sync.ts +238 -0
  301. package/src/codex/transition-state.ts +612 -0
  302. package/src/codex/upstream-host-health.ts +368 -0
  303. package/src/codex/user-identity.ts +374 -0
  304. package/src/codex/warmup.ts +192 -0
  305. package/src/codex/websocket-registry.ts +100 -0
  306. package/src/codex/write-coordination.ts +114 -0
  307. package/src/combos/failover.ts +140 -0
  308. package/src/combos/index.ts +44 -0
  309. package/src/combos/request.ts +64 -0
  310. package/src/combos/resolve.ts +232 -0
  311. package/src/combos/types.ts +392 -0
  312. package/src/config.ts +3270 -0
  313. package/src/generated/model-metadata.ts +144 -0
  314. package/src/github/star-state.ts +203 -0
  315. package/src/grok/inject.ts +540 -0
  316. package/src/grok/inspect.ts +45 -0
  317. package/src/grok/status.ts +127 -0
  318. package/src/grok/sync.ts +66 -0
  319. package/src/images/artifacts.ts +516 -0
  320. package/src/images/fulfill-video.ts +163 -0
  321. package/src/images/fulfill.ts +149 -0
  322. package/src/images/index.ts +4 -0
  323. package/src/images/loop.ts +922 -0
  324. package/src/images/plan.ts +133 -0
  325. package/src/images/synthetic-tool.ts +133 -0
  326. package/src/images/types.ts +41 -0
  327. package/src/images/xai-client.ts +141 -0
  328. package/src/images/xai-video-client.ts +163 -0
  329. package/src/index.ts +22 -0
  330. package/src/integrations/config-io.ts +151 -0
  331. package/src/integrations/journal.ts +315 -0
  332. package/src/integrations/merge.ts +135 -0
  333. package/src/integrations/native/ownership-preflight.ts +202 -0
  334. package/src/integrations/ownership.ts +111 -0
  335. package/src/integrations/registry.ts +108 -0
  336. package/src/integrations/serialize.ts +235 -0
  337. package/src/integrations/state.ts +290 -0
  338. package/src/integrations/store.ts +103 -0
  339. package/src/integrations/writer.ts +492 -0
  340. package/src/lib/abort.ts +146 -0
  341. package/src/lib/admin-secrets.ts +25 -0
  342. package/src/lib/admission.ts +83 -0
  343. package/src/lib/app-owned-memory-stores.ts +173 -0
  344. package/src/lib/app-owned-memory.ts +265 -0
  345. package/src/lib/bounded-body.ts +242 -0
  346. package/src/lib/bun-binary-validator.d.mts +3 -0
  347. package/src/lib/bun-binary-validator.mjs +18 -0
  348. package/src/lib/bun-runtime.ts +184 -0
  349. package/src/lib/bun-stream-caps.ts +127 -0
  350. package/src/lib/config-ownership.ts +438 -0
  351. package/src/lib/crash-guard.ts +344 -0
  352. package/src/lib/debug-log-buffer.ts +83 -0
  353. package/src/lib/debug-settings.ts +108 -0
  354. package/src/lib/debug.ts +31 -0
  355. package/src/lib/destination-policy.ts +316 -0
  356. package/src/lib/errors.ts +364 -0
  357. package/src/lib/eventstream-decoder.ts +253 -0
  358. package/src/lib/gcp-adc.ts +341 -0
  359. package/src/lib/injection-debug-log.ts +58 -0
  360. package/src/lib/local-management-attestation.ts +51 -0
  361. package/src/lib/open-url.ts +25 -0
  362. package/src/lib/pinned-http.ts +182 -0
  363. package/src/lib/privacy.ts +20 -0
  364. package/src/lib/process-control.ts +168 -0
  365. package/src/lib/provider-environment.ts +470 -0
  366. package/src/lib/provider-outbound.ts +203 -0
  367. package/src/lib/provider-url.ts +14 -0
  368. package/src/lib/proxy-env.ts +18 -0
  369. package/src/lib/redact.ts +510 -0
  370. package/src/lib/remodex-home.ts +616 -0
  371. package/src/lib/retry-after.ts +55 -0
  372. package/src/lib/service-secrets.ts +178 -0
  373. package/src/lib/shadow-call.ts +54 -0
  374. package/src/lib/sidecar-tracker.ts +52 -0
  375. package/src/lib/sse-decoder.ts +364 -0
  376. package/src/lib/state-store-registrations.ts +109 -0
  377. package/src/lib/state-store-sweeper.ts +184 -0
  378. package/src/lib/system-restart-contract.ts +73 -0
  379. package/src/lib/test-home-guard.ts +98 -0
  380. package/src/lib/token-estimate.ts +69 -0
  381. package/src/lib/translator-budget.ts +366 -0
  382. package/src/lib/upstream-reachability.ts +91 -0
  383. package/src/lib/upstream-retry.ts +508 -0
  384. package/src/lib/win-exec.ts +115 -0
  385. package/src/lib/win-paths.ts +68 -0
  386. package/src/lib/windows-elevation.ts +705 -0
  387. package/src/lib/windows-secret-acl.ts +817 -0
  388. package/src/lib/windows-user-principal.ts +283 -0
  389. package/src/lib/winsw.ts +402 -0
  390. package/src/model-sources.ts +73 -0
  391. package/src/oauth/anthropic-routing.ts +594 -0
  392. package/src/oauth/anthropic.ts +177 -0
  393. package/src/oauth/callback-server.ts +294 -0
  394. package/src/oauth/chatgpt.ts +150 -0
  395. package/src/oauth/command-code.ts +239 -0
  396. package/src/oauth/cursor.ts +231 -0
  397. package/src/oauth/github-copilot.ts +428 -0
  398. package/src/oauth/google-antigravity.ts +230 -0
  399. package/src/oauth/health.ts +443 -0
  400. package/src/oauth/index.ts +1280 -0
  401. package/src/oauth/key-providers.ts +128 -0
  402. package/src/oauth/kimi.ts +213 -0
  403. package/src/oauth/kiro-credentials.ts +726 -0
  404. package/src/oauth/kiro.ts +621 -0
  405. package/src/oauth/local-token-detect.ts +121 -0
  406. package/src/oauth/log.ts +48 -0
  407. package/src/oauth/login-cli.ts +163 -0
  408. package/src/oauth/pkce.ts +15 -0
  409. package/src/oauth/store.ts +655 -0
  410. package/src/oauth/token-guardian.ts +309 -0
  411. package/src/oauth/types.ts +62 -0
  412. package/src/oauth/xai.ts +241 -0
  413. package/src/providers/alibaba-region-backup.ts +75 -0
  414. package/src/providers/alibaba-region-migration.ts +156 -0
  415. package/src/providers/alibaba-region-startup.ts +36 -0
  416. package/src/providers/antigravity-models.ts +317 -0
  417. package/src/providers/api-keys.ts +140 -0
  418. package/src/providers/base-url-choices.ts +64 -0
  419. package/src/providers/codex-capacity.ts +288 -0
  420. package/src/providers/command-code-efforts.ts +85 -0
  421. package/src/providers/context-cap.ts +73 -0
  422. package/src/providers/derive.ts +451 -0
  423. package/src/providers/free-directory.ts +187 -0
  424. package/src/providers/github-copilot-transport.ts +56 -0
  425. package/src/providers/google-vertex-location.ts +14 -0
  426. package/src/providers/key-failover.ts +271 -0
  427. package/src/providers/kiro-models.ts +67 -0
  428. package/src/providers/label.ts +19 -0
  429. package/src/providers/model-discovery-limits.ts +16 -0
  430. package/src/providers/model-discovery.ts +361 -0
  431. package/src/providers/openai-sidecar.ts +235 -0
  432. package/src/providers/openai-tier-startup.ts +27 -0
  433. package/src/providers/openai-tiers.ts +301 -0
  434. package/src/providers/openai-virtual-models.ts +83 -0
  435. package/src/providers/openrouter-routing.ts +102 -0
  436. package/src/providers/provider-id-rewrite.ts +179 -0
  437. package/src/providers/quota.ts +1942 -0
  438. package/src/providers/reasoning-capabilities.ts +336 -0
  439. package/src/providers/registry.ts +2375 -0
  440. package/src/providers/slug-codec.ts +74 -0
  441. package/src/providers/xai-transport.ts +149 -0
  442. package/src/reasoning-effort.ts +243 -0
  443. package/src/responses/compaction.ts +124 -0
  444. package/src/responses/hosted-tool-policy.ts +9 -0
  445. package/src/responses/parser.ts +714 -0
  446. package/src/responses/reasoning-envelope.ts +60 -0
  447. package/src/responses/reasoning-replay-cache.ts +106 -0
  448. package/src/responses/schema.ts +159 -0
  449. package/src/responses/spill-store.ts +431 -0
  450. package/src/responses/state.ts +1039 -0
  451. package/src/responses/tool-groups.ts +19 -0
  452. package/src/router.ts +802 -0
  453. package/src/routing/analytics.ts +377 -0
  454. package/src/routing/capability.ts +205 -0
  455. package/src/routing/cost.ts +77 -0
  456. package/src/routing/evaluator.ts +444 -0
  457. package/src/routing/health.ts +401 -0
  458. package/src/routing/history/cursor.ts +43 -0
  459. package/src/routing/history/indexer.ts +605 -0
  460. package/src/routing/history/schema.ts +72 -0
  461. package/src/routing/profile-namespace.ts +15 -0
  462. package/src/routing/profile.ts +424 -0
  463. package/src/routing/quota.ts +145 -0
  464. package/src/routing/request-evidence.ts +45 -0
  465. package/src/routing/trace.ts +686 -0
  466. package/src/server/adapter-resolve.ts +83 -0
  467. package/src/server/auth-cors.ts +606 -0
  468. package/src/server/chat-completions.ts +379 -0
  469. package/src/server/claude-messages.ts +980 -0
  470. package/src/server/effort-policy.ts +251 -0
  471. package/src/server/github-copilot-responses-repair.ts +338 -0
  472. package/src/server/gui-static.ts +152 -0
  473. package/src/server/image-retry.ts +42 -0
  474. package/src/server/images.ts +485 -0
  475. package/src/server/index.ts +1633 -0
  476. package/src/server/lifecycle.ts +482 -0
  477. package/src/server/live.ts +609 -0
  478. package/src/server/management/agent-settings-routes.ts +1180 -0
  479. package/src/server/management/android-remote-routes.ts +390 -0
  480. package/src/server/management/api-access.ts +141 -0
  481. package/src/server/management/api-key-usage.ts +167 -0
  482. package/src/server/management/body.ts +35 -0
  483. package/src/server/management/combo-routes.ts +244 -0
  484. package/src/server/management/config-routes.ts +602 -0
  485. package/src/server/management/context.ts +88 -0
  486. package/src/server/management/integration-routes.ts +538 -0
  487. package/src/server/management/logs-usage-routes.ts +516 -0
  488. package/src/server/management/model-routes.ts +519 -0
  489. package/src/server/management/model-rows.ts +143 -0
  490. package/src/server/management/native-integration-routes.ts +781 -0
  491. package/src/server/management/oauth-account-routes.ts +573 -0
  492. package/src/server/management/provider-routes.ts +781 -0
  493. package/src/server/management/request-history-routes.ts +191 -0
  494. package/src/server/management/routing-analytics-routes.ts +74 -0
  495. package/src/server/management/routing-profile-routes.ts +384 -0
  496. package/src/server/management/shared.ts +277 -0
  497. package/src/server/management/sidebar-routes.ts +106 -0
  498. package/src/server/management/sync-response.ts +69 -0
  499. package/src/server/management/system-restart.ts +433 -0
  500. package/src/server/management/system-routes.ts +141 -0
  501. package/src/server/management/usage-summary-cache.ts +86 -0
  502. package/src/server/management-api.ts +269 -0
  503. package/src/server/management-auth.ts +353 -0
  504. package/src/server/memory-watchdog.ts +156 -0
  505. package/src/server/port-reclaim.ts +307 -0
  506. package/src/server/ports.ts +156 -0
  507. package/src/server/proxy-liveness.ts +326 -0
  508. package/src/server/proxy-stop.ts +92 -0
  509. package/src/server/readiness.ts +99 -0
  510. package/src/server/relay-eager.ts +353 -0
  511. package/src/server/relay.ts +1179 -0
  512. package/src/server/request-decompress.ts +132 -0
  513. package/src/server/request-log-conversation.ts +168 -0
  514. package/src/server/request-log.ts +1072 -0
  515. package/src/server/responses/collaboration.ts +409 -0
  516. package/src/server/responses/compact.ts +710 -0
  517. package/src/server/responses/core.ts +3561 -0
  518. package/src/server/responses/encrypted-payload.ts +308 -0
  519. package/src/server/responses/fetch-helpers.ts +171 -0
  520. package/src/server/responses/passthrough-error.ts +78 -0
  521. package/src/server/responses/policy-fallback.ts +152 -0
  522. package/src/server/responses/terminal-guard.ts +230 -0
  523. package/src/server/responses/upstream-error.ts +48 -0
  524. package/src/server/responses-image-gen-repair.ts +132 -0
  525. package/src/server/responses-item-id-repair.ts +272 -0
  526. package/src/server/responses-json-events.ts +52 -0
  527. package/src/server/responses-model-rewrite.ts +29 -0
  528. package/src/server/responses-snapshot-repair.ts +621 -0
  529. package/src/server/responses.ts +10 -0
  530. package/src/server/search.ts +181 -0
  531. package/src/server/sse-frame-buffer.ts +292 -0
  532. package/src/server/sse-payload-rewrite.ts +263 -0
  533. package/src/server/startup-action-control.ts +308 -0
  534. package/src/server/startup-health-cache.ts +119 -0
  535. package/src/server/system-env.ts +418 -0
  536. package/src/server/windows-tcp-drop.ts +184 -0
  537. package/src/server/windows-tray-control.ts +41 -0
  538. package/src/server/ws-bridge.ts +470 -0
  539. package/src/service-manager-probe.ts +824 -0
  540. package/src/service.ts +3011 -0
  541. package/src/stall-timeout.ts +20 -0
  542. package/src/storage/cleanup-job.ts +57 -0
  543. package/src/storage/cleanup.ts +3085 -0
  544. package/src/storage/policy-job.ts +457 -0
  545. package/src/storage/policy-scheduler.ts +40 -0
  546. package/src/storage/policy-worker.ts +59 -0
  547. package/src/storage/policy.ts +527 -0
  548. package/src/storage/restore-job.ts +299 -0
  549. package/src/storage/restore-worker.ts +58 -0
  550. package/src/storage/scanner.ts +238 -0
  551. package/src/storage/storage-mutation-coordinator.ts +139 -0
  552. package/src/storage/worker-lifecycle.ts +215 -0
  553. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  554. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  555. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  556. package/src/tray/assets/opencodex-tray.png +0 -0
  557. package/src/tray/windows-tray.ps1 +364 -0
  558. package/src/tray/windows.ts +738 -0
  559. package/src/types.ts +1531 -0
  560. package/src/update/badge.ts +72 -0
  561. package/src/update/desktop-release.ts +1620 -0
  562. package/src/update/index.ts +402 -0
  563. package/src/update/job.ts +1906 -0
  564. package/src/update/notify.ts +261 -0
  565. package/src/update/npm-cache-preflight.d.mts +47 -0
  566. package/src/update/npm-cache-preflight.mjs +201 -0
  567. package/src/update/npm-invocation.d.mts +23 -0
  568. package/src/update/npm-invocation.mjs +94 -0
  569. package/src/update/tray-update-plan.d.mts +18 -0
  570. package/src/update/tray-update-plan.mjs +38 -0
  571. package/src/usage/cost.ts +0 -0
  572. package/src/usage/debug.ts +97 -0
  573. package/src/usage/expected-prices.ts +283 -0
  574. package/src/usage/log.ts +695 -0
  575. package/src/usage/summary.ts +585 -0
  576. package/src/usage/totals.ts +14 -0
  577. package/src/vision/anthropic-describe.ts +185 -0
  578. package/src/vision/describe.ts +127 -0
  579. package/src/vision/index.ts +558 -0
  580. package/src/vision/reasoning.ts +55 -0
  581. package/src/web-search/anthropic-executor.ts +189 -0
  582. package/src/web-search/executor.ts +105 -0
  583. package/src/web-search/format-result.ts +89 -0
  584. package/src/web-search/index.ts +196 -0
  585. package/src/web-search/loop.ts +791 -0
  586. package/src/web-search/parse.ts +235 -0
  587. package/src/web-search/progress-stream.ts +342 -0
  588. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,3561 @@
1
+ import type { Server } from "bun";
2
+ import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
3
+ import { formatPassthroughUpstreamError } from "./passthrough-error";
4
+ import { describeUpstreamConnectFailure } from "./upstream-error";
5
+ import {
6
+ getConfigPath,
7
+ multiAgentGuidanceEnabled,
8
+ resolveEnvValue,
9
+ } from "../../config";
10
+ import { parseRequest } from "../../responses/parser";
11
+ import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
12
+ import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
13
+ import {
14
+ expandPreviousResponseInput,
15
+ previousResponseProviderState,
16
+ previousResponseReplayFailure,
17
+ rememberResponseState,
18
+ } from "../../responses/state";
19
+ import { comboRouteDecisionTrace, NoEligiblePolicyCandidateError, routeModel, type RouteResult } from "../../router";
20
+ import { evidenceFromBody } from "../../routing/request-evidence";
21
+ import {
22
+ advanceComboAfterFailure,
23
+ comboDefaultEffort,
24
+ comboFailureDecision,
25
+ comboIdFromRawBody,
26
+ concreteComboRequestBody,
27
+ getCombo,
28
+ isComboTargetInCooldown,
29
+ NoAvailableComboTargetsError,
30
+ noteComboSuccess,
31
+ parseRetryAfterMs,
32
+ pickComboTarget,
33
+ targetKey,
34
+ } from "../../combos";
35
+ import { isInjectionDebugEnabled } from "../../lib/debug-settings";
36
+ import { injectionDebugLog } from "../../lib/injection-debug-log";
37
+ import { resolveClientRetryAfter } from "../../lib/retry-after";
38
+ import { modelInList, namespacedToolName } from "../../types";
39
+ import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
40
+ import {
41
+ forceRefreshOAuthAccessSnapshot,
42
+ getOAuthCredentialApiBaseUrl,
43
+ getValidAccessTokenForAccount,
44
+ getValidAccessTokenSnapshot,
45
+ type OAuthAccessSnapshot,
46
+ UnsupportedOAuthProviderError,
47
+ } from "../../oauth";
48
+ import {
49
+ ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
50
+ anthropicSessionKeyFromParts,
51
+ bindAnthropicSessionAffinity,
52
+ formatAnthropicProviderForLog,
53
+ getAnthropicPoolAccessToken,
54
+ getAnthropicPoolRetryAfterSeconds,
55
+ isAnthropicAccountPoolEnabled,
56
+ promoteAnthropicActiveAccount,
57
+ resolveAnthropicAccountForSession,
58
+ rotateAnthropicAccountOn429,
59
+ } from "../../oauth/anthropic-routing";
60
+ import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
61
+ import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
62
+ import { describeImagesInPlace, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
63
+ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
64
+ import {
65
+ applyCodexAuthContextToProvider,
66
+ CodexAccountCooldownError,
67
+ codexMainProfileDrainingResponse,
68
+ cooldownErrorResponse,
69
+ CodexAuthContextError,
70
+ CodexDirectAuthenticationError,
71
+ CodexMainProfileDrainingError,
72
+ CodexPoolAuthenticationError,
73
+ CodexThreadAffinityExpiredError,
74
+ headersForCodexAuthContext,
75
+ isCodexAuthContextUsable,
76
+ resolveCodexAuthContext,
77
+ codexProbeLeaseId,
78
+ codexProbeQuotaScope,
79
+ releaseCodexAuthContextProbeLease,
80
+ stripCodexRuntimeProviderFields,
81
+ type CodexAuthContext,
82
+ } from "../../codex/auth-context";
83
+ import {
84
+ computeQuotaCooldown,
85
+ formatCodexProviderForLog,
86
+ previewCodexAccountForRequest,
87
+ recordCodexUpstreamOutcome,
88
+ type CodexUpstreamOutcome,
89
+ } from "../../codex/routing";
90
+ import {
91
+ applyUpstreamRecoveryInit,
92
+ fetchWithResetRetry,
93
+ fetchWithTransientNonJsonRetry,
94
+ fetchWithTransientRetry,
95
+ prepareSameTarget429Wait,
96
+ } from "../../lib/upstream-retry";
97
+ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
98
+ import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
99
+ import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
100
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
101
+ import { slugsEquivalent } from "../../providers/slug-codec";
102
+ import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
103
+ import { isUsageDebugEnabled } from "../../usage/debug";
104
+ import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
105
+ import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
106
+ import {
107
+ providerModelResponsesUpstreamStreaming,
108
+ providerPreStreamNonJsonRetryStatuses,
109
+ type InboundWire,
110
+ } from "../../providers/registry";
111
+ import type { AdapterRequest } from "../../adapters/base";
112
+ import {
113
+ hasKeyPoolFailover,
114
+ rateLimitRetryDelayMs,
115
+ rateLimitRetryPolicyFor,
116
+ rotateProviderTransportOn429,
117
+ } from "../../providers/key-failover";
118
+ import { shouldAttemptImageTierRetry } from "../image-retry";
119
+ import { resolveProviderTransport } from "../../providers/xai-transport";
120
+ import type { WsData } from "../ws-bridge";
121
+ import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
122
+ import { redactSecretString } from "../../lib/redact";
123
+ import { readBoundedResponseBody } from "../../lib/bounded-body";
124
+ import type { AdmissionLease } from "../../lib/admission";
125
+ import { applyReasoningCapability, isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
126
+ import {
127
+ applySubagentModelFallback,
128
+ maybePrimeSubagentQuota,
129
+ recordSubagentQuotaFailureForThreadSpawn,
130
+ } from "../../codex/subagent-model-fallback";
131
+ import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
132
+ import {
133
+ beginRequestAttempt,
134
+ catalogModelSupportsServiceTier,
135
+ finishRequestAttempt,
136
+ inspectResponseLogJson,
137
+ noteAttemptSend,
138
+ readConfiguredCodexServiceTier,
139
+ recordAdapterReasoning,
140
+ recordAttemptRequestedEffort,
141
+ requestLogSpeedLabel,
142
+ sealRequestAttemptIdentity,
143
+ usageFromResponsesPayload,
144
+ type RequestLogContext,
145
+ } from "../request-log";
146
+ import {
147
+ conversationIdFromResponsesRequest,
148
+ normalizeLogConversationId,
149
+ sessionIdHeaderFromRequest,
150
+ } from "../request-log-conversation";
151
+ import type { AttemptRecoveryKind } from "../../usage/log";
152
+ import {
153
+ consumeForInspection,
154
+ consumeForResponseLogMetadata,
155
+ createSseInspector,
156
+ isEagerRelaySseResponse,
157
+ isNativePassthroughSseResponse,
158
+ markEagerRelaySseResponse,
159
+ markNativePassthroughSseResponse,
160
+ relaySseWithFailedTail,
161
+ relayWithAbort,
162
+ sanitizePassthroughHeaders,
163
+ } from "../relay";
164
+ import { relaySseEagerBounded } from "../relay-eager";
165
+ import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
166
+ import { cancelBodyOnAbort } from "../../lib/abort";
167
+ import {
168
+ createResponsesItemIdPayloadRewrite,
169
+ hasResponsesItemIdRepair,
170
+ repairResponsesJsonItemIds,
171
+ } from "../responses-item-id-repair";
172
+ import {
173
+ createImageGenCallRestoreRewrite,
174
+ imageGenToolCallAliases,
175
+ restoreImageGenCallsInJson,
176
+ } from "../responses-image-gen-repair";
177
+ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
178
+ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
179
+
180
+ import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
181
+ import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
182
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
183
+ import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
184
+ import {
185
+ acquireUpstreamHostAdmission,
186
+ disableUpstreamHostCircuitForKey,
187
+ normalizeUpstreamHostCircuitThreshold,
188
+ recordUpstreamHostFailure,
189
+ releaseUpstreamHostAdmission,
190
+ resetUpstreamHostHealth,
191
+ upstreamHostHealthKey,
192
+ type UpstreamHostAdmissionLease,
193
+ } from "../../codex/upstream-host-health";
194
+ import {
195
+ createResponsesSnapshotBlockRewrite,
196
+ hasResponsesSnapshotRepair,
197
+ repairResponsesSnapshotJson,
198
+ } from "../responses-snapshot-repair";
199
+ import {
200
+ composeSseBlockRewrites,
201
+ composeSsePayloadRewrites,
202
+ payloadRewriteAsBlockRewrite,
203
+ relaySseWithBlockRewrite,
204
+ } from "../sse-payload-rewrite";
205
+ import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
206
+ import { responsesJsonToSseBody } from "../responses-json-events";
207
+ import { guardTerminalEventStream } from "./terminal-guard";
208
+
209
+ /**
210
+ * Adapters whose continuation state must survive Codex's store:false requests.
211
+ */
212
+ export function adapterNeedsForcedContinuation(name: string): boolean {
213
+ return name === "kiro" || name === "cursor";
214
+ }
215
+
216
+ export function sidecarOutcomeRecorder(
217
+ config: OcxConfig,
218
+ authCtx: CodexAuthContext,
219
+ threadId?: string | null,
220
+ ): ((outcome: CodexUpstreamOutcome) => void) | undefined {
221
+ return authCtx.kind === "pool" || authCtx.kind === "main-pool"
222
+ ? outcome => recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
223
+ threadId,
224
+ fixedAccount: authCtx.fixedAccount,
225
+ probeLeaseId: authCtx.probeLeaseId,
226
+ probeQuotaScope: authCtx.probeQuotaScope,
227
+ writerGeneration: authCtx.writerGeneration,
228
+ })
229
+ : undefined;
230
+ }
231
+
232
+
233
+
234
+ import { isShadowSourceModel, shouldInterceptShadowCall } from "../../lib/shadow-call";
235
+
236
+ export { DEFAULT_SHADOW_SOURCE_MODELS, isShadowSourceModel, shadowSourceModels } from "../../lib/shadow-call";
237
+
238
+
239
+
240
+ export function codexLogAccountId(authCtx: CodexAuthContext): string | null {
241
+ return authCtx.kind === "pool" || authCtx.kind === "main-pool" ? authCtx.accountId : null;
242
+ }
243
+
244
+ function isFixedCodexAccount(authCtx: CodexAuthContext): boolean {
245
+ return (authCtx.kind === "pool" || authCtx.kind === "main-pool")
246
+ && authCtx.fixedAccount === true;
247
+ }
248
+
249
+
250
+
251
+ export function usesCodexForwardPoolAuth(
252
+ authCtx: CodexAuthContext,
253
+ provider: OcxProviderConfig,
254
+ ): authCtx is Extract<CodexAuthContext, { kind: "pool" | "main-pool" }> {
255
+ return (authCtx.kind === "pool" || authCtx.kind === "main-pool")
256
+ && provider.authMode === "forward" && provider.adapter === "openai-responses";
257
+ }
258
+
259
+ export function preAuthUpstreamHostCircuitKey(
260
+ route: Pick<RouteResult, "provider" | "providerName" | "codexAccountMode" | "codexAccountId">,
261
+ config: OcxConfig,
262
+ options: { requireResponsesAdapter?: boolean } = {},
263
+ ): string | null {
264
+ if (
265
+ normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) === 0
266
+ || route.codexAccountMode !== "pool"
267
+ || route.codexAccountId !== undefined
268
+ || route.provider.authMode !== "forward"
269
+ || (options.requireResponsesAdapter !== false && route.provider.adapter !== "openai-responses")
270
+ ) return null;
271
+ return upstreamHostHealthKey(route.providerName, safeOriginLabel(route.provider.baseUrl ?? ""));
272
+ }
273
+
274
+ export function upstreamHostCircuitOpenResponse(retryAfterSeconds: number): Response {
275
+ return formatErrorResponse(
276
+ 503,
277
+ "upstream_host_circuit_open",
278
+ "Provider host is temporarily unavailable",
279
+ { retryAfter: String(retryAfterSeconds) },
280
+ );
281
+ }
282
+
283
+ function normalizeCodexUnsupportedModelDetail(value: string): string {
284
+ return value.trim().replace(/\s+/gu, " ").toLocaleLowerCase("en-US");
285
+ }
286
+
287
+ function isAllowListedCodexAccountModel400(
288
+ status: number,
289
+ bodyText: string,
290
+ modelId: string,
291
+ ): boolean {
292
+ if (status !== 400) return false;
293
+ try {
294
+ const payload = JSON.parse(bodyText) as unknown;
295
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
296
+ const detail = (payload as { detail?: unknown }).detail;
297
+ if (typeof detail !== "string") return false;
298
+ const expected = `The '${modelId}' model is not supported when using Codex with a ChatGPT account.`;
299
+ return normalizeCodexUnsupportedModelDetail(detail)
300
+ === normalizeCodexUnsupportedModelDetail(expected);
301
+ } catch {
302
+ return false;
303
+ }
304
+ }
305
+
306
+ async function shouldRetryCodexPoolAccountModel400(
307
+ response: Response,
308
+ modelId: string,
309
+ signal?: AbortSignal,
310
+ ): Promise<boolean> {
311
+ if (response.status !== 400) return false;
312
+ try {
313
+ const body = await readBoundedResponseBody(response.clone(), { signal });
314
+ return body.displaySafe
315
+ && !body.truncated
316
+ && isAllowListedCodexAccountModel400(response.status, body.text, modelId);
317
+ } catch {
318
+ return false;
319
+ }
320
+ }
321
+
322
+ /** Pre-stream quota/billing rejections that warrant one alternate-account attempt (#584). */
323
+ export function shouldRetryCodexPoolAccountQuota(response: Response): boolean {
324
+ return response.status === 402 || response.status === 429;
325
+ }
326
+
327
+ interface CodexPoolAccountRetryArgs {
328
+ req: Request;
329
+ config: OcxConfig;
330
+ route: { providerName: string; modelId: string; provider: OcxProviderConfig };
331
+ parsed: OcxParsedRequest;
332
+ logCtx: RequestLogContext;
333
+ options: {
334
+ abortSignal?: AbortSignal;
335
+ onCodexAuthContextResolved?: (ctx: CodexAuthContext) => void;
336
+ deferCodexResetDerivedCooldown?: boolean;
337
+ // Narrowed subset of HandleResponsesOptions: the retry rebuilds the adapter, so it
338
+ // needs the inbound scope or the retry could land on a different wire than the
339
+ // first attempt.
340
+ inboundWire?: InboundWire;
341
+ translatorBudget: TranslatorBudget;
342
+ turnAdmissionLease?: AdmissionLease;
343
+ };
344
+ firstAuthCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
345
+ firstResponse: Response;
346
+ outcomeStatus: number;
347
+ upstream: AbortController;
348
+ connectMs: number;
349
+ passthroughEstimate?: number;
350
+ stream: boolean;
351
+ }
352
+
353
+ type CodexPoolAccountRetryResult =
354
+ | {
355
+ kind: "retried";
356
+ authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
357
+ request: Awaited<ReturnType<ReturnType<typeof resolveAdapter>["buildRequest"]>>;
358
+ upstreamResponse: Response;
359
+ selectedForwardHeaders: Headers;
360
+ }
361
+ | { kind: "no-alternate" }
362
+ | {
363
+ kind: "transport";
364
+ error: unknown;
365
+ authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
366
+ };
367
+
368
+ function codexQuotaOutcomeMeta(response: Response): {
369
+ retryAfter: string | null;
370
+ resetAt: string[];
371
+ } {
372
+ return {
373
+ retryAfter: response.headers.get("retry-after"),
374
+ resetAt: [
375
+ response.headers.get("x-codex-primary-reset-at"),
376
+ response.headers.get("x-codex-secondary-reset-at"),
377
+ response.headers.get("x-codex-tertiary-reset-at"),
378
+ ].filter((value): value is string => !!value),
379
+ };
380
+ }
381
+
382
+ /**
383
+ * A reset timestamp describes a quota window, not an explicit instruction to
384
+ * stop using the whole account. A combo may therefore try a later model in the
385
+ * same request, while Retry-After and headerless quota failures remain blocking.
386
+ */
387
+ function shouldDeferCodexResetDerivedCooldown(response: Response, enabled?: boolean): boolean {
388
+ return enabled === true
389
+ && (response.status === 429 || response.status === 402)
390
+ && computeQuotaCooldown(codexQuotaOutcomeMeta(response)).source === "reset-derived";
391
+ }
392
+
393
+ /**
394
+ * One bounded alternate-account retry for Codex pool auth. Used for allow-listed
395
+ * model-400 and for pre-stream 429/402 quota failures (#584).
396
+ */
397
+ async function retryCodexPoolOnAlternateAccount(
398
+ args: CodexPoolAccountRetryArgs,
399
+ ): Promise<CodexPoolAccountRetryResult> {
400
+ const {
401
+ req, config, route, parsed, logCtx, options, firstAuthCtx, firstResponse,
402
+ outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
403
+ } = args;
404
+ // Defense in depth: exact account selectors must never reach alternate-account resolution,
405
+ // even if a future caller forgets to guard this helper.
406
+ if (firstAuthCtx.fixedAccount) return { kind: "no-alternate" };
407
+ const inboundWire = options.inboundWire ?? "responses";
408
+ let retryAuthCtx: CodexAuthContext | undefined;
409
+ try {
410
+ retryAuthCtx = await resolveCodexAuthContext(
411
+ req.headers,
412
+ config,
413
+ "pool",
414
+ {
415
+ excludeAccountId: firstAuthCtx.accountId,
416
+ modelId: route.modelId,
417
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
418
+ },
419
+ );
420
+ } catch (error) {
421
+ if (
422
+ !(error instanceof CodexPoolAuthenticationError)
423
+ && !(error instanceof CodexAuthContextError)
424
+ && !(error instanceof CodexAccountCooldownError)
425
+ && !(error instanceof CodexMainProfileDrainingError)
426
+ ) throw error;
427
+ }
428
+ if (retryAuthCtx?.kind !== "pool" && retryAuthCtx?.kind !== "main-pool") {
429
+ return { kind: "no-alternate" };
430
+ }
431
+
432
+ const quotaMeta = codexQuotaOutcomeMeta(firstResponse);
433
+ if (outcomeStatus === 429 || outcomeStatus === 402) {
434
+ const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
435
+ applyAccountQuotaFromUpstreamHeaders(
436
+ firstAuthCtx.accountId,
437
+ firstResponse.headers,
438
+ firstAuthCtx.writerGeneration,
439
+ );
440
+ }
441
+ if (!shouldDeferCodexResetDerivedCooldown(firstResponse, options.deferCodexResetDerivedCooldown)) {
442
+ recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
443
+ ...quotaMeta,
444
+ threadId: req.headers.get("x-codex-parent-thread-id"),
445
+ modelId: route.modelId,
446
+ probeLeaseId: codexProbeLeaseId(firstAuthCtx),
447
+ probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
448
+ writerGeneration: firstAuthCtx.writerGeneration,
449
+ // Retry already advanced the RR ring via excludeAccountId — reuse for promotion.
450
+ ...(retryAuthCtx.accountId ? { promoteAccountId: retryAuthCtx.accountId } : {}),
451
+ });
452
+ }
453
+
454
+ const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx);
455
+ const retryProvider = applyCodexAuthContextToProvider(
456
+ stripCodexRuntimeProviderFields(route.provider),
457
+ retryAuthCtx,
458
+ "pool",
459
+ );
460
+ const retryAdapter = resolveAdapter(
461
+ resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
462
+ config.cacheRetention,
463
+ );
464
+ const request = await retryAdapter.buildRequest(parsed, {
465
+ headers: retryHeaders,
466
+ translatorBudget: options.translatorBudget,
467
+ });
468
+ recordAdapterReasoning(logCtx, request);
469
+
470
+ await firstResponse.body?.cancel().catch(() => undefined);
471
+ options.onCodexAuthContextResolved?.(retryAuthCtx);
472
+ route.provider = retryProvider;
473
+ logCtx.provider = formatCodexProviderForLog(
474
+ route.providerName,
475
+ retryAuthCtx.accountId,
476
+ config,
477
+ );
478
+
479
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate);
480
+ try {
481
+ const upstreamResponse = await fetchWithHeaderTimeout(
482
+ request.url,
483
+ {
484
+ method: request.method,
485
+ headers: request.headers,
486
+ body: request.body,
487
+ },
488
+ upstream.signal,
489
+ connectMs,
490
+ stream,
491
+ providerFetch(route.provider),
492
+ // Credential-bearing forward send: never follow a redirect into a
493
+ // dead-host rejection after the credential was seen (#914).
494
+ route.provider.authMode === "forward",
495
+ );
496
+ // A real HTTP response proves the host was reached (#914).
497
+ const retryHostKey = upstreamHostHealthKey(route.providerName, safeOriginLabel(request.url));
498
+ if (normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0) {
499
+ resetUpstreamHostHealth(retryHostKey, null);
500
+ } else {
501
+ resetUpstreamHostHealth(retryHostKey);
502
+ }
503
+ return {
504
+ kind: "retried",
505
+ authCtx: retryAuthCtx,
506
+ request,
507
+ upstreamResponse,
508
+ selectedForwardHeaders: retryHeaders,
509
+ };
510
+ } catch (error) {
511
+ // Attribute the transport failure to the alternate account (already selected).
512
+ return { kind: "transport", error, authCtx: retryAuthCtx };
513
+ } finally {
514
+ request.releaseBodyObservation?.();
515
+ }
516
+ }
517
+
518
+
519
+
520
+ export function codexForwardTerminalOutcomeRecorder(
521
+ config: OcxConfig,
522
+ authCtx: CodexAuthContext,
523
+ provider: OcxProviderConfig,
524
+ modelId?: string,
525
+ logCtx?: RequestLogContext,
526
+ threadId?: string | null,
527
+ ): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
528
+ if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
529
+ return (status, httpStatusOverride) => {
530
+ if (status === "incomplete") {
531
+ // Normal limit/content-filter/stall terminal — the account served the
532
+ // request. Don't penalize account health; record success to clear any
533
+ // prior soft-avoid so a healthy account isn't stuck avoided.
534
+ recordCodexUpstreamOutcome(config, authCtx.accountId, 200, {
535
+ threadId,
536
+ fixedAccount: authCtx.fixedAccount,
537
+ modelId,
538
+ probeLeaseId: codexProbeLeaseId(authCtx),
539
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
540
+ writerGeneration: authCtx.writerGeneration,
541
+ });
542
+ return;
543
+ }
544
+ // status === "completed" or "failed": use the semantic HTTP status derived
545
+ // from the terminal SSE error payload (httpStatusFromTerminalError in
546
+ // request-log inspection) instead of collapsing every non-completed terminal
547
+ // to 502. A 400 invalid_request_error must not soft-avoid the account or
548
+ // rebind threads — only genuine transport/5xx failures should trigger
549
+ // transient health recording.
550
+ // httpStatusOverride: the combo WS path inspects SSE payloads into the parent
551
+ // logCtx, but this recorder closes over the child logCtx. The caller passes
552
+ // the parent's terminalHttpStatus so the semantic status is not lost.
553
+ const outcome = status === "completed"
554
+ ? 200
555
+ : (httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
556
+ recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
557
+ threadId,
558
+ fixedAccount: authCtx.fixedAccount,
559
+ modelId,
560
+ probeLeaseId: codexProbeLeaseId(authCtx),
561
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
562
+ writerGeneration: authCtx.writerGeneration,
563
+ });
564
+ };
565
+ }
566
+
567
+
568
+
569
+ export function decodeRequestErrorResponse(err: unknown, label: string): Response {
570
+ if (isTranslatorBudgetExceededError(err)) {
571
+ return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
572
+ code: "translation_buffer_limit",
573
+ });
574
+ }
575
+ if (err instanceof UnsupportedContentEncodingError) {
576
+ return formatErrorResponse(415, "invalid_request_error", err.message);
577
+ }
578
+ if (err instanceof DecompressedBodyTooLargeError) {
579
+ return formatErrorResponse(413, "invalid_request_error", err.message);
580
+ }
581
+ console.warn(`[${label}] request body decode/parse failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
582
+ return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
583
+ }
584
+
585
+
586
+
587
+ export function comboUnavailableResponse(message: string): Response {
588
+ return new Response(
589
+ JSON.stringify({
590
+ error: { message, type: "server_error", code: "combo_unavailable" },
591
+ }),
592
+ { status: 503, headers: { "Content-Type": "application/json" } },
593
+ );
594
+ }
595
+
596
+
597
+
598
+ export interface ConsumedComboFailure {
599
+ response: Response;
600
+ classificationText: string;
601
+ /** Structured upstream `error.code` when present in the failure body. */
602
+ upstreamCode?: string;
603
+ /** Valid numeric/date value used only for cooldown calculation. */
604
+ retryAfter?: string;
605
+ /** Reserved for 040 usage attribution without adding another body read. */
606
+ usage?: OcxUsage;
607
+ }
608
+
609
+
610
+
611
+ export interface HandleResponsesOptions {
612
+ turnAdmissionLease?: AdmissionLease;
613
+ forceEmptyResponseId?: boolean;
614
+ abortSignal?: AbortSignal;
615
+ /** One-shot TTFT callback: first non-empty model output observed (WP4). */
616
+ onFirstOutput?: () => void;
617
+ onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
618
+ recordTerminalOutcomes?: boolean;
619
+ setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
620
+ onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
621
+ onNativePassthroughCancel?: () => void;
622
+ /**
623
+ * When true, body `prompt_cache_key` is a Claude Desktop shared cache cohort
624
+ * (system/tools hash), not a per-session id — do not use it for Anthropic pool affinity.
625
+ */
626
+ promptCacheKeyIsSharedCohort?: boolean;
627
+ /**
628
+ * Wire protocol the ORIGINAL client spoke. The Chat and Anthropic surfaces translate
629
+ * their body into a Responses shape and replay through this function, so without an
630
+ * explicit value the replay would look like a native Responses request and an
631
+ * inbound-scoped registry wire default would fire for a client that never asked for
632
+ * it. Omitted means a genuine Responses inbound.
633
+ */
634
+ inboundWire?: InboundWire;
635
+ /** Internal transport identity for route-scoped upstream compatibility policy. */
636
+ inboundTransport?: "websocket";
637
+ /** Internal recursion guard; callers outside this module must not set it. */
638
+ comboAttempt?: boolean;
639
+ /** Internal combo handoff: allow a later same-provider model after a reset-derived 429/402. */
640
+ deferCodexResetDerivedCooldown?: boolean;
641
+ /** 030-owned handoff when a child consumed the original failure under bounds. */
642
+ onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
643
+ /** Caller-owned for Chat/Claude replay; omitted only at genuine Responses ingress. */
644
+ translatorBudget?: TranslatorBudget;
645
+ }
646
+
647
+
648
+
649
+ /**
650
+ * Build the 499 JSON error the proxy returns when the client disconnects before the
651
+ * response completes (`client_cancelled`).
652
+ */
653
+ export function clientCancelledResponse(): Response {
654
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled request");
655
+ }
656
+
657
+
658
+
659
+ export function sanitizedRetryAfter(value: string | null, now: number): string | undefined {
660
+ const trimmed = value?.trim();
661
+ if (!trimmed || trimmed.length > 128) return undefined;
662
+ return parseRetryAfterMs(trimmed, now) !== undefined ? trimmed : undefined;
663
+ }
664
+
665
+
666
+
667
+ export async function consumeComboFailure(
668
+ response: Response,
669
+ signal?: AbortSignal,
670
+ now = Date.now(),
671
+ ): Promise<ConsumedComboFailure> {
672
+ const fallback = `Provider error ${response.status}`;
673
+ let classificationText = fallback;
674
+ let usage: OcxUsage | undefined;
675
+ let upstreamCode: string | undefined;
676
+ try {
677
+ const body = await readBoundedResponseBody(response, { signal });
678
+ usage = usageFromComboFailureText(body.text);
679
+ if (body.displaySafe) {
680
+ const safeText = redactSecretString(body.text).slice(0, 500);
681
+ if (safeText) classificationText = safeText;
682
+ try {
683
+ const parsed = JSON.parse(body.text) as { error?: { code?: unknown } | string };
684
+ const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.code : undefined;
685
+ if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
686
+ } catch {
687
+ /* non-JSON upstream body — message-only classification */
688
+ }
689
+ }
690
+ } catch (error) {
691
+ if (signal?.aborted) throw error;
692
+ classificationText = fallback;
693
+ }
694
+ const message = classificationText === fallback
695
+ ? fallback
696
+ : `${fallback}: ${classificationText}`;
697
+ const upstreamRetryAfter = response.headers.get("retry-after");
698
+ // Client response may get the synthetic "2" fallback; cooldown metadata must not —
699
+ // otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
700
+ const clientRetryAfter = resolveClientRetryAfter({
701
+ status: response.status,
702
+ message,
703
+ upstreamRetryAfter,
704
+ now,
705
+ });
706
+ const cooldownRetryAfter = resolveClientRetryAfter({
707
+ status: response.status,
708
+ message,
709
+ upstreamRetryAfter,
710
+ now,
711
+ includeDefault: false,
712
+ });
713
+ return {
714
+ response: formatErrorResponse(response.status, "upstream_error", message, {
715
+ ...(upstreamCode !== undefined ? { code: upstreamCode } : {}),
716
+ ...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
717
+ }),
718
+ classificationText,
719
+ ...(upstreamCode !== undefined ? { upstreamCode } : {}),
720
+ ...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
721
+ ...(usage ? { usage } : {}),
722
+ };
723
+ }
724
+
725
+
726
+
727
+ export function usageFromComboFailureText(text: string): OcxUsage | undefined {
728
+ try {
729
+ const payload = JSON.parse(text) as Record<string, unknown>;
730
+ const nested = payload.response;
731
+ const source = nested && typeof nested === "object" && !Array.isArray(nested)
732
+ ? nested as Record<string, unknown>
733
+ : payload;
734
+ return usageFromResponsesPayload(source.usage);
735
+ } catch {
736
+ return undefined;
737
+ }
738
+ }
739
+
740
+
741
+
742
+ export function createChildPassthroughCallbackGate(options: HandleResponsesOptions) {
743
+ type Pending =
744
+ | { kind: "terminal"; status: ResponsesTerminalStatus }
745
+ | { kind: "cancel" };
746
+ let state: "pending" | "committed" | "discarded" = "pending";
747
+ let pending: Pending | undefined;
748
+ let accepted = false;
749
+ const publish = (value: Pending): void => {
750
+ if (value.kind === "terminal") options.onNativePassthroughTerminal?.(value.status);
751
+ else options.onNativePassthroughCancel?.();
752
+ };
753
+ const receive = (value: Pending): void => {
754
+ if (state === "discarded" || accepted) return;
755
+ accepted = true;
756
+ if (state === "committed") return publish(value);
757
+ pending ??= value;
758
+ };
759
+ return {
760
+ onTerminal: (status: ResponsesTerminalStatus) => receive({ kind: "terminal", status }),
761
+ onCancel: () => receive({ kind: "cancel" }),
762
+ commit: () => {
763
+ if (state !== "pending") return;
764
+ state = "committed";
765
+ if (pending) publish(pending);
766
+ pending = undefined;
767
+ },
768
+ discard: () => {
769
+ state = "discarded";
770
+ pending = undefined;
771
+ },
772
+ };
773
+ }
774
+
775
+
776
+
777
+ export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
778
+ const childHeaders = new Headers(parentHeaders);
779
+ // Combo children re-serialize already-decoded JSON. Keeping transport metadata from
780
+ // the parent would make the child decoder treat plain JSON as compressed bytes.
781
+ childHeaders.delete("content-length");
782
+ childHeaders.delete("content-encoding");
783
+ return childHeaders;
784
+ }
785
+
786
+ const UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE =
787
+ "Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.";
788
+
789
+ // Whole-body policy for non-streaming upstream JSON responses (see the application/json
790
+ // branch of the passthrough return path). 32 MiB matches the continuation snapshot read
791
+ // bound and is far above any legitimate non-streaming completion, including base64 image
792
+ // payloads. The stall deadlines only govern the body transfer — generation time before
793
+ // the response headers is untouched. Generation after early/chunked headers but before
794
+ // the first body byte previously used the 30-second inactivity deadline; this call site
795
+ // gives it the full body deadline instead.
796
+ const MAX_UPSTREAM_JSON_BODY_BYTES = 32 * 1024 * 1024;
797
+ const UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS = 180_000;
798
+ const UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS = 30_000;
799
+ export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
800
+ maxBytes: MAX_UPSTREAM_JSON_BODY_BYTES,
801
+ totalTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
802
+ inactivityTimeoutMs: UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS,
803
+ firstByteTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
804
+ };
805
+
806
+ function unreadableEncryptedAgentTaskResponse(): Response {
807
+ return new Response(
808
+ JSON.stringify({
809
+ error: {
810
+ message: UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE,
811
+ type: "invalid_request_error",
812
+ code: "unreadable_encrypted_agent_task",
813
+ },
814
+ }),
815
+ { status: 400, headers: { "Content-Type": "application/json" } },
816
+ );
817
+ }
818
+
819
+ type ResponsesAuthResolution =
820
+ | { ok: true; authCtx: CodexAuthContext; headers: Headers }
821
+ | { ok: false; response: Response };
822
+
823
+ /**
824
+ * Resolve Codex auth for a route. On unusable contexts, releases any probe lease
825
+ * before returning the 401 (nothing reaches upstream).
826
+ */
827
+ async function resolveResponsesCodexAuth(
828
+ req: Request,
829
+ config: OcxConfig,
830
+ route: RouteResult,
831
+ options: HandleResponsesOptions,
832
+ ): Promise<ResponsesAuthResolution> {
833
+ try {
834
+ if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
835
+ let authCtx: CodexAuthContext;
836
+ if (route.codexAccountMode) {
837
+ authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
838
+ accountId: route.codexAccountId,
839
+ modelId: route.modelId,
840
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
841
+ });
842
+ options.onCodexAuthContextResolved?.(authCtx);
843
+ } else {
844
+ authCtx = { kind: "main", accountId: null };
845
+ options.onCodexAuthContextResolved?.(undefined);
846
+ }
847
+ if (!isCodexAuthContextUsable(authCtx, config)) {
848
+ releaseCodexAuthContextProbeLease(authCtx);
849
+ return {
850
+ ok: false,
851
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
852
+ };
853
+ }
854
+ return {
855
+ ok: true,
856
+ authCtx,
857
+ headers: headersForCodexAuthContext(req.headers, authCtx),
858
+ };
859
+ } catch (err) {
860
+ if (err instanceof CodexAccountCooldownError) {
861
+ return { ok: false, response: cooldownErrorResponse(err, Date.now(), route.codexAccountNamespace) };
862
+ }
863
+ if (err instanceof CodexMainProfileDrainingError) {
864
+ return { ok: false, response: codexMainProfileDrainingResponse() };
865
+ }
866
+ if (err instanceof CodexThreadAffinityExpiredError) {
867
+ return {
868
+ ok: false,
869
+ response: formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"),
870
+ };
871
+ }
872
+ if (err instanceof CodexAuthContextError) {
873
+ const safeAccountLabel = route.codexAccountNamespace
874
+ ? `${route.providerName}-${route.codexAccountNamespace}`
875
+ : formatCodexProviderForLog(route.providerName, err.accountId, config);
876
+ console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
877
+ return {
878
+ ok: false,
879
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
880
+ };
881
+ }
882
+ if (err instanceof CodexPoolAuthenticationError) {
883
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
884
+ }
885
+ if (err instanceof CodexDirectAuthenticationError) {
886
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
887
+ }
888
+ if (err instanceof ForwardAdmissionCredentialError) {
889
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
890
+ }
891
+ throw err;
892
+ }
893
+ }
894
+
895
+ /**
896
+ * Apply every route-dependent request mutation against the final selected route.
897
+ * Must run only after subagent fallback has settled the model/provider.
898
+ */
899
+ async function applyFinalRouteRequestNormalization(args: {
900
+ parsed: OcxParsedRequest;
901
+ route: RouteResult;
902
+ config: OcxConfig;
903
+ req: Request;
904
+ logCtx: RequestLogContext;
905
+ inboundWire: InboundWire;
906
+ inboundTransport?: "websocket";
907
+ }): Promise<void> {
908
+ const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
909
+
910
+ // Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
911
+ // their existing response.model contract even when their public and wire model ids differ.
912
+ const responseModelId = parsed.modelId;
913
+ const preserveAnthropicResponseModel = route.providerName === "anthropic"
914
+ || route.provider.adapter === "anthropic";
915
+
916
+ // Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
917
+ if (route.modelId !== parsed.modelId) {
918
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
919
+ (parsed._rawBody as { model?: string }).model = route.modelId;
920
+ }
921
+ parsed.modelId = route.modelId;
922
+ }
923
+ // Transport-neutral reliability policy (#875): applies to any Responses
924
+ // upstream whose final adapter is openai-responses, not only WS turns.
925
+ const responsesUpstreamStreaming = providerModelResponsesUpstreamStreaming(
926
+ route.providerName,
927
+ route.provider,
928
+ route.modelId,
929
+ );
930
+
931
+ // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
932
+ // this request will actually use (#404).
933
+ route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
934
+ if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
935
+ logCtx.model = route.modelId;
936
+ logCtx.provider = route.providerName;
937
+ logCtx.providerAdapter = route.provider.adapter;
938
+ logCtx.routeDecision = route.routeDecision;
939
+
940
+ if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
941
+ parsed.stream = false;
942
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
943
+ (parsed._rawBody as Record<string, unknown>).stream = false;
944
+ }
945
+ }
946
+
947
+ // Final selected model before virtual wire-model rewriting (Pro aliases).
948
+ const finalSelectedModelId = route.modelId;
949
+
950
+ // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
951
+ applyOpenAiVirtualModel(parsed, route, logCtx);
952
+ if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
953
+ logCtx.resolvedModel = route.modelId;
954
+ logCtx.preserveResolvedModelFromRoute = true;
955
+ }
956
+
957
+ // The global Fast Mode setting is a fallback for clients that did not choose a
958
+ // per-chat response speed. Android/Desktop's explicit Standard/Fast selection
959
+ // must win, while the capability gate below still strips unsupported routes.
960
+ const requestHasExplicitServiceTier = parsed.options.serviceTier !== undefined;
961
+ if (
962
+ !requestHasExplicitServiceTier
963
+ && config.fastMode !== undefined
964
+ && route.provider.adapter === "openai-responses"
965
+ && route.provider.supportsServiceTier === true
966
+ ) {
967
+ const tier = config.fastMode ? "priority" : undefined;
968
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
969
+ if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
970
+ else delete (parsed._rawBody as Record<string, unknown>).service_tier;
971
+ }
972
+ parsed.options.serviceTier = tier;
973
+ }
974
+ applyServiceTierGate(route.provider, parsed._rawBody, parsed.options);
975
+
976
+ const reasoningNormalization = applyReasoningCapability(parsed, route);
977
+ if (reasoningNormalization) {
978
+ logCtx.requestedEffort = `${reasoningNormalization.from}->${reasoningNormalization.to}`;
979
+ }
980
+
981
+ {
982
+ const guidance = await multiAgentGuidanceText(parsed, {
983
+ multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
984
+ codexAccountNamespace: route.codexAccountNamespace,
985
+ injectionModel: config.injectionModel,
986
+ injectionEffort: config.injectionEffort,
987
+ subagentModels: config.subagentModels,
988
+ subagentModelFallback: config.subagentModelFallback,
989
+ injectionPrompt: config.injectionPrompt,
990
+ });
991
+ if (guidance) {
992
+ injectDeveloperMessage(parsed, guidance);
993
+ if (isInjectionDebugEnabled()) {
994
+ injectionDebugLog(`[Remodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
995
+ }
996
+ } else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
997
+ injectionDebugLog(`[Remodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
998
+ }
999
+ }
1000
+
1001
+ {
1002
+ const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
1003
+ const surface = collabSurface(parsed);
1004
+ if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
1005
+ const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
1006
+ if (capped) {
1007
+ logCtx.requestedEffort = `${capped.from}->${capped.to}`;
1008
+ if (isInjectionDebugEnabled()) {
1009
+ injectionDebugLog(`[Remodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
1010
+ }
1011
+ }
1012
+ } else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
1013
+ injectionDebugLog(`[Remodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
1014
+ }
1015
+ }
1016
+
1017
+ {
1018
+ const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
1019
+ const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
1020
+ ? nativeEffortClamp(route.modelId, parsed.options.reasoning)
1021
+ : null;
1022
+ if (clamped) {
1023
+ parsed.options.reasoning = clamped;
1024
+ const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
1025
+ if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
1026
+ logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
1027
+ }
1028
+ }
1029
+ recordAttemptRequestedEffort(logCtx);
1030
+ logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
1031
+ route.modelId,
1032
+ logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
1033
+ );
1034
+ }
1035
+
1036
+
1037
+
1038
+ export async function handleComboResponses(
1039
+ req: Request,
1040
+ rawBody: unknown,
1041
+ comboId: string,
1042
+ config: OcxConfig,
1043
+ logCtx: RequestLogContext,
1044
+ options: HandleResponsesOptions,
1045
+ ): Promise<Response> {
1046
+ const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
1047
+ ? (rawBody as { model: string }).model
1048
+ : `combo/${comboId}`;
1049
+ Object.assign(logCtx, {
1050
+ requestedModel,
1051
+ model: requestedModel,
1052
+ provider: "combo",
1053
+ comboId,
1054
+ });
1055
+ const combo = getCombo(config, comboId);
1056
+ if (!combo) {
1057
+ return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
1058
+ }
1059
+ const adoptFailedChildLog = (childLog: RequestLogContext): void => {
1060
+ // Attempts remain the complete physical history; the logical row mirrors the most recent
1061
+ // failed target so an exhausted combo still has useful top-level reasoning diagnostics.
1062
+ Object.assign(logCtx, childLog, {
1063
+ requestedModel,
1064
+ model: requestedModel,
1065
+ provider: "combo",
1066
+ comboId,
1067
+ routeDecision: logCtx.routeDecision,
1068
+ attempts: logCtx.attempts,
1069
+ activeAttempt: undefined,
1070
+ activeAttemptStartedAt: undefined,
1071
+ });
1072
+ };
1073
+
1074
+ const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
1075
+ (rawBody as { input?: unknown } | undefined)?.input,
1076
+ );
1077
+ const canDecryptUnreadableAgentTask = (target: (typeof combo.targets)[number]): boolean => {
1078
+ const provider = config.providers[target.provider];
1079
+ if (!provider || provider.disabled === true) return false;
1080
+ try {
1081
+ const route = routeModel(config, `${target.provider}/${target.model}`);
1082
+ return isCanonicalOpenAiForwardProvider(route.provider);
1083
+ } catch {
1084
+ return false;
1085
+ }
1086
+ };
1087
+ const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
1088
+ !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
1089
+
1090
+ if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
1091
+ return unreadableEncryptedAgentTaskResponse();
1092
+ }
1093
+
1094
+ const initialNow = Date.now();
1095
+ let pick = pickComboTarget(config, comboId, {
1096
+ eligible: target => payloadEligible(target)
1097
+ && !isComboTargetInCooldown(comboId, target, initialNow),
1098
+ });
1099
+ if (!pick) {
1100
+ return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
1101
+ }
1102
+ // One immutable combo selection trace, before any child dispatch; child
1103
+ // adoption below must never replace it with a concrete child route trace.
1104
+ logCtx.routeDecision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
1105
+
1106
+ let lastFailure: Response | null = null;
1107
+ while (pick) {
1108
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
1109
+ const childLog: RequestLogContext = {
1110
+ model: pick.target.model,
1111
+ provider: pick.target.provider,
1112
+ ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
1113
+ ...(logCtx.surface ? { surface: logCtx.surface } : {}),
1114
+ };
1115
+ const targetRoute = routeModel(config, `${pick.target.provider}/${pick.target.model}`);
1116
+ const childBody = concreteComboRequestBody(
1117
+ rawBody,
1118
+ pick.target,
1119
+ comboDefaultEffort(config, comboId),
1120
+ supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
1121
+ );
1122
+ const childHeaders = buildComboChildHeaders(req.headers);
1123
+ const childRequest = new Request(req.url, {
1124
+ method: req.method,
1125
+ headers: childHeaders,
1126
+ body: JSON.stringify(childBody),
1127
+ });
1128
+ let resolvedAuth: CodexAuthContext | undefined;
1129
+ let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
1130
+ const started = Date.now();
1131
+ const attempt = beginRequestAttempt(
1132
+ (logCtx.attempts?.length ?? 0) + 1,
1133
+ pick.target.provider,
1134
+ pick.target.model,
1135
+ config.providers[pick.target.provider]!.adapter,
1136
+ );
1137
+ childLog.activeAttempt = attempt;
1138
+ let attemptRetained = false;
1139
+ const retainCancelledAttempt = (): void => {
1140
+ if (attemptRetained) return;
1141
+ sealRequestAttemptIdentity(
1142
+ attempt,
1143
+ childLog.provider,
1144
+ childLog.providerAdapter ?? attempt.adapter,
1145
+ );
1146
+ finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
1147
+ (logCtx.attempts ??= []).push(attempt);
1148
+ attemptRetained = true;
1149
+ };
1150
+ let consumedChildFailure: ConsumedComboFailure | undefined;
1151
+ const callbackGate = createChildPassthroughCallbackGate(options);
1152
+ let response: Response;
1153
+ try {
1154
+ const currentTargetProvider = pick.target.provider;
1155
+ const deferCodexResetDerivedCooldown = combo.strategy === "failover"
1156
+ && combo.targets.slice(pick.targetIndex + 1).some(target =>
1157
+ target.provider === currentTargetProvider
1158
+ && payloadEligible(target)
1159
+ && !isComboTargetInCooldown(comboId, target),
1160
+ );
1161
+ response = await handleResponses(childRequest, config, childLog, {
1162
+ ...options,
1163
+ comboAttempt: true,
1164
+ deferCodexResetDerivedCooldown,
1165
+ // Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
1166
+ // Object.assign(logCtx, childLog) would overwrite the request-relative value).
1167
+ onFirstOutput: () => {
1168
+ if (attempt.firstOutputMs === undefined) {
1169
+ attempt.firstOutputMs = Math.max(0, Date.now() - started);
1170
+ }
1171
+ options.onFirstOutput?.();
1172
+ },
1173
+ onCodexAuthContextResolved: value => { resolvedAuth = value; },
1174
+ setTerminalOutcomeRecorder: value => { terminalRecorder = value; },
1175
+ onConsumedComboFailure: value => { consumedChildFailure = value; },
1176
+ onNativePassthroughTerminal: callbackGate.onTerminal,
1177
+ onNativePassthroughCancel: callbackGate.onCancel,
1178
+ });
1179
+ } catch (error) {
1180
+ callbackGate.discard();
1181
+ if (options.abortSignal?.aborted) {
1182
+ retainCancelledAttempt();
1183
+ return clientCancelledResponse();
1184
+ }
1185
+ throw error;
1186
+ }
1187
+
1188
+ if (options.abortSignal?.aborted) {
1189
+ callbackGate.discard();
1190
+ retainCancelledAttempt();
1191
+ return clientCancelledResponse();
1192
+ }
1193
+
1194
+ if (response.ok) {
1195
+ sealRequestAttemptIdentity(
1196
+ attempt,
1197
+ childLog.provider,
1198
+ childLog.providerAdapter ?? attempt.adapter,
1199
+ );
1200
+ (logCtx.attempts ??= []).push(attempt);
1201
+ attemptRetained = true;
1202
+ noteComboSuccess(comboId, combo, pick.target, pick.writerGeneration);
1203
+ Object.assign(logCtx, childLog, {
1204
+ requestedModel,
1205
+ model: requestedModel,
1206
+ provider: "combo",
1207
+ comboId,
1208
+ routeDecision: logCtx.routeDecision,
1209
+ attempts: logCtx.attempts,
1210
+ activeAttempt: attempt,
1211
+ activeAttemptStartedAt: started,
1212
+ resolvedModel: childLog.resolvedModel ?? childLog.model,
1213
+ });
1214
+ options.onCodexAuthContextResolved?.(resolvedAuth);
1215
+ options.setTerminalOutcomeRecorder?.(terminalRecorder);
1216
+ callbackGate.commit();
1217
+ return response;
1218
+ }
1219
+
1220
+ callbackGate.discard();
1221
+ if (response.status === 499) {
1222
+ retainCancelledAttempt();
1223
+ return clientCancelledResponse();
1224
+ }
1225
+ let failure: ConsumedComboFailure;
1226
+ try {
1227
+ failure = consumedChildFailure
1228
+ ?? await consumeComboFailure(response, options.abortSignal);
1229
+ } catch (error) {
1230
+ if (options.abortSignal?.aborted) {
1231
+ retainCancelledAttempt();
1232
+ return clientCancelledResponse();
1233
+ }
1234
+ throw error;
1235
+ }
1236
+ if (options.abortSignal?.aborted) {
1237
+ retainCancelledAttempt();
1238
+ return clientCancelledResponse();
1239
+ }
1240
+ sealRequestAttemptIdentity(
1241
+ attempt,
1242
+ childLog.provider,
1243
+ childLog.providerAdapter ?? attempt.adapter,
1244
+ );
1245
+ finishRequestAttempt(
1246
+ attempt,
1247
+ response.status,
1248
+ Date.now() - started,
1249
+ failure.usage,
1250
+ );
1251
+ (logCtx.attempts ??= []).push(attempt);
1252
+ attemptRetained = true;
1253
+ lastFailure = failure.response;
1254
+ if (comboFailureDecision(failure.response.status, failure.classificationText, {
1255
+ code: failure.upstreamCode,
1256
+ }) === "stop") {
1257
+ adoptFailedChildLog(childLog);
1258
+ return lastFailure;
1259
+ }
1260
+ console.warn(
1261
+ `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
1262
+ );
1263
+ const nextPick = advanceComboAfterFailure(config, pick, {
1264
+ retryAfter: failure.retryAfter,
1265
+ now: Date.now(),
1266
+ eligible: payloadEligible,
1267
+ });
1268
+ if (!nextPick) adoptFailedChildLog(childLog);
1269
+ pick = nextPick;
1270
+ }
1271
+ return lastFailure!;
1272
+ }
1273
+
1274
+
1275
+
1276
+ function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget): Response {
1277
+ if (!response.body) {
1278
+ budget.dispose();
1279
+ return response;
1280
+ }
1281
+ const reader = response.body.getReader();
1282
+ let finalized = false;
1283
+ const finalize = () => {
1284
+ if (finalized) return;
1285
+ finalized = true;
1286
+ budget.dispose();
1287
+ };
1288
+ const body = new ReadableStream<Uint8Array>({
1289
+ async pull(controller) {
1290
+ try {
1291
+ const result = await reader.read();
1292
+ if (result.done) {
1293
+ finalize();
1294
+ controller.close();
1295
+ } else {
1296
+ controller.enqueue(result.value);
1297
+ }
1298
+ } catch (error) {
1299
+ finalize();
1300
+ controller.error(error);
1301
+ }
1302
+ },
1303
+ async cancel(reason) {
1304
+ try { await reader.cancel(reason); } finally { finalize(); }
1305
+ },
1306
+ });
1307
+ const finalizedResponse = new Response(body, {
1308
+ status: response.status,
1309
+ statusText: response.statusText,
1310
+ headers: response.headers,
1311
+ });
1312
+ if (isNativePassthroughSseResponse(response)) {
1313
+ markNativePassthroughSseResponse(finalizedResponse);
1314
+ }
1315
+ if (isEagerRelaySseResponse(response)) {
1316
+ markEagerRelaySseResponse(finalizedResponse);
1317
+ }
1318
+ return finalizedResponse;
1319
+ }
1320
+
1321
+ /**
1322
+ * Service-tier capability gate, applied after the final route/wire is settled. A
1323
+ * provider explicitly documented as NOT supporting `service_tier` must never
1324
+ * receive it: strip the field and clear the logging value even when the caller
1325
+ * supplied one (fail closed). Tri-state contract: `true` supports (injection
1326
+ * allowed, caller values preserved), `false` strips, and an UNCLASSIFIED custom
1327
+ * provider (`undefined`) preserves caller-supplied values but never gets an
1328
+ * injection — deleting the caller's field there would silently change their
1329
+ * request against a gateway we know nothing about.
1330
+ */
1331
+ export function applyServiceTierGate(
1332
+ provider: OcxProviderConfig,
1333
+ rawBody: unknown,
1334
+ options: { serviceTier?: string },
1335
+ ): void {
1336
+ if (provider.adapter !== "openai-responses" || provider.supportsServiceTier !== false) return;
1337
+ if (rawBody && typeof rawBody === "object") {
1338
+ delete (rawBody as Record<string, unknown>).service_tier;
1339
+ }
1340
+ options.serviceTier = undefined;
1341
+ }
1342
+
1343
+ /**
1344
+ * Route one `/v1/responses` request through the adapter pipeline: recovery loop, passthrough
1345
+ * wire, image/web-search bridges, and the terminal-guard continuation.
1346
+ */
1347
+ export async function handleResponses(
1348
+ req: Request,
1349
+ config: OcxConfig,
1350
+ logCtx: RequestLogContext,
1351
+ options: HandleResponsesOptions = {},
1352
+ ): Promise<Response> {
1353
+ const ownsBudget = options.translatorBudget === undefined;
1354
+ const translatorBudget = options.translatorBudget ?? createTranslatorBudget();
1355
+ try {
1356
+ const response = await handleResponsesInner(req, config, logCtx, { ...options, translatorBudget });
1357
+ return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
1358
+ } catch (error) {
1359
+ if (ownsBudget) translatorBudget.dispose();
1360
+ throw error;
1361
+ }
1362
+ }
1363
+
1364
+ /**
1365
+ * Inner implementation of `handleResponses`; owns the pre-stream recovery loop and the
1366
+ * per-request same-target 429 retry budgets.
1367
+ */
1368
+ async function handleResponsesInner(
1369
+ req: Request,
1370
+ config: OcxConfig,
1371
+ logCtx: RequestLogContext,
1372
+ options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
1373
+ ): Promise<Response> {
1374
+ let pendingHostAdmissionLease: UpstreamHostAdmissionLease | null = null;
1375
+ let authCtx: CodexAuthContext = { kind: "main", accountId: null };
1376
+ try {
1377
+ // The Chat and Anthropic surfaces replay through here with a Responses-shaped body,
1378
+ // so an omitted value means a genuine Responses inbound.
1379
+ const inboundWire = options.inboundWire ?? "responses";
1380
+ const translatorBudget = options.translatorBudget;
1381
+ let body: unknown;
1382
+ try {
1383
+ body = await readJsonRequestBody(req, translatorBudget);
1384
+ } catch (err) {
1385
+ return decodeRequestErrorResponse(err, "responses");
1386
+ }
1387
+ const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
1388
+ if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
1389
+ return handleComboResponses(req, body, comboId, config, logCtx, options);
1390
+ }
1391
+ const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
1392
+ (body as { input?: unknown } | undefined)?.input,
1393
+ );
1394
+ const originalBody = body;
1395
+ body = expandPreviousResponseInput(body);
1396
+ if (previousResponseReplayFailure(body)) {
1397
+ return formatErrorResponse(
1398
+ 400,
1399
+ "previous_response_not_found",
1400
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
1401
+ );
1402
+ }
1403
+ const previousResponseInputExpanded = body !== originalBody;
1404
+
1405
+ // Spawn-message compatibility (both directions): agent_message task payloads ride in
1406
+ // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
1407
+ // parsing so every consumer sees the payload: parseRequest (routed/translated providers read
1408
+ // the parsed messages) and the native passthrough (_rawBody is this same object, serialized
1409
+ // verbatim). Genuine backend ciphertext is left byte-identical (looksLikeBackendCiphertext).
1410
+ {
1411
+ const rewritten = sanitizeEncryptedContentInPlace(
1412
+ (body as { input?: unknown } | undefined)?.input,
1413
+ );
1414
+ if (rewritten > 0)
1415
+ console.warn(
1416
+ `[Remodex] rewrote ${rewritten} plaintext encrypted_content part(s) to input_text (spawn-message compatibility)`,
1417
+ );
1418
+ }
1419
+
1420
+ let parsed;
1421
+ let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
1422
+ try {
1423
+ parsed = parseRequest(body);
1424
+ toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
1425
+ if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
1426
+ parsed._providerContinuation = previousResponseProviderState(parsed.previousResponseId);
1427
+ parsed._cursorConversationId = parsed._providerContinuation?.cursor?.conversationId;
1428
+ const clientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim();
1429
+ if (clientThreadId) parsed._clientThreadId = clientThreadId;
1430
+ } catch (err) {
1431
+ if (isTranslatorBudgetExceededError(err)) {
1432
+ return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
1433
+ code: "translation_buffer_limit",
1434
+ });
1435
+ }
1436
+ return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
1437
+ }
1438
+ // Prefer a pre-populated id (routed Claude) over Responses headers that may be
1439
+ // absent or synthetically injected (session_id from prompt_cache_key).
1440
+ if (!logCtx.conversationId) {
1441
+ logCtx.conversationId = conversationIdFromResponsesRequest({
1442
+ clientThreadId: parsed._clientThreadId,
1443
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
1444
+ threadIdHeader: req.headers.get("thread-id"),
1445
+ cursorConversationId: parsed._cursorConversationId,
1446
+ });
1447
+ }
1448
+ logCtx.requestedModel = parsed.modelId;
1449
+ logCtx.requestedEffort = parsed.options.reasoning;
1450
+ logCtx.requestedServiceTier = parsed.options.serviceTier;
1451
+ logCtx.requestedSpeedLabel = requestLogSpeedLabel(parsed.options.serviceTier);
1452
+ logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
1453
+ logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
1454
+
1455
+ // Shadow call intercept: rewrite Codex 0.145.0+ helper calls (gpt-5.6-luna).
1456
+ // Ancient clients using gpt-5.4-mini remain configurable via sourceModels.
1457
+ const _sci = config.shadowCallIntercept;
1458
+ if (_sci?.enabled && _sci.model && shouldInterceptShadowCall(
1459
+ parsed.modelId,
1460
+ _sci.sourceModels,
1461
+ req.headers,
1462
+ )) {
1463
+ const _sciOriginal = parsed.modelId;
1464
+ parsed.modelId = _sci.model;
1465
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
1466
+ (parsed._rawBody as { model?: string }).model = _sci.model;
1467
+ }
1468
+ // Force effort to low for shadow/helper calls (matching upstream behavior)
1469
+ parsed.options.reasoning = "low";
1470
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
1471
+ (parsed._rawBody as Record<string, unknown>).reasoning = { effort: "low" };
1472
+ }
1473
+ (logCtx as unknown as Record<string, unknown>).shadowCallRewrittenFrom = _sciOriginal;
1474
+ // Helpers must not resume/append into the parent thread's Cursor conversation.
1475
+ parsed._cursorIsolateConversation = true;
1476
+ }
1477
+ if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
1478
+
1479
+ let route: RouteResult;
1480
+ try {
1481
+ route = routeModel(config, parsed.modelId, evidenceFromBody(parsed._rawBody));
1482
+ logCtx.routeDecision = route.routeDecision;
1483
+ } catch (err) {
1484
+ if (err instanceof NoAvailableComboTargetsError) {
1485
+ return comboUnavailableResponse(err.message);
1486
+ }
1487
+ if (err instanceof NoEligiblePolicyCandidateError) {
1488
+ // Persist the evaluation trace (per-candidate exclusions + the
1489
+ // no-eligible reason) so failed policy requests stay auditable.
1490
+ logCtx.routeDecision = err.trace;
1491
+ }
1492
+ return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
1493
+ }
1494
+
1495
+ const hasUnexpandedPreviousResponse = !!parsed.previousResponseId
1496
+ && parsed._previousResponseInputExpanded !== true;
1497
+ // Exact account selectors are isolated from Pool-wide quota work. A canonical replay miss must
1498
+ // also fail closed without polling quota upstream. Cached fallback state can still select a
1499
+ // provider with native continuation support below.
1500
+ const threadSpawn = isThreadSpawnRequest(req.headers);
1501
+ const previewSelectionAdmission = threadSpawn && route.codexAccountId === undefined
1502
+ ? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
1503
+ : undefined;
1504
+ const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
1505
+ const nativeMainReadsForbidden = nativeMainRecoveryBlocked
1506
+ || previewSelectionAdmission?.mainProfileDraining === true;
1507
+ const previewSelectionOptions = {
1508
+ nativeMainSelectionOnly: !nativeMainRecoveryBlocked
1509
+ && previewSelectionAdmission?.mainProfileDraining === true,
1510
+ };
1511
+ let selectedForwardHeaders = req.headers;
1512
+ let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
1513
+ let subagentQuotaFailureModel = parsed.modelId;
1514
+
1515
+ try {
1516
+ if (
1517
+ threadSpawn
1518
+ && route.codexAccountId === undefined
1519
+ && !(hasUnexpandedPreviousResponse && isCanonicalOpenAiForwardProvider(route.provider))
1520
+ ) {
1521
+ await maybePrimeSubagentQuota(config, Date.now(), { nativeMainReadsForbidden });
1522
+ }
1523
+
1524
+ // Subagent fallback must settle the final model/provider BEFORE route-dependent
1525
+ // normalization (virtual models, effort caps, service tier, wire protocol).
1526
+ // Preview the preferred Codex account without acquiring a probe lease or refreshing
1527
+ // tokens — auth is resolved only after the final route is selected.
1528
+ if (threadSpawn && !options.comboAttempt && route.codexAccountId === undefined) {
1529
+ const threadId = req.headers.get("x-codex-parent-thread-id");
1530
+ const previewAccountId = previewCodexAccountForRequest(
1531
+ threadId,
1532
+ config,
1533
+ Date.now(),
1534
+ undefined,
1535
+ previewSelectionOptions,
1536
+ );
1537
+ subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
1538
+ const fallback = applySubagentModelFallback(
1539
+ parsed,
1540
+ req.headers,
1541
+ config,
1542
+ previewAccountId,
1543
+ Date.now(),
1544
+ unreadableEncryptedAgentTask,
1545
+ previewSelectionOptions,
1546
+ );
1547
+ if (fallback) {
1548
+ (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
1549
+ (logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
1550
+ if (isInjectionDebugEnabled()) {
1551
+ injectionDebugLog(`[Remodex] subagent model fallback ${fallback.from} -> ${fallback.to}`);
1552
+ }
1553
+ }
1554
+ subagentQuotaFailureModel = fallback?.to ?? parsed.modelId;
1555
+
1556
+ if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
1557
+ try {
1558
+ route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
1559
+ logCtx.routeDecision = route.routeDecision;
1560
+ } catch (err) {
1561
+ if (err instanceof NoAvailableComboTargetsError) {
1562
+ return comboUnavailableResponse(err.message);
1563
+ }
1564
+ if (err instanceof NoEligiblePolicyCandidateError) {
1565
+ logCtx.routeDecision = err.trace;
1566
+ }
1567
+ return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
1568
+ }
1569
+ }
1570
+ }
1571
+ } finally {
1572
+ previewSelectionAdmission?.release();
1573
+ }
1574
+
1575
+ // Encrypted child tasks may only reach the canonical native backend. This check
1576
+ // runs against the FINAL route so native-only fallback can rescue a routed primary.
1577
+ if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
1578
+ return unreadableEncryptedAgentTaskResponse();
1579
+ }
1580
+
1581
+ // The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no
1582
+ // safe way to recover the omitted history. Fail before auth, adapter construction, or upstream
1583
+ // I/O instead of stripping the id and silently forwarding a context-free delta (#702).
1584
+ if (
1585
+ hasUnexpandedPreviousResponse
1586
+ && isCanonicalOpenAiForwardProvider(route.provider)
1587
+ ) {
1588
+ return formatErrorResponse(
1589
+ 400,
1590
+ "invalid_request_error",
1591
+ "OpenAI forward continuation state is unavailable or expired; start a new session instead of reusing this previous_response_id.",
1592
+ );
1593
+ }
1594
+
1595
+ // Captured before normalization: whether the CLIENT asked for SSE. The
1596
+ // transport-neutral upstream-streaming policy below may force a bounded JSON
1597
+ // upstream for reliability (#875); the answer must then be reframed to SSE
1598
+ // for streaming clients.
1599
+ const clientRequestedStream = parsed.stream;
1600
+ await applyFinalRouteRequestNormalization({
1601
+ parsed,
1602
+ route,
1603
+ config,
1604
+ req,
1605
+ logCtx,
1606
+ inboundWire,
1607
+ inboundTransport: options.inboundTransport,
1608
+ });
1609
+ // Attribute local auth/cooldown failures to the public selector too; exact auth may fail before
1610
+ // the normal post-resolution provider label is assigned.
1611
+ if (route.codexAccountNamespace) {
1612
+ logCtx.provider = `${route.providerName}-${route.codexAccountNamespace}`;
1613
+ }
1614
+
1615
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
1616
+ const preAuthHostKey = preAuthUpstreamHostCircuitKey(route, config);
1617
+ if (preAuthHostKey) {
1618
+ const admission = acquireUpstreamHostAdmission(
1619
+ preAuthHostKey,
1620
+ config.upstreamHostCircuitThreshold,
1621
+ );
1622
+ if (admission.kind === "blocked") {
1623
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
1624
+ }
1625
+ pendingHostAdmissionLease = admission.lease;
1626
+ }
1627
+
1628
+ {
1629
+ const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
1630
+ if (!finalAuth.ok) return finalAuth.response;
1631
+ authCtx = finalAuth.authCtx;
1632
+ selectedForwardHeaders = finalAuth.headers;
1633
+ }
1634
+
1635
+ route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
1636
+ logCtx.provider = route.codexAccountNamespace
1637
+ ? `${route.providerName}-${route.codexAccountNamespace}`
1638
+ : formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
1639
+ // Prefer Codex pool account as the Cursor thread namespace when present. Cursor routes without
1640
+ // codexAccountMode still get a credential-derived scope inside the Cursor adapter.
1641
+ const identityScope = codexLogAccountId(authCtx);
1642
+ if (identityScope) parsed._cursorIdentityScope = identityScope;
1643
+ subagentFallbackAccountId = authCtx.kind === "pool" || authCtx.kind === "main-pool"
1644
+ ? authCtx.accountId
1645
+ : config.activeCodexAccountId ?? null;
1646
+
1647
+ // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
1648
+ // existing openai-chat / anthropic adapters authenticate with no change.
1649
+ const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
1650
+ && route.provider.authMode === "oauth";
1651
+ let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
1652
+ let anthropicPoolAccountId: string | null = null;
1653
+ let anthropicPoolFailovers = 0;
1654
+ const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
1655
+ ? anthropicSessionKeyFromParts({
1656
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
1657
+ threadIdHeader: req.headers.get("thread-id"),
1658
+ promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
1659
+ clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
1660
+ promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
1661
+ })
1662
+ : null;
1663
+ if (route.provider.authMode === "oauth") {
1664
+ try {
1665
+ if (route.providerName === "anthropic" && isAnthropicAccountPoolEnabled(config)) {
1666
+ const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config);
1667
+ if (!selection.accountId) {
1668
+ if (selection.reason === "all-cooled") {
1669
+ const retryAfterSec = getAnthropicPoolRetryAfterSeconds();
1670
+ return formatErrorResponse(
1671
+ 429,
1672
+ "rate_limit_error",
1673
+ "All Anthropic OAuth accounts are temporarily rate-limited",
1674
+ retryAfterSec !== null ? { retryAfter: String(retryAfterSec) } : undefined,
1675
+ );
1676
+ }
1677
+ return formatErrorResponse(401, "authentication_error", "No eligible Anthropic OAuth account available");
1678
+ }
1679
+ const accessToken = await getAnthropicPoolAccessToken(selection.accountId);
1680
+ anthropicPoolAccountId = selection.accountId;
1681
+ bindAnthropicSessionAffinity(anthropicSessionKey, selection.accountId);
1682
+ promoteAnthropicActiveAccount(selection.accountId);
1683
+ route.provider = { ...route.provider, apiKey: accessToken };
1684
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
1685
+ } else {
1686
+ const resolved = await getValidAccessTokenSnapshot(route.providerName);
1687
+ if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
1688
+ route.provider = { ...route.provider, apiKey: resolved.accessToken };
1689
+ if (route.providerName === "kiro") {
1690
+ // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
1691
+ // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
1692
+ parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
1693
+ }
1694
+ // Antigravity (cloud-code-assist) needs the discovered Cloud Code Assist project id in the
1695
+ // CCA envelope. Keep it paired with the token snapshot so an account rotation cannot mix
1696
+ // a fresh token with project metadata re-read from a different credential generation.
1697
+ if (route.provider.googleMode === "cloud-code-assist" && !route.provider.project) {
1698
+ const projectId = resolved.projectId;
1699
+ if (projectId) route.provider = { ...route.provider, project: projectId };
1700
+ }
1701
+ }
1702
+ } catch (err) {
1703
+ if (err instanceof UnsupportedOAuthProviderError) {
1704
+ return formatErrorResponse(
1705
+ 400,
1706
+ "invalid_request_error",
1707
+ `${err.message}. Remove or reconfigure provider '${route.providerName}' in ${getConfigPath()}.`,
1708
+ );
1709
+ }
1710
+ return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
1711
+ }
1712
+ }
1713
+ route.provider = resolveProviderTransport(
1714
+ route.providerName,
1715
+ route.provider,
1716
+ parsed.options.promptCacheKey,
1717
+ route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
1718
+ );
1719
+ const adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
1720
+ const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
1721
+ logCtx.providerAdapter = adapter.name;
1722
+ // Ordinary requests receive one durable attempt only after their final initial
1723
+ // adapter is resolved. Combo children own their attempt and retries keep it.
1724
+ if (!options.comboAttempt && !logCtx.activeAttempt) {
1725
+ const attempt = beginRequestAttempt(
1726
+ (logCtx.attempts?.length ?? 0) + 1,
1727
+ logCtx.provider,
1728
+ route.modelId,
1729
+ adapter.name,
1730
+ );
1731
+ logCtx.activeAttempt = attempt;
1732
+ logCtx.activeAttemptStartedAt = Date.now();
1733
+ (logCtx.attempts ??= []).push(attempt);
1734
+ }
1735
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name);
1736
+ const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
1737
+
1738
+ if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
1739
+ return formatErrorResponse(
1740
+ 400,
1741
+ "invalid_request_error",
1742
+ "Kiro continuation state is missing; start a new session instead of reusing this previous_response_id.",
1743
+ );
1744
+ }
1745
+
1746
+ let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
1747
+ const needsOpenAiVision = shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed);
1748
+ const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
1749
+ if (needsOpenAiVision || needsOpenAiSearch) {
1750
+ try {
1751
+ openAiSidecar = await resolveFirstUsableOpenAiSidecar(
1752
+ listOpenAiForwardSidecarCandidates(config),
1753
+ req.headers,
1754
+ config,
1755
+ {
1756
+ // Account-qualified native routes are passthrough, so their in-turn helper is vision.
1757
+ // Scope its cooldown and outcome to the helper model, not the routed text model.
1758
+ ...(route.codexAccountId !== undefined
1759
+ ? { exactAccount: { accountId: route.codexAccountId, modelId: resolveOpenAiVisionModel(config) } }
1760
+ : {}),
1761
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
1762
+ },
1763
+ );
1764
+ } catch (err) {
1765
+ // Sidecars are optional helpers for an otherwise independent routed turn.
1766
+ // An unavailable/cooling/expired Multi credential disables the helper; it
1767
+ // must not turn a valid routed-provider request into a Codex-auth failure.
1768
+ if (
1769
+ !(err instanceof CodexPoolAuthenticationError)
1770
+ && !(err instanceof CodexAuthContextError)
1771
+ && !(err instanceof CodexAccountCooldownError)
1772
+ && !(err instanceof CodexThreadAffinityExpiredError)
1773
+ && !(err instanceof CodexMainProfileDrainingError)
1774
+ ) throw err;
1775
+ }
1776
+ }
1777
+
1778
+ // Vision sidecar: the routed model can't see images (provider.noVisionModels). Describe each
1779
+ // attached image through the selected sidecar backend and replace it with text BEFORE the main
1780
+ // call, so the text-only model can reason about it.
1781
+ const visionPlan = planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
1782
+ const recordSidecarOutcome = openAiSidecar?.recordOutcome;
1783
+ if (visionPlan) {
1784
+ await describeImagesInPlace(
1785
+ parsed,
1786
+ visionPlan,
1787
+ openAiSidecar?.headers ?? selectedForwardHeaders,
1788
+ options.abortSignal,
1789
+ recordSidecarOutcome,
1790
+ translatorBudget,
1791
+ );
1792
+ } else if (modelInList(route.provider.noVisionModels, route.modelId)) {
1793
+ // Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar
1794
+ // disabled): fail closed — never forward raw images to a text-only upstream.
1795
+ stripImagesInPlace(parsed, translatorBudget);
1796
+ }
1797
+
1798
+ const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
1799
+
1800
+ const continuationStateForResponse = (
1801
+ emitted?: OcxProviderContinuationState,
1802
+ ): OcxProviderContinuationState | undefined => {
1803
+ const cursorConversationId = parsed._cursorConversationId;
1804
+ const inherited = parsed._providerContinuation;
1805
+ if (!emitted && !inherited && !cursorConversationId) return undefined;
1806
+ return {
1807
+ ...(inherited ?? {}),
1808
+ ...(emitted ?? {}),
1809
+ ...((inherited?.kiro || emitted?.kiro)
1810
+ ? { kiro: { ...(inherited?.kiro ?? {}), ...(emitted?.kiro ?? {}) } }
1811
+ : {}),
1812
+ ...(cursorConversationId
1813
+ ? {
1814
+ cursor: {
1815
+ ...(inherited?.cursor ?? {}),
1816
+ ...(emitted?.cursor ?? {}),
1817
+ conversationId: cursorConversationId,
1818
+ },
1819
+ }
1820
+ : {}),
1821
+ };
1822
+ };
1823
+
1824
+ // Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
1825
+ // one `{type:"compaction"}` output item (codex-rs compact_remote_v2.rs). Passthrough handles it
1826
+ // natively upstream; here we run the routed model as a plain summarizer — no tools, no web-search
1827
+ // sidecar — and the bridge appends the synthetic compaction item (src/responses/compaction.ts).
1828
+ // A Responses-shaped wire does not imply support for Codex's private
1829
+ // `compaction_trigger` item — only the canonical ChatGPT backend speaks that
1830
+ // contract. An API-key gateway would receive the trigger, answer with an ordinary
1831
+ // message, and leave Codex fataling on a missing compaction item (#422).
1832
+ const routedCompaction = parsed._compactionRequest === true
1833
+ && !isCanonicalOpenAiForwardProvider(route.provider);
1834
+ if (routedCompaction) {
1835
+ delete parsed.context.tools;
1836
+ delete parsed._webSearch;
1837
+ delete parsed.options.toolChoice;
1838
+ delete parsed.options.parallelToolCalls;
1839
+ // The compaction turn is a plain prose summary; a surviving structured-output format
1840
+ // would force schema-constrained JSON into the synthetic compaction item. The flag and
1841
+ // the raw `text` controls go too: Kiro's capability guard reads both and would reject
1842
+ // the turn outright, and the key-mode openai-responses adapter builds from _rawBody.
1843
+ delete parsed.options.textFormat;
1844
+ delete parsed._structuredOutput;
1845
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
1846
+ delete (parsed._rawBody as Record<string, unknown>).text;
1847
+ }
1848
+ parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() });
1849
+ }
1850
+
1851
+ if ("passthrough" in adapter && adapter.passthrough && !routedCompaction) {
1852
+ let hostAdmissionLease = pendingHostAdmissionLease;
1853
+ pendingHostAdmissionLease = null;
1854
+ try {
1855
+ const imageGenCallAliases = route.provider.authMode === "forward"
1856
+ ? new Map<string, { namespace: string; name: string }>()
1857
+ : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
1858
+ // Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
1859
+ // previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
1860
+ // REST backend rejects the parameter — the adapter strips it in forward mode, so the ONLY
1861
+ // way a chained turn keeps its earlier context is the local replay expansion. Record
1862
+ // completed passthrough responses (force bypasses Codex's blanket store:false) so the next
1863
+ // turn's expansion hits. Never record a body whose own previous_response_id failed to
1864
+ // expand: its input is a delta, and storing it would replay a truncated conversation.
1865
+ // Compaction turns are excluded: _rawBody still carries the full pre-compaction history and
1866
+ // recording it would let a later expansion rehydrate the chain Codex just replaced.
1867
+ const passthroughRecordEligible = parsed._compactionRequest !== true
1868
+ && (!parsed.previousResponseId || parsed._previousResponseInputExpanded === true);
1869
+ const rememberPassthroughResponse = passthroughRecordEligible
1870
+ ? (response: { id?: unknown; output?: unknown; status?: unknown }) =>
1871
+ rememberResponseState(parsed._rawBody, response, undefined, { force: true })
1872
+ : undefined;
1873
+ if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
1874
+ console.warn(
1875
+ `[responses] previous_response_id ${parsed.previousResponseId} not found in local replay state `
1876
+ + `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
1877
+ );
1878
+ }
1879
+ let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
1880
+ try {
1881
+ request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
1882
+ } catch (error) {
1883
+ releaseCodexAuthContextProbeLease(authCtx);
1884
+ throw error;
1885
+ }
1886
+ recordAdapterReasoning(logCtx, request);
1887
+ const actualHostKey = upstreamHostHealthKey(
1888
+ route.providerName,
1889
+ safeOriginLabel(request.url),
1890
+ );
1891
+ const hostKey = route.provider.authMode === "forward"
1892
+ ? actualHostKey
1893
+ : null;
1894
+ const hostCircuitEnabled = hostKey !== null
1895
+ && normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0;
1896
+ if (hostKey !== null && !hostCircuitEnabled) {
1897
+ disableUpstreamHostCircuitForKey(actualHostKey);
1898
+ }
1899
+ if (hostAdmissionLease && hostAdmissionLease.key !== hostKey) {
1900
+ return formatErrorResponse(502, "upstream_error", "Provider host changed after circuit admission");
1901
+ }
1902
+ if (options.abortSignal?.aborted) {
1903
+ releaseCodexAuthContextProbeLease(authCtx);
1904
+ return clientCancelledResponse();
1905
+ }
1906
+ if (!hostAdmissionLease && hostCircuitEnabled) {
1907
+ const admission = acquireUpstreamHostAdmission(
1908
+ hostKey!,
1909
+ config.upstreamHostCircuitThreshold,
1910
+ );
1911
+ if (admission.kind === "blocked") {
1912
+ releaseCodexAuthContextProbeLease(authCtx);
1913
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
1914
+ }
1915
+ hostAdmissionLease = admission.lease;
1916
+ }
1917
+ const settleObservedHostResponse = (): void => {
1918
+ if (hostCircuitEnabled) {
1919
+ resetUpstreamHostHealth(actualHostKey, hostAdmissionLease);
1920
+ } else {
1921
+ resetUpstreamHostHealth(actualHostKey);
1922
+ }
1923
+ hostAdmissionLease = null;
1924
+ };
1925
+ const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
1926
+ ? request.usageLog.inputTokens
1927
+ : undefined;
1928
+ if (passthroughEstimate !== undefined) {
1929
+ logCtx.usageLogInputTokens = passthroughEstimate;
1930
+ }
1931
+ // Abort the upstream if the client disconnects. A directly-relayed body does not propagate the
1932
+ // consumer's cancel to a signalled fetch, so we pass the signal and relay through relayWithAbort,
1933
+ // whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
1934
+ const upstream = new AbortController();
1935
+ linkAbortSignal(upstream, options.abortSignal);
1936
+ const connectMs = config.connectTimeoutMs ?? 200_000;
1937
+ let upstreamResponse: Response;
1938
+ const transportFailureResponse = (err: unknown): Response => {
1939
+ upstream.abort();
1940
+ if (options.abortSignal?.aborted) {
1941
+ releaseUpstreamHostAdmission(hostAdmissionLease);
1942
+ hostAdmissionLease = null;
1943
+ releaseCodexAuthContextProbeLease(authCtx);
1944
+ return clientCancelledResponse();
1945
+ }
1946
+ const outcome = classifyTransportFailureKind(err);
1947
+ // Host-level evidence stands regardless of pool membership: a direct
1948
+ // forward send has no pool accounting, but the reachability failure is
1949
+ // still host-wide, not account evidence (#914 review).
1950
+ if (outcome === "connect_neutral") {
1951
+ if (hostCircuitEnabled) {
1952
+ recordUpstreamHostFailure(actualHostKey, {
1953
+ code: transportErrorCode(err),
1954
+ threshold: config.upstreamHostCircuitThreshold,
1955
+ lease: hostAdmissionLease,
1956
+ });
1957
+ } else {
1958
+ recordUpstreamHostFailure(actualHostKey, { code: transportErrorCode(err) });
1959
+ }
1960
+ hostAdmissionLease = null;
1961
+ } else {
1962
+ releaseUpstreamHostAdmission(hostAdmissionLease);
1963
+ hostAdmissionLease = null;
1964
+ }
1965
+ if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1966
+ recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
1967
+ threadId: req.headers.get("x-codex-parent-thread-id"),
1968
+ fixedAccount: authCtx.fixedAccount,
1969
+ modelId: route.modelId,
1970
+ probeLeaseId: codexProbeLeaseId(authCtx),
1971
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
1972
+ writerGeneration: authCtx.writerGeneration,
1973
+ });
1974
+ }
1975
+ const msg = outcome === "timeout"
1976
+ ? `Provider connect timeout after ${connectMs}ms`
1977
+ : describeUpstreamConnectFailure(err, connectMs);
1978
+ return formatErrorResponse(502, "upstream_error", msg);
1979
+ };
1980
+ try {
1981
+ // Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
1982
+ // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
1983
+ // Body is a replayable string; nothing has streamed to the client yet.
1984
+ upstreamResponse = await fetchWithTransientRetry(
1985
+ recovery => {
1986
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery);
1987
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
1988
+ method: request.method,
1989
+ headers: request.headers,
1990
+ body: request.body,
1991
+ }, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider),
1992
+ route.provider.authMode === "forward")
1993
+ // Every real attempt response — including an intermediate 5xx the
1994
+ // retry wrapper replaces — proves the host was reached (#914 review).
1995
+ .then(res => {
1996
+ settleObservedHostResponse();
1997
+ return res;
1998
+ });
1999
+ },
2000
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
2001
+ );
2002
+ } catch (err) {
2003
+ return transportFailureResponse(err);
2004
+ } finally {
2005
+ request.releaseBodyObservation?.();
2006
+ }
2007
+
2008
+ // Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
2009
+ // passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
2010
+ // key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
2011
+ // immediately with no same-key replay. Pre-stream only — nothing has been relayed yet, so
2012
+ // the replay is lossless (same invariant as the recovery loop). Forward/OAuth providers
2013
+ // keep their pool logic below (rateLimitRetryPolicyFor returns null for them).
2014
+ const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
2015
+ let rateLimitRetries = 0;
2016
+ while (
2017
+ upstreamResponse.status === 429
2018
+ && rateLimitPolicy !== null
2019
+ && rateLimitRetries < rateLimitPolicy.attempts
2020
+ ) {
2021
+ rateLimitRetries += 1;
2022
+ // Release unread body + deliberate wait via the shared same-target helper.
2023
+ const retryAfterHeader = upstreamResponse.headers.get("retry-after");
2024
+ try {
2025
+ for await (const _ of prepareSameTarget429Wait({
2026
+ body: upstreamResponse.body,
2027
+ signal: options.abortSignal,
2028
+ delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
2029
+ })) {
2030
+ // pre-stream: no stall watchdog to feed
2031
+ }
2032
+ } catch {
2033
+ upstream.abort();
2034
+ return clientCancelledResponse();
2035
+ }
2036
+ // Client cancellation wins over any stale timer edge: re-check before dispatching the
2037
+ // replay so the wire never starts work for a request the client already abandoned.
2038
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
2039
+ upstream.abort();
2040
+ return clientCancelledResponse();
2041
+ }
2042
+ try {
2043
+ upstreamResponse = await fetchWithTransientRetry(
2044
+ recovery => {
2045
+ // The first send of every replay is itself a rate-limit retry; inner transient-5xx
2046
+ // recoveries keep their own label (recovery is provided for those).
2047
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "rate-limit-429");
2048
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
2049
+ method: request.method,
2050
+ headers: request.headers,
2051
+ body: request.body,
2052
+ }, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider),
2053
+ route.provider.authMode === "forward")
2054
+ .then(res => {
2055
+ settleObservedHostResponse();
2056
+ return res;
2057
+ });
2058
+ },
2059
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
2060
+ );
2061
+ } catch (err) {
2062
+ return transportFailureResponse(err);
2063
+ }
2064
+ }
2065
+
2066
+ if (usesCodexForwardPoolAuth(authCtx, route.provider) && !authCtx.fixedAccount) {
2067
+ let poolRetryOutcome: number | undefined;
2068
+ if (await shouldRetryCodexPoolAccountModel400(
2069
+ upstreamResponse,
2070
+ route.modelId,
2071
+ options.abortSignal,
2072
+ )) {
2073
+ poolRetryOutcome = 400;
2074
+ } else if (shouldRetryCodexPoolAccountQuota(upstreamResponse)) {
2075
+ // Pre-stream only: once SSE has begun, mid-stream quota stays terminal.
2076
+ poolRetryOutcome = upstreamResponse.status;
2077
+ }
2078
+
2079
+ if (poolRetryOutcome !== undefined) {
2080
+ const retry = await retryCodexPoolOnAlternateAccount({
2081
+ req,
2082
+ config,
2083
+ route,
2084
+ parsed,
2085
+ logCtx,
2086
+ options,
2087
+ firstAuthCtx: authCtx,
2088
+ firstResponse: upstreamResponse,
2089
+ outcomeStatus: poolRetryOutcome,
2090
+ upstream,
2091
+ connectMs,
2092
+ passthroughEstimate,
2093
+ stream: parsed.stream,
2094
+ });
2095
+ if (retry.kind === "transport") {
2096
+ authCtx = retry.authCtx;
2097
+ return transportFailureResponse(retry.error);
2098
+ }
2099
+ if (retry.kind === "retried") {
2100
+ authCtx = retry.authCtx;
2101
+ request = retry.request;
2102
+ upstreamResponse = retry.upstreamResponse;
2103
+ selectedForwardHeaders = retry.selectedForwardHeaders;
2104
+ // Keep subagent quota-failure health keyed to the account that actually served.
2105
+ subagentFallbackAccountId = retry.authCtx.accountId;
2106
+ }
2107
+ }
2108
+ }
2109
+ const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
2110
+ const resolvedModel = headers.get("openai-model")?.trim();
2111
+ if (resolvedModel) logCtx.resolvedModel = resolvedModel;
2112
+ if (isUsageDebugEnabled()) {
2113
+ const upstreamContentType = upstreamResponse.headers.get("content-type");
2114
+ if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
2115
+ }
2116
+ // The chatgpt backend may omit Content-Type on SSE responses. Fall back to
2117
+ // treating a successful body as SSE when the caller requested streaming.
2118
+ const passthroughCt = headers.get("content-type")?.toLowerCase();
2119
+ const isEventStream = passthroughCt?.includes("text/event-stream")
2120
+ || (upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
2121
+ const terminalRecorder = codexForwardTerminalOutcomeRecorder(
2122
+ config,
2123
+ authCtx,
2124
+ route.provider,
2125
+ route.modelId,
2126
+ logCtx,
2127
+ req.headers.get("x-codex-parent-thread-id"),
2128
+ );
2129
+ const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
2130
+ // Capture quota from upstream response for multi-account tracking
2131
+ if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
2132
+ // primary was the 5h window; it now carries weekly data for GPT plans.
2133
+ // Prefer primary when present, fall back to secondary for compatibility.
2134
+ const quotaMeta = codexQuotaOutcomeMeta(upstreamResponse);
2135
+ const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
2136
+ applyAccountQuotaFromUpstreamHeaders(
2137
+ authCtx.accountId,
2138
+ upstreamResponse.headers,
2139
+ authCtx.writerGeneration,
2140
+ );
2141
+ if (terminalBodyWillRecord) {
2142
+ options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
2143
+ terminalRecorder(status, httpStatusOverride);
2144
+ if (status === "failed") {
2145
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
2146
+ || logCtx.terminalHttpStatus === 429
2147
+ || logCtx.terminalHttpStatus === 402
2148
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
2149
+ : undefined;
2150
+ if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
2151
+ recordSubagentQuotaFailureForThreadSpawn(
2152
+ req.headers,
2153
+ subagentQuotaFailureModel,
2154
+ quotaFailureMessage,
2155
+ config,
2156
+ subagentFallbackAccountId,
2157
+ );
2158
+ }
2159
+ }
2160
+ options.onNativePassthroughTerminal?.(status);
2161
+ });
2162
+ } else if (!shouldDeferCodexResetDerivedCooldown(
2163
+ upstreamResponse,
2164
+ options.deferCodexResetDerivedCooldown,
2165
+ )) {
2166
+ recordCodexUpstreamOutcome(config, authCtx.accountId, upstreamResponse.status, {
2167
+ ...quotaMeta,
2168
+ threadId: req.headers.get("x-codex-parent-thread-id"),
2169
+ fixedAccount: authCtx.fixedAccount,
2170
+ modelId: route.modelId,
2171
+ probeLeaseId: codexProbeLeaseId(authCtx),
2172
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
2173
+ writerGeneration: authCtx.writerGeneration,
2174
+ });
2175
+ }
2176
+ }
2177
+
2178
+ // Non-2xx passthrough failures must never reach Codex as an empty body —
2179
+ // Codex renders that as the opaque "Unknown error" (#452). Combo attempts
2180
+ // keep their typed failure envelope. Non-empty bodies are relayed verbatim
2181
+ // (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
2182
+ // Manual-redirect policy (#914): a 3xx is relayed as-is (Location preserved
2183
+ // through sanitizePassthroughHeaders) so a redirect to a dead host can never
2184
+ // masquerade as a pre-connection failure after the credential was seen.
2185
+ // The numeric outcome above already classified it neutral — no streak.
2186
+ if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
2187
+ return new Response(upstreamResponse.body, {
2188
+ status: upstreamResponse.status,
2189
+ statusText: upstreamResponse.statusText,
2190
+ headers: sanitizePassthroughHeaders(upstreamResponse.headers),
2191
+ });
2192
+ }
2193
+ if (!upstreamResponse.ok) {
2194
+ if (options.comboAttempt) {
2195
+ const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
2196
+ options.onConsumedComboFailure?.(failure);
2197
+ return failure.response;
2198
+ }
2199
+ const errorText = await upstreamResponse.text().catch(() => "");
2200
+ return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
2201
+ statusText: upstreamResponse.statusText,
2202
+ headers,
2203
+ });
2204
+ }
2205
+
2206
+ // Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
2207
+ // async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
2208
+ // native relay, never enters JS Sink.write); branch[1] is consumed in the
2209
+ // background for terminal-outcome/quota inspection only.
2210
+ // #314 alternative shape: win32 no-rewrite traffic follows the runtime/config
2211
+ // gate; darwin no-rewrite traffic joins it only for explicit
2212
+ // `streamMode: "eager-relay"` opt-in. Darwin `auto` always stays tee. The
2213
+ // eager shape skips tee and uses one bounded reader with inline inspection
2214
+ // (src/server/relay-eager.ts; policy:
2215
+ // devlog/_fin/260731_macos_rss_retention/100_darwin_eager_optin.md).
2216
+ // The bundled known-bad runtime remains on tee by default on both platforms.
2217
+ if (isEventStream && upstreamResponse.body) {
2218
+ const repairConfig = route.provider.responsesItemIdRepair;
2219
+ const snapshotRepairEnabled = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair);
2220
+ const githubCopilotRepairEnabled = route.providerName === "github-copilot";
2221
+ const responseModelRewrite = parsed._responseModelId !== undefined
2222
+ && parsed._responseModelId !== parsed.modelId
2223
+ ? createResponsesModelPayloadRewrite(parsed._responseModelId)
2224
+ : undefined;
2225
+ // Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
2226
+ const payloadRewrites = [
2227
+ createImageGenCallRestoreRewrite(imageGenCallAliases),
2228
+ hasResponsesItemIdRepair(repairConfig)
2229
+ ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
2230
+ : undefined,
2231
+ responseModelRewrite,
2232
+ ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
2233
+ // #893: sparse-snapshot gateways get field backfills AND lifecycle event
2234
+ // injection at the block level, after payload rewrites. Defaults come
2235
+ // from the finalized OUTBOUND body — the normalized internal tool shapes
2236
+ // are not the Responses wire shapes the snapshot must mirror.
2237
+ const snapshotDefaultsRequest = (() => {
2238
+ try {
2239
+ return JSON.parse(request.body) as unknown;
2240
+ } catch {
2241
+ return undefined;
2242
+ }
2243
+ })();
2244
+ const blockRewrites = [
2245
+ payloadRewrites.length > 0
2246
+ ? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
2247
+ : undefined,
2248
+ githubCopilotRepairEnabled
2249
+ ? createGithubCopilotResponsesBlockRewrite(translatorBudget)
2250
+ : undefined,
2251
+ snapshotRepairEnabled
2252
+ ? createResponsesSnapshotBlockRewrite(snapshotDefaultsRequest, translatorBudget)
2253
+ : undefined,
2254
+ ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
2255
+ const clientBlockRewrite = blockRewrites.length > 0
2256
+ ? composeSseBlockRewrites(...blockRewrites)
2257
+ : undefined;
2258
+ const needsClientRewrite = clientBlockRewrite !== undefined;
2259
+ // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
2260
+ // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
2261
+ // lost). The eager single reader applies the same rewrites inline.
2262
+ const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
2263
+ const eagerPath = selectEagerPath(
2264
+ process.platform,
2265
+ needsClientRewrite,
2266
+ config.streamMode ?? "auto",
2267
+ );
2268
+ const inlineEagerRewrite = needsClientRewrite
2269
+ && (win32EagerRewrite || eagerPath?.useEagerRelay === true);
2270
+ if (eagerPath?.useEagerRelay || win32EagerRewrite) {
2271
+ const turnAc = new AbortController();
2272
+ linkAbortSignal(upstream, turnAc.signal);
2273
+ registerTurn(turnAc, options.turnAdmissionLease);
2274
+ const reportNativeTerminal = recordTerminalOutcomes
2275
+ ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
2276
+ terminalRecorder?.(status, httpStatusOverride);
2277
+ if (status === "failed") {
2278
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
2279
+ || logCtx.terminalHttpStatus === 429
2280
+ || logCtx.terminalHttpStatus === 402
2281
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
2282
+ : undefined;
2283
+ if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
2284
+ recordSubagentQuotaFailureForThreadSpawn(
2285
+ req.headers,
2286
+ subagentQuotaFailureModel,
2287
+ quotaFailureMessage,
2288
+ config,
2289
+ subagentFallbackAccountId,
2290
+ );
2291
+ }
2292
+ }
2293
+ options.onNativePassthroughTerminal?.(status);
2294
+ }
2295
+ : undefined;
2296
+ const inspector = createSseInspector({
2297
+ onTerminal: reportNativeTerminal,
2298
+ logCtx,
2299
+ onCompletedResponse: rememberPassthroughResponse,
2300
+ onFirstOutput: options.onFirstOutput,
2301
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
2302
+ });
2303
+ const eagerBody = relaySseEagerBounded(upstreamResponse.body, turnAc, {
2304
+ inspectChunk: chunk => inspector.feed(chunk),
2305
+ finishInspection: () => inspector.finish(),
2306
+ disposeInspection: () => inspector.dispose(),
2307
+ // Stream lifetime follows the protocol terminal even when this request
2308
+ // has no outcome callback configured (reported() would stay false).
2309
+ sawTerminal: () => inspector.terminalSeen(),
2310
+ ...(clientBlockRewrite
2311
+ ? { rewriteBlocks: clientBlockRewrite }
2312
+ : {}),
2313
+ onSynthetic: kind => {
2314
+ if (!reportNativeTerminal) return;
2315
+ if (kind === "incomplete") {
2316
+ logCtx.terminalSource = "synthetic";
2317
+ reportNativeTerminal("incomplete");
2318
+ } else {
2319
+ logCtx.transportPhase = "mid_stream";
2320
+ logCtx.terminalSource = "synthetic";
2321
+ reportNativeTerminal("failed", 502);
2322
+ }
2323
+ },
2324
+ onClientCancel: () => options.onNativePassthroughCancel?.(),
2325
+ onDone: () => unregisterTurn(turnAc),
2326
+ }, inlineEagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
2327
+ // When selected, this relay closes response.completed even if upstream
2328
+ // keeps the connection alive. Windows forced-rewrite traffic and Darwin
2329
+ // explicit eager traffic apply client rewrites inline rather than via
2330
+ // the tee()+JS-pull chain.
2331
+ if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
2332
+ return markEagerRelaySseResponse(
2333
+ markNativePassthroughSseResponse(new Response(eagerBody, {
2334
+ status: upstreamResponse.status,
2335
+ headers,
2336
+ })),
2337
+ );
2338
+ }
2339
+ const [nativeBody, inspectBody] = upstreamResponse.body.tee();
2340
+ const turnAc = new AbortController();
2341
+ const clientGone = new AbortController();
2342
+ linkAbortSignal(upstream, turnAc.signal);
2343
+ registerTurn(turnAc, options.turnAdmissionLease);
2344
+ const inspectionConsumerOptions = {
2345
+ clientGoneSignal: clientGone.signal,
2346
+ drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
2347
+ upstream,
2348
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
2349
+ };
2350
+ if (recordTerminalOutcomes) {
2351
+ // A real terminal was parsed from the (teed) inspection stream — record it as the outcome
2352
+ // even if the client has already disconnected: the turn genuinely reached that terminal, so
2353
+ // it must log as completed/failed, not be dropped or downgraded to a cancel (#44). A pure
2354
+ // client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
2355
+ const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
2356
+ terminalRecorder?.(status, httpStatusOverride);
2357
+ if (status === "failed") {
2358
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
2359
+ || logCtx.terminalHttpStatus === 429
2360
+ || logCtx.terminalHttpStatus === 402
2361
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
2362
+ : undefined;
2363
+ if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
2364
+ recordSubagentQuotaFailureForThreadSpawn(
2365
+ req.headers,
2366
+ subagentQuotaFailureModel,
2367
+ quotaFailureMessage,
2368
+ config,
2369
+ subagentFallbackAccountId,
2370
+ );
2371
+ }
2372
+ }
2373
+ options.onNativePassthroughTerminal?.(status);
2374
+ };
2375
+ consumeForInspection(
2376
+ inspectBody,
2377
+ reportNativeTerminal,
2378
+ turnAc.signal,
2379
+ () => unregisterTurn(turnAc),
2380
+ logCtx,
2381
+ () => options.onNativePassthroughCancel?.(),
2382
+ rememberPassthroughResponse,
2383
+ options.onFirstOutput,
2384
+ inspectionConsumerOptions,
2385
+ );
2386
+ } else {
2387
+ consumeForResponseLogMetadata(
2388
+ inspectBody,
2389
+ logCtx,
2390
+ turnAc.signal,
2391
+ () => unregisterTurn(turnAc),
2392
+ rememberPassthroughResponse,
2393
+ options.onFirstOutput,
2394
+ inspectionConsumerOptions,
2395
+ );
2396
+ }
2397
+ if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
2398
+ // Windows was handled by the eager terminal-aware branch above. Remaining
2399
+ // tee traffic can use the JS relay to close on a protocol terminal and to
2400
+ // convert a mid-stream reset into a clean response.failed event.
2401
+ const rewrittenBody = clientBlockRewrite !== undefined
2402
+ ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget)
2403
+ : nativeBody;
2404
+ const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason));
2405
+ return markNativePassthroughSseResponse(new Response(clientBody, {
2406
+ status: upstreamResponse.status,
2407
+ headers,
2408
+ }));
2409
+ }
2410
+ if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
2411
+ // Bounded whole-body read: a non-streaming upstream JSON body is fully materialized
2412
+ // here (and again by the request-log finalizer and the WebSocket bridge's reframing),
2413
+ // so an unbounded .text() would let a hostile or stuck upstream grow proxy memory
2414
+ // without limit. This path is no longer rare — WebSocket turns for models whose
2415
+ // streaming terminal event is unreliable are deliberately answered with bounded JSON.
2416
+ // Oversize and stall deadlines both fail closed; a partial body is never parsed.
2417
+ const bounded = await readBoundedResponseBody(upstreamResponse, UPSTREAM_JSON_BODY_READ_OPTIONS);
2418
+ if (bounded.oversized) {
2419
+ return formatErrorResponse(502, "upstream_error", "upstream JSON response exceeded the safe body limit");
2420
+ }
2421
+ if (bounded.truncated) {
2422
+ return formatErrorResponse(502, "upstream_error", "upstream JSON response stalled before completing");
2423
+ }
2424
+ const text = bounded.text;
2425
+ inspectResponseLogJson(logCtx, text);
2426
+ if (rememberPassthroughResponse) {
2427
+ try {
2428
+ rememberPassthroughResponse(JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown });
2429
+ } catch { /* non-JSON despite content-type; recording is best-effort */ }
2430
+ }
2431
+ const clientJson = (() => {
2432
+ const restored = restoreImageGenCallsInJson(text, imageGenCallAliases);
2433
+ const repaired = (() => {
2434
+ if (!hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)) return restored;
2435
+ let outbound: unknown;
2436
+ try {
2437
+ outbound = JSON.parse(request.body);
2438
+ } catch {
2439
+ outbound = undefined;
2440
+ }
2441
+ return repairResponsesSnapshotJson(restored, outbound);
2442
+ })();
2443
+ return parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
2444
+ ? rewriteResponsesModelJson(repaired, parsed._responseModelId)
2445
+ : repaired;
2446
+ })();
2447
+ // #875: the transport-neutral reliability policy forced a bounded JSON
2448
+ // upstream for a client that asked for SSE. Reframe the completed JSON
2449
+ // as the canonical terminal SSE sequence (created → output_item.done →
2450
+ // terminal → [DONE]) so Codex commits the turn instead of hanging on a
2451
+ // stream that never closes. Non-streaming clients keep the plain JSON.
2452
+ if (clientRequestedStream === true
2453
+ && options.inboundTransport !== "websocket"
2454
+ && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
2455
+ && route.provider.adapter === "openai-responses") {
2456
+ try {
2457
+ let completed = JSON.parse(clientJson) as Record<string, unknown>;
2458
+ // The bounded-JSON answer bypasses the SSE relay, so it also bypasses
2459
+ // the SSE item-id rewrite. Apply the same client-facing normalization
2460
+ // here or this policy would silently disable id repair for the very
2461
+ // providers that need it (raw record already happened above).
2462
+ if (hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)) {
2463
+ completed = repairResponsesJsonItemIds(completed, route.provider.responsesItemIdRepair!, translatorBudget);
2464
+ }
2465
+ const sseHeaders = sanitizePassthroughHeaders(headers);
2466
+ sseHeaders.set("content-type", "text/event-stream");
2467
+ sseHeaders.set("cache-control", "no-store");
2468
+ return new Response(responsesJsonToSseBody(completed), {
2469
+ status: upstreamResponse.status,
2470
+ statusText: upstreamResponse.statusText,
2471
+ headers: sseHeaders,
2472
+ });
2473
+ } catch {
2474
+ // Non-JSON despite content-type: fall through to the plain relay.
2475
+ }
2476
+ }
2477
+ // WS turns reframe this JSON into events in the bridge, which is the
2478
+ // other relay-free path — normalize ids so both bounded-JSON paths agree.
2479
+ const outboundJson = options.inboundTransport === "websocket"
2480
+ && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
2481
+ && hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
2482
+ ? (() => {
2483
+ try {
2484
+ return JSON.stringify(repairResponsesJsonItemIds(
2485
+ JSON.parse(clientJson) as Record<string, unknown>,
2486
+ route.provider.responsesItemIdRepair!,
2487
+ translatorBudget,
2488
+ ));
2489
+ } catch {
2490
+ return clientJson;
2491
+ }
2492
+ })()
2493
+ : clientJson;
2494
+ return new Response(outboundJson, {
2495
+ status: upstreamResponse.status,
2496
+ statusText: upstreamResponse.statusText,
2497
+ headers,
2498
+ });
2499
+ }
2500
+ const body = relayWithAbort(upstreamResponse.body, upstream);
2501
+ const turnAc = new AbortController();
2502
+ const tracked = body ? trackStreamLifetime(body, turnAc, undefined, options.turnAdmissionLease) : null;
2503
+ return new Response(tracked, {
2504
+ status: upstreamResponse.status,
2505
+ headers,
2506
+ });
2507
+ } finally {
2508
+ if (hostAdmissionLease) {
2509
+ releaseUpstreamHostAdmission(hostAdmissionLease);
2510
+ releaseCodexAuthContextProbeLease(authCtx);
2511
+ }
2512
+ }
2513
+ }
2514
+
2515
+ // Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
2516
+ // Routed-compaction turns must NOT hit the image bridge: compaction clears tools/_webSearch but
2517
+ // leaves _imageGeneration, so planImageBridge would activate and return a normal Responses
2518
+ // completion instead of the synthetic compaction item Codex expects (#424).
2519
+ //
2520
+ // Web-search's loop only supports buildRequest/fetch/parseStream — NOT adapter.runTurn. Sending
2521
+ // Cursor/runTurn requests into runWithWebSearch produces empty HTTP failures. So:
2522
+ // - non-runTurn: web-search wins over image when both eligible (documented priority)
2523
+ // - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
2524
+ // can proceed for web-search-only turns
2525
+ const wsPlan = !routedCompaction
2526
+ ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar)
2527
+ : undefined;
2528
+ const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
2529
+ const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
2530
+ const canRunWebSearch = !!wsPlan && !adapter.runTurn;
2531
+ if ((imgPlan || vidPlan) && canRunWebSearch) {
2532
+ // Web search takes priority when both are active — the media bridge cannot run
2533
+ // alongside runWithWebSearch. Surface a runtime signal so the user knows their
2534
+ // configured video/image bridge was skipped for this turn, rather than silently
2535
+ // dropping a paid capability.
2536
+ if (vidPlan) console.warn("[videos] video bridge skipped: web search is active for this turn");
2537
+ if (imgPlan) console.warn("[images] image bridge skipped: web search is active for this turn");
2538
+ }
2539
+ if ((imgPlan || vidPlan) && (!wsPlan || adapter.runTurn)) {
2540
+ // The image bridge detects a hosted image_generation tool and requires streaming.
2541
+ // The video bridge activates from config and injects a tool — it also needs streaming
2542
+ // (the loop returns SSE). For video-only (no imgPlan) on a non-streaming request, skip
2543
+ // the bridge entirely so enabling the feature doesn't break ordinary non-streaming traffic.
2544
+ if (!parsed.stream) {
2545
+ if (imgPlan) {
2546
+ return formatErrorResponse(400, "invalid_request_error", "image bridge requires stream=true");
2547
+ }
2548
+ // Video-only: skip bridge for non-streaming requests
2549
+ } else {
2550
+ // Replace any pre-existing image_gen/video_gen aliases instead of appending duplicate wire names.
2551
+ const priorTools = parsed.context.tools ?? [];
2552
+ const bridgeTools = [...priorTools.filter(t => {
2553
+ if (t.imageGeneration) return false;
2554
+ if (t.videoGeneration) return false;
2555
+ if (imgPlan && imgPlan.toolNames.has(t.name)) return false;
2556
+ if (imgPlan && t.namespace && imgPlan.toolNames.has(namespacedToolName(t.namespace, t.name))) return false;
2557
+ // Only strip unnamespaced video_gen aliases — a namespaced MCP video_gen is left alone.
2558
+ if (vidPlan && !t.namespace && vidPlan.toolNames.has(t.name)) return false;
2559
+ return true;
2560
+ })];
2561
+ const existingNames = new Set(bridgeTools.map(t => t.name));
2562
+ if (imgPlan && !existingNames.has(IMAGE_GEN_TOOL_NAME)) bridgeTools.push(buildImageTool());
2563
+ if (vidPlan && !existingNames.has(VIDEO_GEN_TOOL_NAME)) bridgeTools.push(buildVideoTool());
2564
+ parsed.context.tools = bridgeTools;
2565
+ // Hosted image_generation tool_choice / allowed_tools must target the synthetic function name.
2566
+ // Gate on imgPlan — in a video-only turn buildImageTool() was never injected, so rewriting
2567
+ // image_generation/image_gen aliases would add an undeclared tool that strict upstreams reject.
2568
+ const tc = parsed.options.toolChoice;
2569
+ if (imgPlan && tc && typeof tc === "object" && "allowedTools" in tc && Array.isArray(tc.allowedTools)) {
2570
+ const mapped = tc.allowedTools.map(name =>
2571
+ name === "image_generation" || name === "image_gen" || (imgPlan.toolNames.has(name) ?? false)
2572
+ ? IMAGE_GEN_TOOL_NAME
2573
+ : name,
2574
+ );
2575
+ parsed.options.toolChoice = { ...tc, allowedTools: [...new Set(mapped)] };
2576
+ } else if (imgPlan && tc && typeof tc === "object" && "name" in tc && typeof tc.name === "string"
2577
+ && (tc.name === "image_generation" || imgPlan.toolNames.has(tc.name))) {
2578
+ parsed.options.toolChoice = { ...tc, name: IMAGE_GEN_TOOL_NAME };
2579
+ }
2580
+ const imgResponse = await runWithImageBridge({
2581
+ parsed, adapter,
2582
+ incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
2583
+ ...(imgPlan ? { plan: imgPlan } : {}),
2584
+ ...(vidPlan ? { videoPlan: vidPlan } : {}),
2585
+ forwardHeaders: selectedForwardHeaders,
2586
+ onAttemptSend: (recovery?: AttemptRecoveryKind) =>
2587
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery),
2588
+ abortSignal: options.abortSignal,
2589
+ maxRounds: imgPlan && vidPlan
2590
+ ? clampImageMaxRounds(Math.min(config.images?.maxRounds ?? 3, config.images?.videoMaxRounds ?? 2))
2591
+ : imgPlan
2592
+ ? clampImageMaxRounds(config.images?.maxRounds)
2593
+ : clampImageMaxRounds(config.images?.videoMaxRounds ?? 2),
2594
+ connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
2595
+ stallTimeoutSec: config.stallTimeoutSec,
2596
+ fetchImpl: providerFetch(route.provider),
2597
+ onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
2598
+ ...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
2599
+ onUsage: usage => {
2600
+ // Cursor may assign _cursorConversationId inside the image loop's first runTurn;
2601
+ // backfill so Logs can filter/total that opening request (parity with the normal
2602
+ // runTurn branch).
2603
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
2604
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
2605
+ }
2606
+ logCtx.usageFromBridge = true;
2607
+ if (usage) {
2608
+ logCtx.usage = usage;
2609
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2610
+ }
2611
+ },
2612
+ on429: retryAfter => {
2613
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
2614
+ retryAfter,
2615
+ now: Date.now(),
2616
+ attemptedKey: route.provider.apiKey,
2617
+ promptCacheKey: parsed.options.promptCacheKey,
2618
+ });
2619
+ if (!rotated) return null;
2620
+ route.provider = rotated;
2621
+ return resolveAdapter(
2622
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2623
+ config.cacheRetention,
2624
+ );
2625
+ },
2626
+ retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
2627
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2628
+ ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
2629
+ onCompletedResponse: (response, providerState) =>
2630
+ rememberResponseState(
2631
+ parsed._rawBody,
2632
+ response,
2633
+ continuationStateForResponse(providerState),
2634
+ adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2635
+ ),
2636
+ });
2637
+ if (imgResponse.body) {
2638
+ const imgTurnAc = new AbortController();
2639
+ return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, undefined, options.turnAdmissionLease), {
2640
+ status: imgResponse.status,
2641
+ headers: imgResponse.headers,
2642
+ });
2643
+ }
2644
+ return imgResponse;
2645
+ } // end else (streaming bridge)
2646
+ }
2647
+
2648
+ // Web-search sidecar: Codex enabled web_search but this is a routed (non-OpenAI) model that can't
2649
+ // run it server-side. Expose web_search as a function tool and run searches via the gpt-mini sidecar
2650
+ // through the ChatGPT passthrough, looping until the model answers. Otherwise take the normal path.
2651
+ // Placed BEFORE the runTurn early-return for non-runTurn adapters so dual-tool turns dispatch
2652
+ // through web-search instead of being swallowed. runTurn adapters never enter this branch.
2653
+ if (canRunWebSearch && wsPlan) {
2654
+ parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
2655
+ const wsResponse = await runWithWebSearch({
2656
+ parsed, adapter,
2657
+ incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
2658
+ backend: wsPlan.backend,
2659
+ forwardProvider: wsPlan.forwardSidecar?.provider,
2660
+ anthropicSidecar: wsPlan.anthropicSidecar,
2661
+ hostedTool: wsPlan.hostedTool,
2662
+ selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? selectedForwardHeaders,
2663
+ settings: wsPlan.settings,
2664
+ maxSearches: wsPlan.maxSearches,
2665
+ forceEmptyResponseId: true,
2666
+ abortSignal: options.abortSignal,
2667
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2668
+ onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
2669
+ onAttemptSend: (recovery?: AttemptRecoveryKind) =>
2670
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery),
2671
+ onUsage: usage => {
2672
+ logCtx.usageFromBridge = true;
2673
+ if (usage) {
2674
+ logCtx.usage = usage;
2675
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2676
+ }
2677
+ },
2678
+ recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
2679
+ connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
2680
+ routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
2681
+ stallTimeoutSec: wsPlan.stallTimeoutSec,
2682
+ on429: retryAfter => {
2683
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
2684
+ retryAfter,
2685
+ now: Date.now(),
2686
+ attemptedKey: route.provider.apiKey,
2687
+ promptCacheKey: parsed.options.promptCacheKey,
2688
+ });
2689
+ if (!rotated) return null;
2690
+ route.provider = rotated;
2691
+ return resolveAdapter(
2692
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2693
+ config.cacheRetention,
2694
+ );
2695
+ },
2696
+ retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
2697
+ });
2698
+ // Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
2699
+ // in-flight web-search turns instead of skipping them during graceful shutdown.
2700
+ if (wsResponse.body) {
2701
+ const wsTurnAc = new AbortController();
2702
+ return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, undefined, options.turnAdmissionLease), {
2703
+ status: wsResponse.status,
2704
+ headers: wsResponse.headers,
2705
+ });
2706
+ }
2707
+ return wsResponse;
2708
+ }
2709
+
2710
+ if (adapter.runTurn) {
2711
+ const runTurnAbort = new AbortController();
2712
+ linkAbortSignal(runTurnAbort, options.abortSignal);
2713
+ const queue = createAdapterEventQueue({
2714
+ onBacklogExceeded: () => runTurnAbort.abort(),
2715
+ });
2716
+ const runTurn = async (): Promise<void> => {
2717
+ try {
2718
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
2719
+ await adapter.runTurn?.(
2720
+ parsed,
2721
+ { headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal, translatorBudget },
2722
+ queue.push,
2723
+ );
2724
+ } catch (err) {
2725
+ queue.push({
2726
+ type: "error",
2727
+ message: err instanceof Error ? err.message : String(err),
2728
+ });
2729
+ } finally {
2730
+ // Cursor assigns a stable conversation id inside runTurn on the first headerless
2731
+ // turn; backfill so Logs can filter/total that opening request (#330 / #522).
2732
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
2733
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
2734
+ }
2735
+ queue.close();
2736
+ }
2737
+ };
2738
+
2739
+ const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
2740
+ if (parsed.stream) {
2741
+ void runTurn();
2742
+ let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
2743
+ if (options.comboAttempt) {
2744
+ const preflight = await preflightAdapterEvents(eventSource);
2745
+ if (preflight.error || preflight.empty) {
2746
+ runTurnAbort.abort();
2747
+ queue.close();
2748
+ const message = preflight.error?.message ?? "Adapter ended before producing a response";
2749
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
2750
+ }
2751
+ eventSource = preflight.stream;
2752
+ }
2753
+ const sseStream = bridgeToResponsesSSE(
2754
+ eventSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
2755
+ () => {
2756
+ runTurnAbort.abort();
2757
+ queue.close();
2758
+ }, 2_000,
2759
+ {
2760
+ translatorBudget,
2761
+ replayCacheScope: parsed._clientThreadId ?? "global",
2762
+ ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
2763
+ stallTimeoutSec: config.stallTimeoutSec,
2764
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
2765
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2766
+ ...(routedCompaction ? { compaction: true } : {}),
2767
+ onUsage: usage => {
2768
+ // Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
2769
+ // zero-default detail objects, so provenance must come from here (cache_detail_missing).
2770
+ logCtx.usageFromBridge = true;
2771
+ if (usage) {
2772
+ logCtx.usage = usage;
2773
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2774
+ }
2775
+ },
2776
+ ...(routedCompaction ? {} : {
2777
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
2778
+ rememberResponseState(
2779
+ parsed._rawBody,
2780
+ response,
2781
+ continuationStateForResponse(providerState),
2782
+ adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2783
+ ),
2784
+ }),
2785
+ },
2786
+ );
2787
+ const bridgeTurnAc = new AbortController();
2788
+ const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
2789
+ return new Response(trackedSse, {
2790
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
2791
+ });
2792
+ }
2793
+
2794
+ await runTurn();
2795
+ const events = await queue.collect();
2796
+ if (options.comboAttempt) {
2797
+ const firstMeaningful = events.find(event => event.type !== "heartbeat");
2798
+ if (!firstMeaningful || firstMeaningful.type === "error") {
2799
+ const message = firstMeaningful?.type === "error"
2800
+ ? firstMeaningful.message
2801
+ : "Adapter ended before producing a response";
2802
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
2803
+ }
2804
+ }
2805
+ let providerState: OcxProviderContinuationState | undefined;
2806
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
2807
+ translatorBudget,
2808
+ replayCacheScope: parsed._clientThreadId ?? "global",
2809
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
2810
+ toolNsMap,
2811
+ freeformToolNames,
2812
+ toolSearchToolNames,
2813
+ ...(routedCompaction ? { compaction: true } : {}),
2814
+ onProviderState: state => { providerState = state; },
2815
+ onUsage: usage => {
2816
+ logCtx.usageFromBridge = true;
2817
+ if (usage) {
2818
+ logCtx.usage = usage;
2819
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2820
+ }
2821
+ },
2822
+ });
2823
+ if (!routedCompaction) {
2824
+ rememberResponseState(
2825
+ parsed._rawBody,
2826
+ json,
2827
+ continuationStateForResponse(providerState),
2828
+ adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2829
+ );
2830
+ }
2831
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
2832
+ }
2833
+
2834
+ const upstream = new AbortController();
2835
+ const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
2836
+ const connectMs = config.connectTimeoutMs ?? 200_000;
2837
+ // Bridge stall budget (seconds of silence before upstream_stall_timeout); the retry backoff
2838
+ // heartbeat interval is derived from it so the watchdog is always fed during deliberate waits.
2839
+ const stallTimeoutMs = typeof config.stallTimeoutSec === "number" && Number.isFinite(config.stallTimeoutSec) && config.stallTimeoutSec > 0
2840
+ ? Math.floor(config.stallTimeoutSec * 1000)
2841
+ : 300_000;
2842
+ let activeAdapter = adapter;
2843
+
2844
+ // One immutable, body-safe outbound request per same-target sequence (URL, serialized body,
2845
+ // auth headers, generated compat headers). Same-target 429 replays reuse it verbatim; the
2846
+ // builder runs again only after a key/account/adapter rotation, an oauth refresh, or an
2847
+ // image-tier bias change (transportToken bump). `body` is always a serialized string, so
2848
+ // reuse is safe, and releaseBodyObservation is idempotent per build.
2849
+ let initialRequest: AdapterRequest | undefined;
2850
+ let inputTokenEstimate: number | undefined;
2851
+ try {
2852
+ initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
2853
+ recordAdapterReasoning(logCtx, initialRequest);
2854
+ inputTokenEstimate = typeof initialRequest.usageLog?.inputTokens === "number"
2855
+ ? initialRequest.usageLog.inputTokens
2856
+ : undefined;
2857
+ if (inputTokenEstimate !== undefined) logCtx.usageLogInputTokens = inputTokenEstimate;
2858
+ } catch (err) {
2859
+ // A throwing buildRequest never returned a request; if a post-build step threw, release
2860
+ // the serialized-body observation (idempotent) so the translator budget is not leaked.
2861
+ // The build runs after linkAbortSignal, so a failure must also tear the link down and
2862
+ // abort the upstream controller instead of escaping handleResponses unmapped.
2863
+ initialRequest?.releaseBodyObservation?.();
2864
+ cleanupUpstreamAbort();
2865
+ upstream.abort();
2866
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
2867
+ const msg = err instanceof Error ? err.message : String(err);
2868
+ return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
2869
+ }
2870
+ // The catch path above always returns, so the request is definitely assigned here.
2871
+ // Capture it in a const so the fetch callbacks read a narrowed, immutable value
2872
+ // (TypeScript drops narrowing for a `let` captured by a nested function).
2873
+ const builtInitialRequest = initialRequest;
2874
+ let sameTargetRequest: AdapterRequest | undefined = builtInitialRequest;
2875
+ let sameTargetParsed: OcxParsedRequest | undefined = parsed;
2876
+ let sameTargetToken = 0;
2877
+ let transportToken = 0;
2878
+ /**
2879
+ * Invalidate the same-target request cache. Every credential/adapter/parsed mutation MUST
2880
+ * go through here: the cache keys on `parsed` REFERENCE identity, so an in-place mutation
2881
+ * is invisible to it and a missed bump would replay a request built with a stale key.
2882
+ */
2883
+ const invalidateSameTargetRequest = (): void => { transportToken += 1; };
2884
+ let upstreamResponse: Response;
2885
+ try {
2886
+ if (activeAdapter.fetchResponse) {
2887
+ noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
2888
+ upstreamResponse = await activeAdapter.fetchResponse(builtInitialRequest, {
2889
+ abortSignal: upstream.signal,
2890
+ timeoutMs: connectMs,
2891
+ stream: parsed.stream,
2892
+ });
2893
+ } else {
2894
+ upstreamResponse = await fetchWithTransientNonJsonRetry(
2895
+ recovery => {
2896
+ noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery);
2897
+ return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
2898
+ method: builtInitialRequest.method,
2899
+ headers: builtInitialRequest.headers,
2900
+ body: builtInitialRequest.body,
2901
+ }, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
2902
+ },
2903
+ {
2904
+ abortSignal: upstream.signal,
2905
+ label: safeHostLabel(builtInitialRequest.url),
2906
+ statuses: providerPreStreamNonJsonRetryStatuses(route.providerName, route.provider),
2907
+ },
2908
+ );
2909
+ }
2910
+ } catch (err) {
2911
+ cleanupUpstreamAbort();
2912
+ upstream.abort();
2913
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
2914
+ const msg = describeUpstreamConnectFailure(err, connectMs);
2915
+ return formatErrorResponse(502, "upstream_error", msg);
2916
+ } finally {
2917
+ builtInitialRequest.releaseBodyObservation?.();
2918
+ }
2919
+
2920
+ // Same-target 429 retry budget is per REQUEST: it lives OUTSIDE the recovery loop (so a 413/401
2921
+ // replay that comes back 429 cannot silently re-arm a fresh budget) and is SHARED with the
2922
+ // terminal-guard continuation below, so the main loop + one continuation can never exceed
2923
+ // `attempts` same-key replays in total (bounded per request).
2924
+ const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
2925
+ let rateLimitRetries = 0;
2926
+ // Shared with the terminal-guard continuation below: an image-tier reduction that let the
2927
+ // main request clear a 413 must not be forgotten on the very next continuation build.
2928
+ let imageTierBias = 0;
2929
+ if (!upstreamResponse.ok) {
2930
+ // Recovery loop: multi-key 429 failover + at most ONE anthropic 413 tightened retry
2931
+ // (devlog/260714_image_normalization_pipeline/030). One mutable activeAdapter serves
2932
+ // both paths so a 429→413 sequence never rebuilds against a stale pre-rotation
2933
+ // adapter, and imageTierBias — once armed — rides EVERY subsequent rebuild so a
2934
+ // 413→429 rotation cannot silently undo the tightening.
2935
+ let imageRetryAttempted = false;
2936
+ let oauth401ReplayAttempted = false;
2937
+ /**
2938
+ * Rebuild the request from the current parsed input (and any image-tier bias) and refetch
2939
+ * it once, tagging the attempt with the given recovery kind. Rebuilds are deterministic
2940
+ * for the same parsed request, so same-target replays stay byte-identical.
2941
+ */
2942
+ const rebuildAndRefetch = async (
2943
+ recovery: AttemptRecoveryKind,
2944
+ ): Promise<Response | { failed: Response }> => {
2945
+ let retryRequest: AdapterRequest;
2946
+ if (sameTargetRequest !== undefined && sameTargetParsed === parsed && sameTargetToken === transportToken) {
2947
+ // Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
2948
+ retryRequest = sameTargetRequest;
2949
+ } else {
2950
+ try {
2951
+ retryRequest = await activeAdapter.buildRequest(parsed, {
2952
+ headers: selectedForwardHeaders,
2953
+ translatorBudget,
2954
+ ...(imageTierBias > 0 ? { imageTierBias } : {}),
2955
+ });
2956
+ recordAdapterReasoning(logCtx, retryRequest);
2957
+ } catch (err) {
2958
+ // A rotated/rebuilt adapter build failure is a request-shaping error, not an
2959
+ // upstream connect failure: tear the abort link down and map it as 400 (no 413
2960
+ // translator-budget mapping here — that stays with parseRequest/buildToolBridgeMaps).
2961
+ cleanupUpstreamAbort();
2962
+ upstream.abort();
2963
+ if (options.abortSignal?.aborted) return { failed: clientCancelledResponse() };
2964
+ const msg = err instanceof Error ? err.message : String(err);
2965
+ return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(msg)) };
2966
+ }
2967
+ sameTargetRequest = retryRequest;
2968
+ sameTargetParsed = parsed;
2969
+ sameTargetToken = transportToken;
2970
+ }
2971
+ const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
2972
+ ? retryRequest.usageLog.inputTokens
2973
+ : undefined;
2974
+ if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
2975
+ logCtx.providerAdapter = activeAdapter.name;
2976
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
2977
+ noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
2978
+ try {
2979
+ try {
2980
+ return activeAdapter.fetchResponse
2981
+ ? await activeAdapter.fetchResponse(retryRequest, { abortSignal: upstream.signal, timeoutMs: connectMs, stream: parsed.stream })
2982
+ : await fetchWithHeaderTimeout(retryRequest.url, {
2983
+ method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
2984
+ }, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
2985
+ } finally {
2986
+ retryRequest.releaseBodyObservation?.();
2987
+ }
2988
+ } catch (err) {
2989
+ cleanupUpstreamAbort();
2990
+ upstream.abort();
2991
+ if (options.abortSignal?.aborted) {
2992
+ return { failed: clientCancelledResponse() };
2993
+ }
2994
+ const msg = describeUpstreamConnectFailure(err, connectMs);
2995
+ return { failed: formatErrorResponse(502, "upstream_error", msg) };
2996
+ }
2997
+ };
2998
+ recovery: for (;;) {
2999
+ if (
3000
+ upstreamResponse.status === 401
3001
+ && isOAuth401ReplayProvider
3002
+ && sentOAuthSnapshot
3003
+ && !oauth401ReplayAttempted
3004
+ ) {
3005
+ oauth401ReplayAttempted = true;
3006
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
3007
+ let refreshed: OAuthAccessSnapshot;
3008
+ try {
3009
+ refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
3010
+ } catch (err) {
3011
+ cleanupUpstreamAbort();
3012
+ return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
3013
+ }
3014
+ sentOAuthSnapshot = refreshed;
3015
+ if (route.providerName === "kiro") {
3016
+ parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
3017
+ }
3018
+ const refreshedProvider = resolveProviderTransport(
3019
+ route.providerName,
3020
+ { ...route.provider, apiKey: refreshed.accessToken },
3021
+ parsed.options.promptCacheKey,
3022
+ route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
3023
+ );
3024
+ route.provider = refreshedProvider;
3025
+ invalidateSameTargetRequest();
3026
+ activeAdapter = resolveAdapter(
3027
+ resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
3028
+ config.cacheRetention,
3029
+ );
3030
+ const result = await rebuildAndRefetch("oauth-401");
3031
+ if ("failed" in result) return result.failed;
3032
+ upstreamResponse = result;
3033
+ continue recovery;
3034
+ }
3035
+
3036
+ // Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
3037
+ // 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
3038
+ // so wait (Retry-After or the fixed interval) and replay the IDENTICAL request on the
3039
+ // same key first. Pre-stream only: a 429 arrives before any bytes are relayed, so the
3040
+ // replay is lossless. Runs before key failover so "primary-first" setups keep the same
3041
+ // key on rate-limit blips; only after the attempts are exhausted does failover run.
3042
+ while (
3043
+ upstreamResponse.status === 429
3044
+ && rateLimitPolicy !== null
3045
+ && rateLimitRetries < rateLimitPolicy.attempts
3046
+ ) {
3047
+ rateLimitRetries += 1;
3048
+ // Release unread body + deliberate wait via the shared same-target helper.
3049
+ const retryAfterHeader = upstreamResponse.headers.get("retry-after");
3050
+ try {
3051
+ for await (const _ of prepareSameTarget429Wait({
3052
+ body: upstreamResponse.body,
3053
+ signal: options.abortSignal,
3054
+ delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
3055
+ })) {
3056
+ // pre-stream: no stall watchdog to feed
3057
+ }
3058
+ } catch {
3059
+ cleanupUpstreamAbort();
3060
+ upstream.abort();
3061
+ return clientCancelledResponse();
3062
+ }
3063
+ // Client cancellation wins over any stale timer edge: re-check before dispatching the
3064
+ // replay so an adapter never starts work for a request the client already abandoned.
3065
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
3066
+ cleanupUpstreamAbort();
3067
+ upstream.abort();
3068
+ return clientCancelledResponse();
3069
+ }
3070
+ const result = await rebuildAndRefetch("rate-limit-429");
3071
+ if ("failed" in result) return result.failed;
3072
+ upstreamResponse = result;
3073
+ }
3074
+
3075
+ // Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
3076
+ // SAME request once per remaining key. OAuth/forward providers and single-key pools
3077
+ // return null immediately, so this stays a no-op for them (src/providers/key-failover.ts).
3078
+ while (upstreamResponse.status === 429 && hasKeyPoolFailover(route.provider)) {
3079
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
3080
+ retryAfter: upstreamResponse.headers.get("retry-after"),
3081
+ now: Date.now(),
3082
+ attemptedKey: route.provider.apiKey,
3083
+ promptCacheKey: parsed.options.promptCacheKey,
3084
+ });
3085
+ if (!rotated) break;
3086
+ // Release the failed response's socket before retrying; unread bodies otherwise linger
3087
+ // until runtime cleanup (one per rotated key under a rate-limit storm).
3088
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
3089
+ route.provider = rotated;
3090
+ invalidateSameTargetRequest();
3091
+ activeAdapter = resolveAdapter(
3092
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
3093
+ config.cacheRetention,
3094
+ );
3095
+ const result = await rebuildAndRefetch("key-429");
3096
+ if ("failed" in result) return result.failed;
3097
+ upstreamResponse = result;
3098
+ }
3099
+
3100
+ // Opt-in Anthropic OAuth account pool (#294): cool the failed account and retry
3101
+ // with another eligible OAuth account (bounded per request). Disabled by default.
3102
+ while (
3103
+ upstreamResponse.status === 429
3104
+ && anthropicPoolAccountId
3105
+ && isAnthropicAccountPoolEnabled(config)
3106
+ && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
3107
+ ) {
3108
+ const nextAccountId = rotateAnthropicAccountOn429(
3109
+ config,
3110
+ anthropicPoolAccountId,
3111
+ upstreamResponse.headers.get("retry-after"),
3112
+ anthropicSessionKey,
3113
+ );
3114
+ if (!nextAccountId) break;
3115
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
3116
+ try {
3117
+ const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
3118
+ anthropicPoolAccountId = nextAccountId;
3119
+ anthropicPoolFailovers += 1;
3120
+ route.provider = { ...route.provider, apiKey: accessToken };
3121
+ invalidateSameTargetRequest();
3122
+ promoteAnthropicActiveAccount(nextAccountId);
3123
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
3124
+ activeAdapter = resolveAdapter(
3125
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
3126
+ config.cacheRetention,
3127
+ );
3128
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
3129
+ const result = await rebuildAndRefetch("anthropic-oauth-429");
3130
+ if ("failed" in result) return result.failed;
3131
+ upstreamResponse = result;
3132
+ } catch {
3133
+ break;
3134
+ }
3135
+ }
3136
+ // Anthropic 413 request_too_large: rebuild once with every image one tier lower
3137
+ // (spiral guard: single attempt). The biased response re-enters the 429 check above.
3138
+ if (shouldAttemptImageTierRetry({
3139
+ status: upstreamResponse.status,
3140
+ adapterName: activeAdapter.name,
3141
+ parsed,
3142
+ alreadyAttempted: imageRetryAttempted,
3143
+ })) {
3144
+ imageRetryAttempted = true;
3145
+ imageTierBias = 1;
3146
+ invalidateSameTargetRequest();
3147
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
3148
+ const result = await rebuildAndRefetch("image-413");
3149
+ if ("failed" in result) return result.failed;
3150
+ upstreamResponse = result;
3151
+ continue recovery;
3152
+ }
3153
+ break;
3154
+ }
3155
+ if (!upstreamResponse.ok) {
3156
+ if (options.comboAttempt) {
3157
+ const failure = await consumeComboFailure(upstreamResponse, options.abortSignal)
3158
+ .finally(cleanupUpstreamAbort);
3159
+ options.onConsumedComboFailure?.(failure);
3160
+ return failure.response;
3161
+ }
3162
+ const errorText = await upstreamResponse.text().catch(() => "unknown error");
3163
+ cleanupUpstreamAbort();
3164
+ if (!isFixedCodexAccount(authCtx)) {
3165
+ recordSubagentQuotaFailureForThreadSpawn(
3166
+ req.headers,
3167
+ subagentQuotaFailureModel,
3168
+ upstreamResponse.status === 429 || upstreamResponse.status === 402
3169
+ ? upstreamResponse.status
3170
+ : `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
3171
+ config,
3172
+ subagentFallbackAccountId,
3173
+ );
3174
+ }
3175
+ // Upstreams occasionally echo request details in error bodies — scrub token-shaped
3176
+ // material before it reaches the client-facing error surface.
3177
+ const message = `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`;
3178
+ const retryAfter = resolveClientRetryAfter({
3179
+ status: upstreamResponse.status,
3180
+ message,
3181
+ upstreamRetryAfter: upstreamResponse.headers.get("retry-after"),
3182
+ });
3183
+ return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
3184
+ ...(retryAfter !== undefined ? { retryAfter } : {}),
3185
+ });
3186
+ }
3187
+ }
3188
+
3189
+ cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
3190
+
3191
+ // Anthropic-only: one bounded internal continuation re-ask for clean end_turn turns that
3192
+ // announced an edit without emitting a tool call.
3193
+ const terminalGuardEnabled = activeAdapter.name === "anthropic" && !options.comboAttempt && !routedCompaction;
3194
+ /**
3195
+ * One bounded internal re-ask for Anthropic end_turn-without-tool-call turns. Replays the
3196
+ * continuation on a 429 with the same-key retry budget (hoisted per request), then falls
3197
+ * back to key/account failover; a failure becomes an in-stream adapter error so the client
3198
+ * never sees a second hidden HTTP response or an unbounded retry loop.
3199
+ */
3200
+ const fetchTerminalGuardContinuation = async function* (nextParsed: OcxParsedRequest): AsyncGenerator<AdapterEvent> {
3201
+ let response: Response | undefined;
3202
+ // One-shot recovery label for the next top-of-loop continuation send after a failover rotation.
3203
+ let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined;
3204
+ /**
3205
+ * Build and fetch one terminal-guard continuation. `recoveryKind` tags same-target and
3206
+ * failover sends (`rate-limit-429`, `key-429`, `anthropic-oauth-429`, `image-413`); the
3207
+ * adapter rebuild is deterministic for the same parsed request (tests assert byte-identical
3208
+ * replays).
3209
+ */
3210
+ const fetchContinuation = async (recoveryKind?: AttemptRecoveryKind): Promise<Response> => {
3211
+ let continuationRequest: AdapterRequest | undefined;
3212
+ if (sameTargetRequest !== undefined && sameTargetParsed === nextParsed && sameTargetToken === transportToken) {
3213
+ // Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
3214
+ continuationRequest = sameTargetRequest;
3215
+ } else {
3216
+ try {
3217
+ continuationRequest = await activeAdapter.buildRequest(nextParsed, {
3218
+ headers: selectedForwardHeaders,
3219
+ translatorBudget,
3220
+ ...(imageTierBias > 0 ? { imageTierBias } : {}),
3221
+ });
3222
+ recordAdapterReasoning(logCtx, continuationRequest);
3223
+ } catch (err) {
3224
+ // The main body is already streaming, so there is no HTTP error surface: release
3225
+ // any partial body observation and surface the failure as an in-stream error via
3226
+ // the outer catch (no upstream.abort() — that would kill the live body stream).
3227
+ continuationRequest?.releaseBodyObservation?.();
3228
+ throw err;
3229
+ }
3230
+ sameTargetRequest = continuationRequest;
3231
+ sameTargetParsed = nextParsed;
3232
+ sameTargetToken = transportToken;
3233
+ }
3234
+ // Both branches assign the request (the build catch rethrows), so capture it in a
3235
+ // const for the fetch callback and finally below — a `let` read inside a nested
3236
+ // function keeps its undefined half, which would break the byte-identical replay.
3237
+ const builtContinuationRequest = continuationRequest;
3238
+ const continuationEstimate = typeof builtContinuationRequest.usageLog?.inputTokens === "number"
3239
+ ? builtContinuationRequest.usageLog.inputTokens
3240
+ : undefined;
3241
+ if (continuationEstimate !== undefined) logCtx.usageLogInputTokens = continuationEstimate;
3242
+ // Optional recovery label for same-target / failover continuation sends.
3243
+ const replayKind: AttemptRecoveryKind | undefined = recoveryKind;
3244
+ try {
3245
+ if (activeAdapter.fetchResponse) {
3246
+ noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replayKind);
3247
+ return await activeAdapter.fetchResponse(builtContinuationRequest, {
3248
+ abortSignal: upstream.signal,
3249
+ timeoutMs: connectMs,
3250
+ stream: nextParsed.stream,
3251
+ });
3252
+ }
3253
+ return await fetchWithResetRetry(
3254
+ recovery => {
3255
+ noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind);
3256
+ return fetchWithHeaderTimeout(
3257
+ builtContinuationRequest.url,
3258
+ applyUpstreamRecoveryInit({
3259
+ method: builtContinuationRequest.method,
3260
+ headers: builtContinuationRequest.headers,
3261
+ body: builtContinuationRequest.body,
3262
+ }, recovery),
3263
+ upstream.signal,
3264
+ connectMs,
3265
+ nextParsed.stream,
3266
+ providerFetch(route.provider),
3267
+ );
3268
+ },
3269
+ { abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url) },
3270
+ );
3271
+ } finally {
3272
+ builtContinuationRequest.releaseBodyObservation?.();
3273
+ }
3274
+ };
3275
+ while (true) {
3276
+ try {
3277
+ const recoveryKind = nextContinuationRecoveryKind;
3278
+ nextContinuationRecoveryKind = undefined;
3279
+ response = await fetchContinuation(recoveryKind);
3280
+ } catch (error) {
3281
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
3282
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
3283
+ } else {
3284
+ yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
3285
+ }
3286
+ return;
3287
+ }
3288
+
3289
+ // Same-target 429 wait-and-retry (opt-in `retryOn429`) before key/account failover:
3290
+ // a primary-key rate-limit blip replays on the SAME key, matching the main recovery
3291
+ // loop; only after the attempts are exhausted does the continuation fail over.
3292
+ while (
3293
+ response.status === 429
3294
+ && rateLimitPolicy !== null
3295
+ && rateLimitRetries < rateLimitPolicy.attempts
3296
+ ) {
3297
+ rateLimitRetries += 1;
3298
+ // Release unread body + heartbeat-fed wait via the shared same-target helper.
3299
+ const retryAfterHeader = response.headers.get("retry-after");
3300
+ try {
3301
+ yield* prepareSameTarget429Wait({
3302
+ body: response.body,
3303
+ // Listen on the upstream signal: once the SSE body is being streamed, a client
3304
+ // cancel aborts `upstream` through the bridge, and upstream is also linked from
3305
+ // options.abortSignal — so this covers both cancellation paths.
3306
+ signal: upstream.signal,
3307
+ delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
3308
+ heartbeatIntervalMs: Math.min(10_000, Math.max(250, stallTimeoutMs / 2)),
3309
+ });
3310
+ } catch {
3311
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
3312
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
3313
+ } else {
3314
+ yield { type: "error", message: "Provider continuation failed: retry wait interrupted" };
3315
+ }
3316
+ return;
3317
+ }
3318
+ // Client cancellation wins over any stale timer edge: re-check before dispatching the
3319
+ // replay so the continuation never starts work for a request the client abandoned.
3320
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
3321
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
3322
+ return;
3323
+ }
3324
+ try {
3325
+ response = await fetchContinuation("rate-limit-429");
3326
+ } catch (error) {
3327
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
3328
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
3329
+ } else {
3330
+ yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
3331
+ }
3332
+ return;
3333
+ }
3334
+ }
3335
+
3336
+ if (response.status === 429 && hasKeyPoolFailover(route.provider)) {
3337
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
3338
+ retryAfter: response.headers.get("retry-after"),
3339
+ now: Date.now(),
3340
+ attemptedKey: route.provider.apiKey,
3341
+ promptCacheKey: nextParsed.options.promptCacheKey,
3342
+ });
3343
+ if (rotated) {
3344
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
3345
+ route.provider = rotated;
3346
+ invalidateSameTargetRequest();
3347
+ activeAdapter = resolveAdapter(
3348
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
3349
+ config.cacheRetention,
3350
+ );
3351
+ nextContinuationRecoveryKind = "key-429";
3352
+ continue;
3353
+ }
3354
+ }
3355
+ if (
3356
+ response.status === 429
3357
+ && anthropicPoolAccountId
3358
+ && isAnthropicAccountPoolEnabled(config)
3359
+ && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
3360
+ ) {
3361
+ const nextAccountId = rotateAnthropicAccountOn429(
3362
+ config,
3363
+ anthropicPoolAccountId,
3364
+ response.headers.get("retry-after"),
3365
+ anthropicSessionKey,
3366
+ );
3367
+ if (nextAccountId) {
3368
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
3369
+ try {
3370
+ const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
3371
+ anthropicPoolAccountId = nextAccountId;
3372
+ anthropicPoolFailovers += 1;
3373
+ route.provider = { ...route.provider, apiKey: accessToken };
3374
+ invalidateSameTargetRequest();
3375
+ promoteAnthropicActiveAccount(nextAccountId);
3376
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
3377
+ activeAdapter = resolveAdapter(
3378
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
3379
+ config.cacheRetention,
3380
+ );
3381
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
3382
+ nextContinuationRecoveryKind = "anthropic-oauth-429";
3383
+ continue;
3384
+ } catch {
3385
+ // fall through to emit continuation error below
3386
+ }
3387
+ }
3388
+ }
3389
+ if (shouldAttemptImageTierRetry({
3390
+ status: response.status,
3391
+ adapterName: activeAdapter.name,
3392
+ parsed: nextParsed,
3393
+ alreadyAttempted: imageTierBias > 0,
3394
+ })) {
3395
+ imageTierBias = 1;
3396
+ invalidateSameTargetRequest();
3397
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
3398
+ nextContinuationRecoveryKind = "image-413";
3399
+ continue;
3400
+ }
3401
+ break;
3402
+ }
3403
+
3404
+ if (!response.ok) {
3405
+ const errorText = await response.text().catch(() => "unknown error");
3406
+ yield {
3407
+ type: "error",
3408
+ status: response.status,
3409
+ message: `Provider continuation error ${response.status}: ${redactSecretString(errorText.slice(0, 500))}`,
3410
+ };
3411
+ return;
3412
+ }
3413
+
3414
+ try {
3415
+ // Protect the continuation body against a client abort landing between fetch resolution and
3416
+ // reader attach, exactly as the initial response is guarded above (#390/366e3053). Without
3417
+ // this, a client cancel during the continuation reopens the Bun fetch-to-reader abort race.
3418
+ const detachContinuationBodyGuard = cancelBodyOnAbort(response.body, upstream.signal);
3419
+ try {
3420
+ if (nextParsed.stream) {
3421
+ yield* activeAdapter.parseStream(response, translatorBudget);
3422
+ } else if (activeAdapter.parseResponse) {
3423
+ yield* await activeAdapter.parseResponse(response, translatorBudget);
3424
+ } else {
3425
+ yield { type: "error", message: "Provider continuation does not support response parsing" };
3426
+ }
3427
+ } finally {
3428
+ detachContinuationBodyGuard();
3429
+ }
3430
+ } catch (error) {
3431
+ if (options.abortSignal?.aborted) {
3432
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
3433
+ } else {
3434
+ yield { type: "error", message: `Provider continuation parse failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
3435
+ }
3436
+ }
3437
+ };
3438
+
3439
+ if (parsed.stream) {
3440
+ const initialEventStream = activeAdapter.parseStream(upstreamResponse, translatorBudget);
3441
+ const eventStream = terminalGuardEnabled
3442
+ ? guardTerminalEventStream({
3443
+ parsed,
3444
+ firstEvents: initialEventStream,
3445
+ adapterName: activeAdapter.name,
3446
+ maxAutoContinuations: 1,
3447
+ continuation: fetchTerminalGuardContinuation,
3448
+ })
3449
+ : initialEventStream;
3450
+ const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3451
+ const sseStream = bridgeToResponsesSSE(
3452
+ eventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
3453
+ () => upstream.abort(), 2_000,
3454
+ {
3455
+ translatorBudget,
3456
+ replayCacheScope: parsed._clientThreadId ?? "global",
3457
+ ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
3458
+ stallTimeoutSec: config.stallTimeoutSec,
3459
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
3460
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
3461
+ ...(routedCompaction ? { compaction: true } : {}),
3462
+ onUsage: usage => {
3463
+ // Raw adapter usage, pre wire-normalization (see the runTurn branch above).
3464
+ logCtx.usageFromBridge = true;
3465
+ if (usage) {
3466
+ logCtx.usage = usage;
3467
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
3468
+ }
3469
+ },
3470
+ // Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
3471
+ // PRE-compaction history, and a later previous_response_id expansion would rehydrate the
3472
+ // giant stale chain Codex just replaced.
3473
+ ...(routedCompaction ? {} : {
3474
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
3475
+ rememberResponseState(
3476
+ parsed._rawBody,
3477
+ response,
3478
+ continuationStateForResponse(providerState),
3479
+ activeAdapter.name === "kiro" ? { force: true } : undefined,
3480
+ ),
3481
+ }),
3482
+ },
3483
+ );
3484
+ const bridgeTurnAc = new AbortController();
3485
+ const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, cleanupUpstreamAbort, options.turnAdmissionLease);
3486
+ return new Response(trackedSse, {
3487
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
3488
+ });
3489
+ }
3490
+
3491
+ if (activeAdapter.parseResponse) {
3492
+ let events: AdapterEvent[];
3493
+ try {
3494
+ const initialEvents = await activeAdapter.parseResponse(upstreamResponse, translatorBudget);
3495
+ if (terminalGuardEnabled) {
3496
+ events = [];
3497
+ for await (const event of guardTerminalEventStream({
3498
+ parsed,
3499
+ firstEvents: (async function* () { yield* initialEvents; })(),
3500
+ adapterName: activeAdapter.name,
3501
+ maxAutoContinuations: 1,
3502
+ continuation: fetchTerminalGuardContinuation,
3503
+ })) events.push(event);
3504
+ } else {
3505
+ events = initialEvents;
3506
+ }
3507
+ } finally {
3508
+ cleanupUpstreamAbort();
3509
+ }
3510
+ const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3511
+ let providerState: OcxProviderContinuationState | undefined;
3512
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
3513
+ translatorBudget,
3514
+ replayCacheScope: parsed._clientThreadId ?? "global",
3515
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
3516
+ toolNsMap,
3517
+ freeformToolNames,
3518
+ toolSearchToolNames,
3519
+ ...(routedCompaction ? { compaction: true } : {}),
3520
+ onProviderState: state => { providerState = state; },
3521
+ onUsage: usage => {
3522
+ logCtx.usageFromBridge = true;
3523
+ if (usage) {
3524
+ logCtx.usage = usage;
3525
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
3526
+ }
3527
+ },
3528
+ });
3529
+ // See the streaming branch: compaction turns skip the continuation cache.
3530
+ if (!routedCompaction) {
3531
+ rememberResponseState(
3532
+ parsed._rawBody,
3533
+ json,
3534
+ continuationStateForResponse(providerState),
3535
+ activeAdapter.name === "kiro" ? { force: true } : undefined,
3536
+ );
3537
+ }
3538
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
3539
+ }
3540
+
3541
+ return formatErrorResponse(400, "invalid_request_error", "Non-streaming not supported by this adapter");
3542
+ } finally {
3543
+ if (pendingHostAdmissionLease) {
3544
+ releaseUpstreamHostAdmission(pendingHostAdmissionLease);
3545
+ releaseCodexAuthContextProbeLease(authCtx);
3546
+ }
3547
+ }
3548
+ }
3549
+
3550
+
3551
+
3552
+ export function linkAbortSignal(upstream: AbortController, signal?: AbortSignal): () => void {
3553
+ if (!signal) return () => {};
3554
+ if (signal.aborted) {
3555
+ upstream.abort(signal.reason);
3556
+ return () => {};
3557
+ }
3558
+ const onAbort = () => upstream.abort(signal.reason);
3559
+ signal.addEventListener("abort", onAbort, { once: true });
3560
+ return () => signal.removeEventListener("abort", onAbort);
3561
+ }