@iislee/opencodex 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.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-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,2758 @@
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 { routeModel, type RouteResult } from "../../router";
20
+ import {
21
+ advanceComboAfterFailure,
22
+ comboDefaultEffort,
23
+ comboFailureDecision,
24
+ comboIdFromRawBody,
25
+ concreteComboRequestBody,
26
+ getCombo,
27
+ isComboTargetInCooldown,
28
+ NoAvailableComboTargetsError,
29
+ noteComboSuccess,
30
+ parseRetryAfterMs,
31
+ pickComboTarget,
32
+ targetKey,
33
+ } from "../../combos";
34
+ import { isInjectionDebugEnabled } from "../../lib/debug-settings";
35
+ import { injectionDebugLog } from "../../lib/injection-debug-log";
36
+ import { resolveClientRetryAfter } from "../../lib/retry-after";
37
+ import { modelInList, namespacedToolName } from "../../types";
38
+ import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
39
+ import {
40
+ forceRefreshOAuthAccessSnapshot,
41
+ getOAuthCredentialApiBaseUrl,
42
+ getOAuthCredentialProjectId,
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, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
63
+ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
64
+ import {
65
+ applyCodexAuthContextToProvider,
66
+ CodexAccountCooldownError,
67
+ cooldownErrorResponse,
68
+ CodexAuthContextError,
69
+ CodexDirectAuthenticationError,
70
+ CodexPoolAuthenticationError,
71
+ CodexThreadAffinityExpiredError,
72
+ headersForCodexAuthContext,
73
+ isCodexAuthContextUsable,
74
+ resolveCodexAuthContext,
75
+ codexProbeLeaseId,
76
+ codexProbeQuotaScope,
77
+ releaseCodexAuthContextProbeLease,
78
+ stripCodexRuntimeProviderFields,
79
+ type CodexAuthContext,
80
+ } from "../../codex/auth-context";
81
+ import {
82
+ computeQuotaCooldown,
83
+ formatCodexProviderForLog,
84
+ previewCodexAccountForRequest,
85
+ recordCodexUpstreamOutcome,
86
+ type CodexUpstreamOutcome,
87
+ } from "../../codex/routing";
88
+ import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../../lib/upstream-retry";
89
+ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
90
+ import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
91
+ import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
92
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
93
+ import { slugsEquivalent } from "../../providers/slug-codec";
94
+ import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
95
+ import { isUsageDebugEnabled } from "../../usage/debug";
96
+ import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
97
+ import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
98
+ import type { InboundWire } from "../../providers/registry";
99
+ import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover";
100
+ import { shouldAttemptImageTierRetry } from "../image-retry";
101
+ import { resolveProviderTransport } from "../../providers/xai-transport";
102
+ import type { WsData } from "../ws-bridge";
103
+ import { registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
104
+ import { redactSecretString } from "../../lib/redact";
105
+ import { readBoundedResponseBody } from "../../lib/bounded-body";
106
+ import type { AdmissionLease } from "../../lib/admission";
107
+ import { supportedLadderFor } from "../effort-policy";
108
+ import { isThreadSpawnRequest } from "../effort-policy";
109
+ import {
110
+ applySubagentModelFallback,
111
+ maybePrimeSubagentQuota,
112
+ recordSubagentQuotaFailureForThreadSpawn,
113
+ } from "../../codex/subagent-model-fallback";
114
+ import {
115
+ beginRequestAttempt,
116
+ catalogModelSupportsServiceTier,
117
+ finishRequestAttempt,
118
+ inspectResponseLogJson,
119
+ noteAttemptSend,
120
+ readConfiguredCodexServiceTier,
121
+ recordAdapterReasoning,
122
+ recordAttemptRequestedEffort,
123
+ requestLogSpeedLabel,
124
+ sealRequestAttemptIdentity,
125
+ usageFromResponsesPayload,
126
+ type RequestLogContext,
127
+ } from "../request-log";
128
+ import {
129
+ conversationIdFromResponsesRequest,
130
+ normalizeLogConversationId,
131
+ sessionIdHeaderFromRequest,
132
+ } from "../request-log-conversation";
133
+ import type { AttemptRecoveryKind } from "../../usage/log";
134
+ import {
135
+ consumeForInspection,
136
+ consumeForResponseLogMetadata,
137
+ createSseInspector,
138
+ isEagerRelaySseResponse,
139
+ isNativePassthroughSseResponse,
140
+ markEagerRelaySseResponse,
141
+ markNativePassthroughSseResponse,
142
+ relaySseWithFailedTail,
143
+ relayWithAbort,
144
+ sanitizePassthroughHeaders,
145
+ } from "../relay";
146
+ import { relaySseEagerBounded } from "../relay-eager";
147
+ import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
148
+ import { cancelBodyOnAbort } from "../../lib/abort";
149
+ import {
150
+ createResponsesItemIdPayloadRewrite,
151
+ hasResponsesItemIdRepair,
152
+ } from "../responses-item-id-repair";
153
+ import {
154
+ createImageGenCallRestoreRewrite,
155
+ imageGenToolCallAliases,
156
+ restoreImageGenCallsInJson,
157
+ } from "../responses-image-gen-repair";
158
+ import { composeSsePayloadRewrites, relaySseWithPayloadRewrite } from "../sse-payload-rewrite";
159
+ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
160
+
161
+ import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
162
+ import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
163
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./fetch-helpers";
164
+ import { guardTerminalEventStream } from "./terminal-guard";
165
+
166
+ /**
167
+ * Adapters whose continuation state must survive Codex's store:false requests.
168
+ */
169
+ export function adapterNeedsForcedContinuation(name: string): boolean {
170
+ return name === "kiro" || name === "cursor";
171
+ }
172
+
173
+ export function sidecarOutcomeRecorder(
174
+ config: OcxConfig,
175
+ authCtx: CodexAuthContext,
176
+ threadId?: string | null,
177
+ ): ((outcome: CodexUpstreamOutcome) => void) | undefined {
178
+ return authCtx.kind === "pool" || authCtx.kind === "main-pool"
179
+ ? outcome => recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
180
+ threadId,
181
+ probeLeaseId: authCtx.probeLeaseId,
182
+ writerGeneration: authCtx.writerGeneration,
183
+ })
184
+ : undefined;
185
+ }
186
+
187
+
188
+
189
+ import { isShadowSourceModel } from "../../lib/shadow-call";
190
+
191
+ export { DEFAULT_SHADOW_SOURCE_MODELS, isShadowSourceModel, shadowSourceModels } from "../../lib/shadow-call";
192
+
193
+
194
+
195
+ export function codexLogAccountId(authCtx: CodexAuthContext): string | null {
196
+ return authCtx.kind === "pool" || authCtx.kind === "main-pool" ? authCtx.accountId : null;
197
+ }
198
+
199
+
200
+
201
+ export function usesCodexForwardPoolAuth(
202
+ authCtx: CodexAuthContext,
203
+ provider: OcxProviderConfig,
204
+ ): authCtx is Extract<CodexAuthContext, { kind: "pool" | "main-pool" }> {
205
+ return (authCtx.kind === "pool" || authCtx.kind === "main-pool")
206
+ && provider.authMode === "forward" && provider.adapter === "openai-responses";
207
+ }
208
+
209
+ function normalizeCodexUnsupportedModelDetail(value: string): string {
210
+ return value.trim().replace(/\s+/gu, " ").toLocaleLowerCase("en-US");
211
+ }
212
+
213
+ function isAllowListedCodexAccountModel400(
214
+ status: number,
215
+ bodyText: string,
216
+ modelId: string,
217
+ ): boolean {
218
+ if (status !== 400) return false;
219
+ try {
220
+ const payload = JSON.parse(bodyText) as unknown;
221
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
222
+ const detail = (payload as { detail?: unknown }).detail;
223
+ if (typeof detail !== "string") return false;
224
+ const expected = `The '${modelId}' model is not supported when using Codex with a ChatGPT account.`;
225
+ return normalizeCodexUnsupportedModelDetail(detail)
226
+ === normalizeCodexUnsupportedModelDetail(expected);
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ async function shouldRetryCodexPoolAccountModel400(
233
+ response: Response,
234
+ modelId: string,
235
+ signal?: AbortSignal,
236
+ ): Promise<boolean> {
237
+ if (response.status !== 400) return false;
238
+ try {
239
+ const body = await readBoundedResponseBody(response.clone(), { signal });
240
+ return body.displaySafe
241
+ && !body.truncated
242
+ && isAllowListedCodexAccountModel400(response.status, body.text, modelId);
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+
248
+ /** Pre-stream quota/billing rejections that warrant one alternate-account attempt (#584). */
249
+ function shouldRetryCodexPoolAccountQuota(response: Response): boolean {
250
+ return response.status === 429 || response.status === 402;
251
+ }
252
+
253
+ interface CodexPoolAccountRetryArgs {
254
+ req: Request;
255
+ config: OcxConfig;
256
+ route: { providerName: string; modelId: string; provider: OcxProviderConfig };
257
+ parsed: OcxParsedRequest;
258
+ logCtx: RequestLogContext;
259
+ options: {
260
+ abortSignal?: AbortSignal;
261
+ onCodexAuthContextResolved?: (ctx: CodexAuthContext) => void;
262
+ deferCodexResetDerivedCooldown?: boolean;
263
+ // Narrowed subset of HandleResponsesOptions: the retry rebuilds the adapter, so it
264
+ // needs the inbound scope or the retry could land on a different wire than the
265
+ // first attempt.
266
+ inboundWire?: InboundWire;
267
+ translatorBudget: TranslatorBudget;
268
+ };
269
+ firstAuthCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
270
+ firstResponse: Response;
271
+ outcomeStatus: number;
272
+ upstream: AbortController;
273
+ connectMs: number;
274
+ passthroughEstimate?: number;
275
+ stream: boolean;
276
+ }
277
+
278
+ type CodexPoolAccountRetryResult =
279
+ | {
280
+ kind: "retried";
281
+ authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
282
+ request: Awaited<ReturnType<ReturnType<typeof resolveAdapter>["buildRequest"]>>;
283
+ upstreamResponse: Response;
284
+ selectedForwardHeaders: Headers;
285
+ }
286
+ | { kind: "no-alternate" }
287
+ | {
288
+ kind: "transport";
289
+ error: unknown;
290
+ authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
291
+ };
292
+
293
+ function codexQuotaOutcomeMeta(response: Response): {
294
+ retryAfter: string | null;
295
+ resetAt: string[];
296
+ } {
297
+ return {
298
+ retryAfter: response.headers.get("retry-after"),
299
+ resetAt: [
300
+ response.headers.get("x-codex-primary-reset-at"),
301
+ response.headers.get("x-codex-secondary-reset-at"),
302
+ response.headers.get("x-codex-tertiary-reset-at"),
303
+ ].filter((value): value is string => !!value),
304
+ };
305
+ }
306
+
307
+ /**
308
+ * A reset timestamp describes a quota window, not an explicit instruction to
309
+ * stop using the whole account. A combo may therefore try a later model in the
310
+ * same request, while Retry-After and headerless quota failures remain blocking.
311
+ */
312
+ function shouldDeferCodexResetDerivedCooldown(response: Response, enabled?: boolean): boolean {
313
+ return enabled === true
314
+ && (response.status === 429 || response.status === 402)
315
+ && computeQuotaCooldown(codexQuotaOutcomeMeta(response)).source === "reset-derived";
316
+ }
317
+
318
+ /**
319
+ * One bounded alternate-account retry for Codex pool auth. Used for allow-listed
320
+ * model-400 and for pre-stream 429/402 quota failures (#584).
321
+ */
322
+ async function retryCodexPoolOnAlternateAccount(
323
+ args: CodexPoolAccountRetryArgs,
324
+ ): Promise<CodexPoolAccountRetryResult> {
325
+ const {
326
+ req, config, route, parsed, logCtx, options, firstAuthCtx, firstResponse,
327
+ outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
328
+ } = args;
329
+ const inboundWire = options.inboundWire ?? "responses";
330
+ let retryAuthCtx: CodexAuthContext | undefined;
331
+ try {
332
+ retryAuthCtx = await resolveCodexAuthContext(
333
+ req.headers,
334
+ config,
335
+ "pool",
336
+ { excludeAccountId: firstAuthCtx.accountId, modelId: route.modelId },
337
+ );
338
+ } catch (error) {
339
+ if (
340
+ !(error instanceof CodexPoolAuthenticationError)
341
+ && !(error instanceof CodexAuthContextError)
342
+ && !(error instanceof CodexAccountCooldownError)
343
+ ) throw error;
344
+ }
345
+ if (retryAuthCtx?.kind !== "pool" && retryAuthCtx?.kind !== "main-pool") {
346
+ return { kind: "no-alternate" };
347
+ }
348
+
349
+ const quotaMeta = codexQuotaOutcomeMeta(firstResponse);
350
+ if (outcomeStatus === 429 || outcomeStatus === 402) {
351
+ const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
352
+ applyAccountQuotaFromUpstreamHeaders(
353
+ firstAuthCtx.accountId,
354
+ firstResponse.headers,
355
+ firstAuthCtx.writerGeneration,
356
+ );
357
+ }
358
+ if (!shouldDeferCodexResetDerivedCooldown(firstResponse, options.deferCodexResetDerivedCooldown)) {
359
+ recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
360
+ ...quotaMeta,
361
+ threadId: req.headers.get("x-codex-parent-thread-id"),
362
+ modelId: route.modelId,
363
+ probeLeaseId: codexProbeLeaseId(firstAuthCtx),
364
+ probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
365
+ writerGeneration: firstAuthCtx.writerGeneration,
366
+ // Retry already advanced the RR ring via excludeAccountId — reuse for promotion.
367
+ ...(retryAuthCtx.accountId ? { promoteAccountId: retryAuthCtx.accountId } : {}),
368
+ });
369
+ }
370
+
371
+ const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx);
372
+ const retryProvider = applyCodexAuthContextToProvider(
373
+ stripCodexRuntimeProviderFields(route.provider),
374
+ retryAuthCtx,
375
+ "pool",
376
+ );
377
+ const retryAdapter = resolveAdapter(
378
+ resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
379
+ config.cacheRetention,
380
+ );
381
+ const request = await retryAdapter.buildRequest(parsed, {
382
+ headers: retryHeaders,
383
+ translatorBudget: options.translatorBudget,
384
+ });
385
+ recordAdapterReasoning(logCtx, request);
386
+
387
+ await firstResponse.body?.cancel().catch(() => undefined);
388
+ options.onCodexAuthContextResolved?.(retryAuthCtx);
389
+ route.provider = retryProvider;
390
+ logCtx.provider = formatCodexProviderForLog(
391
+ route.providerName,
392
+ retryAuthCtx.accountId,
393
+ config,
394
+ );
395
+
396
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate);
397
+ try {
398
+ const upstreamResponse = await fetchWithHeaderTimeout(
399
+ request.url,
400
+ {
401
+ method: request.method,
402
+ headers: request.headers,
403
+ body: request.body,
404
+ },
405
+ upstream.signal,
406
+ connectMs,
407
+ stream,
408
+ providerFetch(route.provider),
409
+ );
410
+ return {
411
+ kind: "retried",
412
+ authCtx: retryAuthCtx,
413
+ request,
414
+ upstreamResponse,
415
+ selectedForwardHeaders: retryHeaders,
416
+ };
417
+ } catch (error) {
418
+ // Attribute the transport failure to the alternate account (already selected).
419
+ return { kind: "transport", error, authCtx: retryAuthCtx };
420
+ } finally {
421
+ request.releaseBodyObservation?.();
422
+ }
423
+ }
424
+
425
+
426
+
427
+ export function codexForwardTerminalOutcomeRecorder(
428
+ config: OcxConfig,
429
+ authCtx: CodexAuthContext,
430
+ provider: OcxProviderConfig,
431
+ modelId?: string,
432
+ logCtx?: RequestLogContext,
433
+ threadId?: string | null,
434
+ ): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
435
+ if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
436
+ return (status, httpStatusOverride) => {
437
+ if (status === "incomplete") {
438
+ // Normal limit/content-filter/stall terminal — the account served the
439
+ // request. Don't penalize account health; record success to clear any
440
+ // prior soft-avoid so a healthy account isn't stuck avoided.
441
+ recordCodexUpstreamOutcome(config, authCtx.accountId, 200, {
442
+ threadId,
443
+ modelId,
444
+ probeLeaseId: codexProbeLeaseId(authCtx),
445
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
446
+ writerGeneration: authCtx.writerGeneration,
447
+ });
448
+ return;
449
+ }
450
+ // status === "completed" or "failed": use the semantic HTTP status derived
451
+ // from the terminal SSE error payload (httpStatusFromTerminalError in
452
+ // request-log inspection) instead of collapsing every non-completed terminal
453
+ // to 502. A 400 invalid_request_error must not soft-avoid the account or
454
+ // rebind threads — only genuine transport/5xx failures should trigger
455
+ // transient health recording.
456
+ // httpStatusOverride: the combo WS path inspects SSE payloads into the parent
457
+ // logCtx, but this recorder closes over the child logCtx. The caller passes
458
+ // the parent's terminalHttpStatus so the semantic status is not lost.
459
+ const outcome = status === "completed"
460
+ ? 200
461
+ : (httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
462
+ recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
463
+ threadId,
464
+ modelId,
465
+ probeLeaseId: codexProbeLeaseId(authCtx),
466
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
467
+ writerGeneration: authCtx.writerGeneration,
468
+ });
469
+ };
470
+ }
471
+
472
+
473
+
474
+ export function decodeRequestErrorResponse(err: unknown, label: string): Response {
475
+ if (isTranslatorBudgetExceededError(err)) {
476
+ return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
477
+ code: "translation_buffer_limit",
478
+ });
479
+ }
480
+ if (err instanceof UnsupportedContentEncodingError) {
481
+ return formatErrorResponse(415, "invalid_request_error", err.message);
482
+ }
483
+ if (err instanceof DecompressedBodyTooLargeError) {
484
+ return formatErrorResponse(413, "invalid_request_error", err.message);
485
+ }
486
+ console.warn(`[${label}] request body decode/parse failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
487
+ return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
488
+ }
489
+
490
+
491
+
492
+ export function comboUnavailableResponse(message: string): Response {
493
+ return new Response(
494
+ JSON.stringify({
495
+ error: { message, type: "server_error", code: "combo_unavailable" },
496
+ }),
497
+ { status: 503, headers: { "Content-Type": "application/json" } },
498
+ );
499
+ }
500
+
501
+
502
+
503
+ export interface ConsumedComboFailure {
504
+ response: Response;
505
+ classificationText: string;
506
+ /** Structured upstream `error.code` when present in the failure body. */
507
+ upstreamCode?: string;
508
+ /** Valid numeric/date value used only for cooldown calculation. */
509
+ retryAfter?: string;
510
+ /** Reserved for 040 usage attribution without adding another body read. */
511
+ usage?: OcxUsage;
512
+ }
513
+
514
+
515
+
516
+ export interface HandleResponsesOptions {
517
+ turnAdmissionLease?: AdmissionLease;
518
+ forceEmptyResponseId?: boolean;
519
+ abortSignal?: AbortSignal;
520
+ /** One-shot TTFT callback: first non-empty model output observed (WP4). */
521
+ onFirstOutput?: () => void;
522
+ onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
523
+ recordTerminalOutcomes?: boolean;
524
+ setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
525
+ onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
526
+ onNativePassthroughCancel?: () => void;
527
+ /**
528
+ * When true, body `prompt_cache_key` is a Claude Desktop shared cache cohort
529
+ * (system/tools hash), not a per-session id — do not use it for Anthropic pool affinity.
530
+ */
531
+ promptCacheKeyIsSharedCohort?: boolean;
532
+ /**
533
+ * Wire protocol the ORIGINAL client spoke. The Chat and Anthropic surfaces translate
534
+ * their body into a Responses shape and replay through this function, so without an
535
+ * explicit value the replay would look like a native Responses request and an
536
+ * inbound-scoped registry wire default would fire for a client that never asked for
537
+ * it. Omitted means a genuine Responses inbound.
538
+ */
539
+ inboundWire?: InboundWire;
540
+ /** Internal recursion guard; callers outside this module must not set it. */
541
+ comboAttempt?: boolean;
542
+ /** Internal combo handoff: allow a later same-provider model after a reset-derived 429/402. */
543
+ deferCodexResetDerivedCooldown?: boolean;
544
+ /** 030-owned handoff when a child consumed the original failure under bounds. */
545
+ onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
546
+ /** Caller-owned for Chat/Claude replay; omitted only at genuine Responses ingress. */
547
+ translatorBudget?: TranslatorBudget;
548
+ }
549
+
550
+
551
+
552
+ export function clientCancelledResponse(): Response {
553
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled request");
554
+ }
555
+
556
+
557
+
558
+ export function sanitizedRetryAfter(value: string | null, now: number): string | undefined {
559
+ const trimmed = value?.trim();
560
+ if (!trimmed || trimmed.length > 128) return undefined;
561
+ return parseRetryAfterMs(trimmed, now) !== undefined ? trimmed : undefined;
562
+ }
563
+
564
+
565
+
566
+ export async function consumeComboFailure(
567
+ response: Response,
568
+ signal?: AbortSignal,
569
+ now = Date.now(),
570
+ ): Promise<ConsumedComboFailure> {
571
+ const fallback = `Provider error ${response.status}`;
572
+ let classificationText = fallback;
573
+ let usage: OcxUsage | undefined;
574
+ let upstreamCode: string | undefined;
575
+ try {
576
+ const body = await readBoundedResponseBody(response, { signal });
577
+ usage = usageFromComboFailureText(body.text);
578
+ if (body.displaySafe) {
579
+ const safeText = redactSecretString(body.text).slice(0, 500);
580
+ if (safeText) classificationText = safeText;
581
+ try {
582
+ const parsed = JSON.parse(body.text) as { error?: { code?: unknown } | string };
583
+ const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.code : undefined;
584
+ if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
585
+ } catch {
586
+ /* non-JSON upstream body — message-only classification */
587
+ }
588
+ }
589
+ } catch (error) {
590
+ if (signal?.aborted) throw error;
591
+ classificationText = fallback;
592
+ }
593
+ const message = classificationText === fallback
594
+ ? fallback
595
+ : `${fallback}: ${classificationText}`;
596
+ const upstreamRetryAfter = response.headers.get("retry-after");
597
+ // Client response may get the synthetic "2" fallback; cooldown metadata must not —
598
+ // otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
599
+ const clientRetryAfter = resolveClientRetryAfter({
600
+ status: response.status,
601
+ message,
602
+ upstreamRetryAfter,
603
+ now,
604
+ });
605
+ const cooldownRetryAfter = resolveClientRetryAfter({
606
+ status: response.status,
607
+ message,
608
+ upstreamRetryAfter,
609
+ now,
610
+ includeDefault: false,
611
+ });
612
+ return {
613
+ response: formatErrorResponse(response.status, "upstream_error", message, {
614
+ ...(upstreamCode !== undefined ? { code: upstreamCode } : {}),
615
+ ...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
616
+ }),
617
+ classificationText,
618
+ ...(upstreamCode !== undefined ? { upstreamCode } : {}),
619
+ ...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
620
+ ...(usage ? { usage } : {}),
621
+ };
622
+ }
623
+
624
+
625
+
626
+ export function usageFromComboFailureText(text: string): OcxUsage | undefined {
627
+ try {
628
+ const payload = JSON.parse(text) as Record<string, unknown>;
629
+ const nested = payload.response;
630
+ const source = nested && typeof nested === "object" && !Array.isArray(nested)
631
+ ? nested as Record<string, unknown>
632
+ : payload;
633
+ return usageFromResponsesPayload(source.usage);
634
+ } catch {
635
+ return undefined;
636
+ }
637
+ }
638
+
639
+
640
+
641
+ export function createChildPassthroughCallbackGate(options: HandleResponsesOptions) {
642
+ type Pending =
643
+ | { kind: "terminal"; status: ResponsesTerminalStatus }
644
+ | { kind: "cancel" };
645
+ let state: "pending" | "committed" | "discarded" = "pending";
646
+ let pending: Pending | undefined;
647
+ let accepted = false;
648
+ const publish = (value: Pending): void => {
649
+ if (value.kind === "terminal") options.onNativePassthroughTerminal?.(value.status);
650
+ else options.onNativePassthroughCancel?.();
651
+ };
652
+ const receive = (value: Pending): void => {
653
+ if (state === "discarded" || accepted) return;
654
+ accepted = true;
655
+ if (state === "committed") return publish(value);
656
+ pending ??= value;
657
+ };
658
+ return {
659
+ onTerminal: (status: ResponsesTerminalStatus) => receive({ kind: "terminal", status }),
660
+ onCancel: () => receive({ kind: "cancel" }),
661
+ commit: () => {
662
+ if (state !== "pending") return;
663
+ state = "committed";
664
+ if (pending) publish(pending);
665
+ pending = undefined;
666
+ },
667
+ discard: () => {
668
+ state = "discarded";
669
+ pending = undefined;
670
+ },
671
+ };
672
+ }
673
+
674
+
675
+
676
+ export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
677
+ const childHeaders = new Headers(parentHeaders);
678
+ // Combo children re-serialize already-decoded JSON. Keeping transport metadata from
679
+ // the parent would make the child decoder treat plain JSON as compressed bytes.
680
+ childHeaders.delete("content-length");
681
+ childHeaders.delete("content-encoding");
682
+ return childHeaders;
683
+ }
684
+
685
+ const UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE =
686
+ "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.";
687
+
688
+ function unreadableEncryptedAgentTaskResponse(): Response {
689
+ return new Response(
690
+ JSON.stringify({
691
+ error: {
692
+ message: UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE,
693
+ type: "invalid_request_error",
694
+ code: "unreadable_encrypted_agent_task",
695
+ },
696
+ }),
697
+ { status: 400, headers: { "Content-Type": "application/json" } },
698
+ );
699
+ }
700
+
701
+ type ResponsesAuthResolution =
702
+ | { ok: true; authCtx: CodexAuthContext; headers: Headers }
703
+ | { ok: false; response: Response };
704
+
705
+ /**
706
+ * Resolve Codex auth for a route. On unusable contexts, releases any probe lease
707
+ * before returning the 401 (nothing reaches upstream).
708
+ */
709
+ async function resolveResponsesCodexAuth(
710
+ req: Request,
711
+ config: OcxConfig,
712
+ route: RouteResult,
713
+ options: HandleResponsesOptions,
714
+ ): Promise<ResponsesAuthResolution> {
715
+ try {
716
+ if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
717
+ let authCtx: CodexAuthContext;
718
+ if (route.codexAccountMode) {
719
+ authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, { modelId: route.modelId });
720
+ options.onCodexAuthContextResolved?.(authCtx);
721
+ } else {
722
+ authCtx = { kind: "main", accountId: null };
723
+ options.onCodexAuthContextResolved?.(undefined);
724
+ }
725
+ if (!isCodexAuthContextUsable(authCtx, config)) {
726
+ releaseCodexAuthContextProbeLease(authCtx);
727
+ return {
728
+ ok: false,
729
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
730
+ };
731
+ }
732
+ return {
733
+ ok: true,
734
+ authCtx,
735
+ headers: headersForCodexAuthContext(req.headers, authCtx),
736
+ };
737
+ } catch (err) {
738
+ if (err instanceof CodexAccountCooldownError) {
739
+ return { ok: false, response: cooldownErrorResponse(err) };
740
+ }
741
+ if (err instanceof CodexThreadAffinityExpiredError) {
742
+ return {
743
+ ok: false,
744
+ response: formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"),
745
+ };
746
+ }
747
+ if (err instanceof CodexAuthContextError) {
748
+ const safeAccountLabel = formatCodexProviderForLog(route.providerName, err.accountId, config);
749
+ console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
750
+ return {
751
+ ok: false,
752
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
753
+ };
754
+ }
755
+ if (err instanceof CodexPoolAuthenticationError) {
756
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
757
+ }
758
+ if (err instanceof CodexDirectAuthenticationError) {
759
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
760
+ }
761
+ if (err instanceof ForwardAdmissionCredentialError) {
762
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
763
+ }
764
+ throw err;
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Apply every route-dependent request mutation against the final selected route.
770
+ * Must run only after subagent fallback has settled the model/provider.
771
+ */
772
+ async function applyFinalRouteRequestNormalization(args: {
773
+ parsed: OcxParsedRequest;
774
+ route: RouteResult;
775
+ config: OcxConfig;
776
+ req: Request;
777
+ logCtx: RequestLogContext;
778
+ inboundWire: InboundWire;
779
+ }): Promise<void> {
780
+ const { parsed, route, config, req, logCtx, inboundWire } = args;
781
+
782
+ // Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
783
+ if (route.modelId !== parsed.modelId) {
784
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
785
+ (parsed._rawBody as { model?: string }).model = route.modelId;
786
+ }
787
+ parsed.modelId = route.modelId;
788
+ }
789
+ // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
790
+ // this request will actually use (#404).
791
+ route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
792
+ logCtx.model = route.modelId;
793
+ logCtx.provider = route.providerName;
794
+ logCtx.providerAdapter = route.provider.adapter;
795
+
796
+ // Final selected model before virtual wire-model rewriting (Pro aliases).
797
+ const finalSelectedModelId = route.modelId;
798
+
799
+ // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
800
+ applyOpenAiVirtualModel(parsed, route, logCtx);
801
+
802
+ // Fast mode override for OpenAI-routed models.
803
+ if (config.fastMode !== undefined && route.provider.adapter === "openai-responses") {
804
+ const tier = config.fastMode ? "priority" : undefined;
805
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
806
+ if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
807
+ else delete (parsed._rawBody as Record<string, unknown>).service_tier;
808
+ }
809
+ parsed.options.serviceTier = tier;
810
+ }
811
+
812
+ {
813
+ const guidance = await multiAgentGuidanceText(parsed, {
814
+ multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
815
+ injectionModel: config.injectionModel,
816
+ injectionEffort: config.injectionEffort,
817
+ subagentModels: config.subagentModels,
818
+ subagentModelFallback: config.subagentModelFallback,
819
+ injectionPrompt: config.injectionPrompt,
820
+ });
821
+ if (guidance) {
822
+ injectDeveloperMessage(parsed, guidance);
823
+ if (isInjectionDebugEnabled()) {
824
+ injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
825
+ }
826
+ } else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
827
+ injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
828
+ }
829
+ }
830
+
831
+ {
832
+ const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
833
+ const surface = collabSurface(parsed);
834
+ if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
835
+ const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
836
+ if (capped) {
837
+ logCtx.requestedEffort = `${capped.from}->${capped.to}`;
838
+ if (isInjectionDebugEnabled()) {
839
+ injectionDebugLog(`[opencodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
840
+ }
841
+ }
842
+ } else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
843
+ injectionDebugLog(`[opencodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
844
+ }
845
+ }
846
+
847
+ {
848
+ const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
849
+ const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
850
+ ? nativeEffortClamp(route.modelId, parsed.options.reasoning)
851
+ : null;
852
+ if (clamped) {
853
+ parsed.options.reasoning = clamped;
854
+ const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
855
+ if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
856
+ logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
857
+ }
858
+ }
859
+ recordAttemptRequestedEffort(logCtx);
860
+ logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
861
+ route.modelId,
862
+ logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
863
+ );
864
+ }
865
+
866
+
867
+
868
+ export async function handleComboResponses(
869
+ req: Request,
870
+ rawBody: unknown,
871
+ comboId: string,
872
+ config: OcxConfig,
873
+ logCtx: RequestLogContext,
874
+ options: HandleResponsesOptions,
875
+ ): Promise<Response> {
876
+ const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
877
+ ? (rawBody as { model: string }).model
878
+ : `combo/${comboId}`;
879
+ Object.assign(logCtx, {
880
+ requestedModel,
881
+ model: requestedModel,
882
+ provider: "combo",
883
+ comboId,
884
+ });
885
+ const combo = getCombo(config, comboId);
886
+ if (!combo) {
887
+ return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
888
+ }
889
+ const adoptFailedChildLog = (childLog: RequestLogContext): void => {
890
+ // Attempts remain the complete physical history; the logical row mirrors the most recent
891
+ // failed target so an exhausted combo still has useful top-level reasoning diagnostics.
892
+ Object.assign(logCtx, childLog, {
893
+ requestedModel,
894
+ model: requestedModel,
895
+ provider: "combo",
896
+ comboId,
897
+ attempts: logCtx.attempts,
898
+ activeAttempt: undefined,
899
+ activeAttemptStartedAt: undefined,
900
+ });
901
+ };
902
+
903
+ const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
904
+ (rawBody as { input?: unknown } | undefined)?.input,
905
+ );
906
+ const canDecryptUnreadableAgentTask = (target: (typeof combo.targets)[number]): boolean => {
907
+ const provider = config.providers[target.provider];
908
+ if (!provider || provider.disabled === true) return false;
909
+ try {
910
+ const route = routeModel(config, `${target.provider}/${target.model}`);
911
+ return isCanonicalOpenAiForwardProvider(route.provider);
912
+ } catch {
913
+ return false;
914
+ }
915
+ };
916
+ const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
917
+ !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
918
+
919
+ if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
920
+ return unreadableEncryptedAgentTaskResponse();
921
+ }
922
+
923
+ const initialNow = Date.now();
924
+ let pick = pickComboTarget(config, comboId, {
925
+ eligible: target => payloadEligible(target)
926
+ && !isComboTargetInCooldown(comboId, target, initialNow),
927
+ });
928
+ if (!pick) {
929
+ return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
930
+ }
931
+
932
+ let lastFailure: Response | null = null;
933
+ while (pick) {
934
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
935
+ const childLog: RequestLogContext = {
936
+ model: pick.target.model,
937
+ provider: pick.target.provider,
938
+ ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
939
+ ...(logCtx.surface ? { surface: logCtx.surface } : {}),
940
+ };
941
+ const targetRoute = routeModel(config, `${pick.target.provider}/${pick.target.model}`);
942
+ const childBody = concreteComboRequestBody(
943
+ rawBody,
944
+ pick.target,
945
+ comboDefaultEffort(config, comboId),
946
+ supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
947
+ );
948
+ const childHeaders = buildComboChildHeaders(req.headers);
949
+ const childRequest = new Request(req.url, {
950
+ method: req.method,
951
+ headers: childHeaders,
952
+ body: JSON.stringify(childBody),
953
+ });
954
+ let resolvedAuth: CodexAuthContext | undefined;
955
+ let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
956
+ const started = Date.now();
957
+ const attempt = beginRequestAttempt(
958
+ (logCtx.attempts?.length ?? 0) + 1,
959
+ pick.target.provider,
960
+ pick.target.model,
961
+ config.providers[pick.target.provider]!.adapter,
962
+ );
963
+ childLog.activeAttempt = attempt;
964
+ let attemptRetained = false;
965
+ const retainCancelledAttempt = (): void => {
966
+ if (attemptRetained) return;
967
+ sealRequestAttemptIdentity(
968
+ attempt,
969
+ childLog.provider,
970
+ childLog.providerAdapter ?? attempt.adapter,
971
+ );
972
+ finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
973
+ (logCtx.attempts ??= []).push(attempt);
974
+ attemptRetained = true;
975
+ };
976
+ let consumedChildFailure: ConsumedComboFailure | undefined;
977
+ const callbackGate = createChildPassthroughCallbackGate(options);
978
+ let response: Response;
979
+ try {
980
+ const currentTargetProvider = pick.target.provider;
981
+ const deferCodexResetDerivedCooldown = combo.strategy === "failover"
982
+ && combo.targets.slice(pick.targetIndex + 1).some(target =>
983
+ target.provider === currentTargetProvider
984
+ && payloadEligible(target)
985
+ && !isComboTargetInCooldown(comboId, target),
986
+ );
987
+ response = await handleResponses(childRequest, config, childLog, {
988
+ ...options,
989
+ comboAttempt: true,
990
+ deferCodexResetDerivedCooldown,
991
+ // Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
992
+ // Object.assign(logCtx, childLog) would overwrite the request-relative value).
993
+ onFirstOutput: () => {
994
+ if (attempt.firstOutputMs === undefined) {
995
+ attempt.firstOutputMs = Math.max(0, Date.now() - started);
996
+ }
997
+ options.onFirstOutput?.();
998
+ },
999
+ onCodexAuthContextResolved: value => { resolvedAuth = value; },
1000
+ setTerminalOutcomeRecorder: value => { terminalRecorder = value; },
1001
+ onConsumedComboFailure: value => { consumedChildFailure = value; },
1002
+ onNativePassthroughTerminal: callbackGate.onTerminal,
1003
+ onNativePassthroughCancel: callbackGate.onCancel,
1004
+ });
1005
+ } catch (error) {
1006
+ callbackGate.discard();
1007
+ if (options.abortSignal?.aborted) {
1008
+ retainCancelledAttempt();
1009
+ return clientCancelledResponse();
1010
+ }
1011
+ throw error;
1012
+ }
1013
+
1014
+ if (options.abortSignal?.aborted) {
1015
+ callbackGate.discard();
1016
+ retainCancelledAttempt();
1017
+ return clientCancelledResponse();
1018
+ }
1019
+
1020
+ if (response.ok) {
1021
+ sealRequestAttemptIdentity(
1022
+ attempt,
1023
+ childLog.provider,
1024
+ childLog.providerAdapter ?? attempt.adapter,
1025
+ );
1026
+ (logCtx.attempts ??= []).push(attempt);
1027
+ attemptRetained = true;
1028
+ noteComboSuccess(comboId, combo, pick.target, pick.writerGeneration);
1029
+ Object.assign(logCtx, childLog, {
1030
+ requestedModel,
1031
+ model: requestedModel,
1032
+ provider: "combo",
1033
+ comboId,
1034
+ attempts: logCtx.attempts,
1035
+ activeAttempt: attempt,
1036
+ activeAttemptStartedAt: started,
1037
+ resolvedModel: childLog.resolvedModel ?? childLog.model,
1038
+ });
1039
+ options.onCodexAuthContextResolved?.(resolvedAuth);
1040
+ options.setTerminalOutcomeRecorder?.(terminalRecorder);
1041
+ callbackGate.commit();
1042
+ return response;
1043
+ }
1044
+
1045
+ callbackGate.discard();
1046
+ if (response.status === 499) {
1047
+ retainCancelledAttempt();
1048
+ return clientCancelledResponse();
1049
+ }
1050
+ let failure: ConsumedComboFailure;
1051
+ try {
1052
+ failure = consumedChildFailure
1053
+ ?? await consumeComboFailure(response, options.abortSignal);
1054
+ } catch (error) {
1055
+ if (options.abortSignal?.aborted) {
1056
+ retainCancelledAttempt();
1057
+ return clientCancelledResponse();
1058
+ }
1059
+ throw error;
1060
+ }
1061
+ if (options.abortSignal?.aborted) {
1062
+ retainCancelledAttempt();
1063
+ return clientCancelledResponse();
1064
+ }
1065
+ sealRequestAttemptIdentity(
1066
+ attempt,
1067
+ childLog.provider,
1068
+ childLog.providerAdapter ?? attempt.adapter,
1069
+ );
1070
+ finishRequestAttempt(
1071
+ attempt,
1072
+ response.status,
1073
+ Date.now() - started,
1074
+ failure.usage,
1075
+ );
1076
+ (logCtx.attempts ??= []).push(attempt);
1077
+ attemptRetained = true;
1078
+ lastFailure = failure.response;
1079
+ if (comboFailureDecision(failure.response.status, failure.classificationText, {
1080
+ code: failure.upstreamCode,
1081
+ }) === "stop") {
1082
+ adoptFailedChildLog(childLog);
1083
+ return lastFailure;
1084
+ }
1085
+ console.warn(
1086
+ `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
1087
+ );
1088
+ const nextPick = advanceComboAfterFailure(config, pick, {
1089
+ retryAfter: failure.retryAfter,
1090
+ now: Date.now(),
1091
+ eligible: payloadEligible,
1092
+ });
1093
+ if (!nextPick) adoptFailedChildLog(childLog);
1094
+ pick = nextPick;
1095
+ }
1096
+ return lastFailure!;
1097
+ }
1098
+
1099
+
1100
+
1101
+ function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget): Response {
1102
+ if (!response.body) {
1103
+ budget.dispose();
1104
+ return response;
1105
+ }
1106
+ const reader = response.body.getReader();
1107
+ let finalized = false;
1108
+ const finalize = () => {
1109
+ if (finalized) return;
1110
+ finalized = true;
1111
+ budget.dispose();
1112
+ };
1113
+ const body = new ReadableStream<Uint8Array>({
1114
+ async pull(controller) {
1115
+ try {
1116
+ const result = await reader.read();
1117
+ if (result.done) {
1118
+ finalize();
1119
+ controller.close();
1120
+ } else {
1121
+ controller.enqueue(result.value);
1122
+ }
1123
+ } catch (error) {
1124
+ finalize();
1125
+ controller.error(error);
1126
+ }
1127
+ },
1128
+ async cancel(reason) {
1129
+ try { await reader.cancel(reason); } finally { finalize(); }
1130
+ },
1131
+ });
1132
+ const finalizedResponse = new Response(body, {
1133
+ status: response.status,
1134
+ statusText: response.statusText,
1135
+ headers: response.headers,
1136
+ });
1137
+ if (isNativePassthroughSseResponse(response)) {
1138
+ markNativePassthroughSseResponse(finalizedResponse);
1139
+ }
1140
+ if (isEagerRelaySseResponse(response)) {
1141
+ markEagerRelaySseResponse(finalizedResponse);
1142
+ }
1143
+ return finalizedResponse;
1144
+ }
1145
+
1146
+ export async function handleResponses(
1147
+ req: Request,
1148
+ config: OcxConfig,
1149
+ logCtx: RequestLogContext,
1150
+ options: HandleResponsesOptions = {},
1151
+ ): Promise<Response> {
1152
+ const ownsBudget = options.translatorBudget === undefined;
1153
+ const translatorBudget = options.translatorBudget ?? createTranslatorBudget();
1154
+ try {
1155
+ const response = await handleResponsesInner(req, config, logCtx, { ...options, translatorBudget });
1156
+ return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
1157
+ } catch (error) {
1158
+ if (ownsBudget) translatorBudget.dispose();
1159
+ throw error;
1160
+ }
1161
+ }
1162
+
1163
+ async function handleResponsesInner(
1164
+ req: Request,
1165
+ config: OcxConfig,
1166
+ logCtx: RequestLogContext,
1167
+ options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
1168
+ ): Promise<Response> {
1169
+ // The Chat and Anthropic surfaces replay through here with a Responses-shaped body,
1170
+ // so an omitted value means a genuine Responses inbound.
1171
+ const inboundWire = options.inboundWire ?? "responses";
1172
+ const translatorBudget = options.translatorBudget;
1173
+ let body: unknown;
1174
+ try {
1175
+ body = await readJsonRequestBody(req, translatorBudget);
1176
+ } catch (err) {
1177
+ return decodeRequestErrorResponse(err, "responses");
1178
+ }
1179
+ const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
1180
+ if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
1181
+ return handleComboResponses(req, body, comboId, config, logCtx, options);
1182
+ }
1183
+ const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
1184
+ (body as { input?: unknown } | undefined)?.input,
1185
+ );
1186
+ const originalBody = body;
1187
+ body = expandPreviousResponseInput(body);
1188
+ if (previousResponseReplayFailure(body)) {
1189
+ return formatErrorResponse(
1190
+ 400,
1191
+ "previous_response_not_found",
1192
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
1193
+ );
1194
+ }
1195
+ const previousResponseInputExpanded = body !== originalBody;
1196
+
1197
+ // Spawn-message compatibility (both directions): agent_message task payloads ride in
1198
+ // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
1199
+ // parsing so every consumer sees the payload: parseRequest (routed/translated providers read
1200
+ // the parsed messages) and the native passthrough (_rawBody is this same object, serialized
1201
+ // verbatim). Genuine backend ciphertext is left byte-identical (looksLikeBackendCiphertext).
1202
+ {
1203
+ const rewritten = sanitizeEncryptedContentInPlace(
1204
+ (body as { input?: unknown } | undefined)?.input,
1205
+ );
1206
+ if (rewritten > 0)
1207
+ console.warn(
1208
+ `[opencodex] rewrote ${rewritten} plaintext encrypted_content part(s) to input_text (spawn-message compatibility)`,
1209
+ );
1210
+ }
1211
+
1212
+ let parsed;
1213
+ let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
1214
+ try {
1215
+ parsed = parseRequest(body);
1216
+ toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
1217
+ if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
1218
+ parsed._providerContinuation = previousResponseProviderState(parsed.previousResponseId);
1219
+ parsed._cursorConversationId = parsed._providerContinuation?.cursor?.conversationId;
1220
+ const clientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim();
1221
+ if (clientThreadId) parsed._clientThreadId = clientThreadId;
1222
+ } catch (err) {
1223
+ if (isTranslatorBudgetExceededError(err)) {
1224
+ return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
1225
+ code: "translation_buffer_limit",
1226
+ });
1227
+ }
1228
+ return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
1229
+ }
1230
+ // Prefer a pre-populated id (routed Claude) over Responses headers that may be
1231
+ // absent or synthetically injected (session_id from prompt_cache_key).
1232
+ if (!logCtx.conversationId) {
1233
+ logCtx.conversationId = conversationIdFromResponsesRequest({
1234
+ clientThreadId: parsed._clientThreadId,
1235
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
1236
+ threadIdHeader: req.headers.get("thread-id"),
1237
+ cursorConversationId: parsed._cursorConversationId,
1238
+ });
1239
+ }
1240
+ logCtx.requestedModel = parsed.modelId;
1241
+ logCtx.requestedEffort = parsed.options.reasoning;
1242
+ logCtx.requestedServiceTier = parsed.options.serviceTier;
1243
+ logCtx.requestedSpeedLabel = requestLogSpeedLabel(parsed.options.serviceTier);
1244
+ logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
1245
+ logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
1246
+
1247
+ // Shadow call intercept: rewrite Codex's hard-coded helper calls
1248
+ // (gpt-5.4-mini on older clients, gpt-5.6-luna on 0.145.0+)
1249
+ const _sci = config.shadowCallIntercept;
1250
+ if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
1251
+ const _sciOriginal = parsed.modelId;
1252
+ parsed.modelId = _sci.model;
1253
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
1254
+ (parsed._rawBody as { model?: string }).model = _sci.model;
1255
+ }
1256
+ // Force effort to low for shadow/helper calls (matching upstream behavior)
1257
+ parsed.options.reasoning = "low";
1258
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
1259
+ (parsed._rawBody as Record<string, unknown>).reasoning = { effort: "low" };
1260
+ }
1261
+ (logCtx as unknown as Record<string, unknown>).shadowCallRewrittenFrom = _sciOriginal;
1262
+ // Helpers must not resume/append into the parent thread's Cursor conversation.
1263
+ parsed._cursorIsolateConversation = true;
1264
+ }
1265
+ if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
1266
+
1267
+ let route: RouteResult;
1268
+ try {
1269
+ route = routeModel(config, parsed.modelId);
1270
+ } catch (err) {
1271
+ if (err instanceof NoAvailableComboTargetsError) {
1272
+ return comboUnavailableResponse(err.message);
1273
+ }
1274
+ return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
1275
+ }
1276
+
1277
+ const hasUnexpandedPreviousResponse = !!parsed.previousResponseId
1278
+ && parsed._previousResponseInputExpanded !== true;
1279
+ // A canonical replay miss must not poll quota upstream before the final fail-closed decision.
1280
+ // Cached fallback state can still select a provider with native continuation support below.
1281
+ if (
1282
+ isThreadSpawnRequest(req.headers)
1283
+ && !(hasUnexpandedPreviousResponse && isCanonicalOpenAiForwardProvider(route.provider))
1284
+ ) {
1285
+ await maybePrimeSubagentQuota(config);
1286
+ }
1287
+
1288
+ let authCtx: CodexAuthContext = { kind: "main", accountId: null };
1289
+ let selectedForwardHeaders = req.headers;
1290
+ let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
1291
+ let subagentQuotaFailureModel = parsed.modelId;
1292
+
1293
+ // Subagent fallback must settle the final model/provider BEFORE route-dependent
1294
+ // normalization (virtual models, effort caps, service tier, wire protocol).
1295
+ // Preview the preferred Codex account without acquiring a probe lease or refreshing
1296
+ // tokens — auth is resolved only after the final route is selected.
1297
+ if (isThreadSpawnRequest(req.headers) && !options.comboAttempt) {
1298
+ const threadId = req.headers.get("x-codex-parent-thread-id");
1299
+ const previewAccountId = previewCodexAccountForRequest(threadId, config);
1300
+ subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
1301
+ const fallback = applySubagentModelFallback(
1302
+ parsed,
1303
+ req.headers,
1304
+ config,
1305
+ previewAccountId,
1306
+ Date.now(),
1307
+ unreadableEncryptedAgentTask,
1308
+ );
1309
+ if (fallback) {
1310
+ (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
1311
+ (logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
1312
+ if (isInjectionDebugEnabled()) {
1313
+ injectionDebugLog(`[opencodex] subagent model fallback ${fallback.from} -> ${fallback.to}`);
1314
+ }
1315
+ }
1316
+ subagentQuotaFailureModel = fallback?.to ?? parsed.modelId;
1317
+
1318
+ if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
1319
+ try {
1320
+ route = routeModel(config, fallback.to);
1321
+ } catch (err) {
1322
+ if (err instanceof NoAvailableComboTargetsError) {
1323
+ return comboUnavailableResponse(err.message);
1324
+ }
1325
+ return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ // Encrypted child tasks may only reach the canonical native backend. This check
1331
+ // runs against the FINAL route so native-only fallback can rescue a routed primary.
1332
+ if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
1333
+ return unreadableEncryptedAgentTaskResponse();
1334
+ }
1335
+
1336
+ // The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no
1337
+ // safe way to recover the omitted history. Fail before auth, adapter construction, or upstream
1338
+ // I/O instead of stripping the id and silently forwarding a context-free delta (#702).
1339
+ if (
1340
+ hasUnexpandedPreviousResponse
1341
+ && isCanonicalOpenAiForwardProvider(route.provider)
1342
+ ) {
1343
+ return formatErrorResponse(
1344
+ 400,
1345
+ "invalid_request_error",
1346
+ "OpenAI forward continuation state is unavailable or expired; start a new session instead of reusing this previous_response_id.",
1347
+ );
1348
+ }
1349
+
1350
+ await applyFinalRouteRequestNormalization({ parsed, route, config, req, logCtx, inboundWire });
1351
+
1352
+ {
1353
+ const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
1354
+ if (!finalAuth.ok) return finalAuth.response;
1355
+ authCtx = finalAuth.authCtx;
1356
+ selectedForwardHeaders = finalAuth.headers;
1357
+ }
1358
+
1359
+ route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
1360
+ logCtx.provider = formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
1361
+ // Prefer Codex pool account as the Cursor thread namespace when present. Cursor routes without
1362
+ // codexAccountMode still get a credential-derived scope inside the Cursor adapter.
1363
+ const identityScope = codexLogAccountId(authCtx);
1364
+ if (identityScope) parsed._cursorIdentityScope = identityScope;
1365
+ subagentFallbackAccountId = authCtx.kind === "pool" || authCtx.kind === "main-pool"
1366
+ ? authCtx.accountId
1367
+ : config.activeCodexAccountId ?? null;
1368
+
1369
+ // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
1370
+ // existing openai-chat / anthropic adapters authenticate with no change.
1371
+ const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
1372
+ && route.provider.authMode === "oauth";
1373
+ let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
1374
+ let anthropicPoolAccountId: string | null = null;
1375
+ let anthropicPoolFailovers = 0;
1376
+ const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
1377
+ ? anthropicSessionKeyFromParts({
1378
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
1379
+ threadIdHeader: req.headers.get("thread-id"),
1380
+ promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
1381
+ clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
1382
+ promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
1383
+ })
1384
+ : null;
1385
+ if (route.provider.authMode === "oauth") {
1386
+ try {
1387
+ if (route.providerName === "anthropic" && isAnthropicAccountPoolEnabled(config)) {
1388
+ const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config);
1389
+ if (!selection.accountId) {
1390
+ if (selection.reason === "all-cooled") {
1391
+ const retryAfterSec = getAnthropicPoolRetryAfterSeconds();
1392
+ return formatErrorResponse(
1393
+ 429,
1394
+ "rate_limit_error",
1395
+ "All Anthropic OAuth accounts are temporarily rate-limited",
1396
+ retryAfterSec !== null ? { retryAfter: String(retryAfterSec) } : undefined,
1397
+ );
1398
+ }
1399
+ return formatErrorResponse(401, "authentication_error", "No eligible Anthropic OAuth account available");
1400
+ }
1401
+ const accessToken = await getAnthropicPoolAccessToken(selection.accountId);
1402
+ anthropicPoolAccountId = selection.accountId;
1403
+ bindAnthropicSessionAffinity(anthropicSessionKey, selection.accountId);
1404
+ promoteAnthropicActiveAccount(selection.accountId);
1405
+ route.provider = { ...route.provider, apiKey: accessToken };
1406
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
1407
+ } else {
1408
+ const resolved = await getValidAccessTokenSnapshot(route.providerName);
1409
+ if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
1410
+ route.provider = { ...route.provider, apiKey: resolved.accessToken };
1411
+ if (route.providerName === "kiro") {
1412
+ // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
1413
+ // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
1414
+ parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
1415
+ }
1416
+ // Antigravity (cloud-code-assist) needs the discovered Cloud Code Assist project id in the
1417
+ // CCA envelope; the server injects only the bare token, so pull project from the credential.
1418
+ if (route.provider.googleMode === "cloud-code-assist" && !route.provider.project) {
1419
+ const projectId = getOAuthCredentialProjectId(route.providerName);
1420
+ if (projectId) route.provider = { ...route.provider, project: projectId };
1421
+ }
1422
+ }
1423
+ } catch (err) {
1424
+ if (err instanceof UnsupportedOAuthProviderError) {
1425
+ return formatErrorResponse(
1426
+ 400,
1427
+ "invalid_request_error",
1428
+ `${err.message}. Remove or reconfigure provider '${route.providerName}' in ${getConfigPath()}.`,
1429
+ );
1430
+ }
1431
+ return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
1432
+ }
1433
+ }
1434
+ route.provider = resolveProviderTransport(
1435
+ route.providerName,
1436
+ route.provider,
1437
+ parsed.options.promptCacheKey,
1438
+ route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
1439
+ );
1440
+ const adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
1441
+ const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
1442
+ logCtx.providerAdapter = adapter.name;
1443
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name);
1444
+ const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
1445
+
1446
+ if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
1447
+ return formatErrorResponse(
1448
+ 400,
1449
+ "invalid_request_error",
1450
+ "Kiro continuation state is missing; start a new session instead of reusing this previous_response_id.",
1451
+ );
1452
+ }
1453
+
1454
+ let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
1455
+ const needsOpenAiVision = shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed);
1456
+ const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
1457
+ if (needsOpenAiVision || needsOpenAiSearch) {
1458
+ try {
1459
+ openAiSidecar = await resolveFirstUsableOpenAiSidecar(
1460
+ listOpenAiForwardSidecarCandidates(config),
1461
+ req.headers,
1462
+ config,
1463
+ );
1464
+ } catch (err) {
1465
+ // Sidecars are optional helpers for an otherwise independent routed turn.
1466
+ // An unavailable/cooling/expired Multi credential disables the helper; it
1467
+ // must not turn a valid routed-provider request into a Codex-auth failure.
1468
+ if (
1469
+ !(err instanceof CodexPoolAuthenticationError)
1470
+ && !(err instanceof CodexAuthContextError)
1471
+ && !(err instanceof CodexAccountCooldownError)
1472
+ && !(err instanceof CodexThreadAffinityExpiredError)
1473
+ ) throw err;
1474
+ }
1475
+ }
1476
+
1477
+ // Vision sidecar: the routed model can't see images (provider.noVisionModels). Describe each
1478
+ // attached image through the selected sidecar backend and replace it with text BEFORE the main
1479
+ // call, so the text-only model can reason about it.
1480
+ const visionPlan = planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
1481
+ const recordSidecarOutcome = openAiSidecar?.recordOutcome;
1482
+ if (visionPlan) {
1483
+ await describeImagesInPlace(
1484
+ parsed,
1485
+ visionPlan,
1486
+ openAiSidecar?.headers ?? selectedForwardHeaders,
1487
+ options.abortSignal,
1488
+ recordSidecarOutcome,
1489
+ translatorBudget,
1490
+ );
1491
+ } else if (modelInList(route.provider.noVisionModels, route.modelId)) {
1492
+ // Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar
1493
+ // disabled): fail closed — never forward raw images to a text-only upstream.
1494
+ stripImagesInPlace(parsed, translatorBudget);
1495
+ }
1496
+
1497
+ const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
1498
+
1499
+ const continuationStateForResponse = (
1500
+ emitted?: OcxProviderContinuationState,
1501
+ ): OcxProviderContinuationState | undefined => {
1502
+ const cursorConversationId = parsed._cursorConversationId;
1503
+ const inherited = parsed._providerContinuation;
1504
+ if (!emitted && !inherited && !cursorConversationId) return undefined;
1505
+ return {
1506
+ ...(inherited ?? {}),
1507
+ ...(emitted ?? {}),
1508
+ ...((inherited?.kiro || emitted?.kiro)
1509
+ ? { kiro: { ...(inherited?.kiro ?? {}), ...(emitted?.kiro ?? {}) } }
1510
+ : {}),
1511
+ ...(cursorConversationId
1512
+ ? {
1513
+ cursor: {
1514
+ ...(inherited?.cursor ?? {}),
1515
+ ...(emitted?.cursor ?? {}),
1516
+ conversationId: cursorConversationId,
1517
+ },
1518
+ }
1519
+ : {}),
1520
+ };
1521
+ };
1522
+
1523
+ // Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
1524
+ // one `{type:"compaction"}` output item (codex-rs compact_remote_v2.rs). Passthrough handles it
1525
+ // natively upstream; here we run the routed model as a plain summarizer — no tools, no web-search
1526
+ // sidecar — and the bridge appends the synthetic compaction item (src/responses/compaction.ts).
1527
+ // A Responses-shaped wire does not imply support for Codex's private
1528
+ // `compaction_trigger` item — only the canonical ChatGPT backend speaks that
1529
+ // contract. An API-key gateway would receive the trigger, answer with an ordinary
1530
+ // message, and leave Codex fataling on a missing compaction item (#422).
1531
+ const routedCompaction = parsed._compactionRequest === true
1532
+ && !isCanonicalOpenAiForwardProvider(route.provider);
1533
+ if (routedCompaction) {
1534
+ delete parsed.context.tools;
1535
+ delete parsed._webSearch;
1536
+ delete parsed.options.toolChoice;
1537
+ delete parsed.options.parallelToolCalls;
1538
+ parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() });
1539
+ }
1540
+
1541
+ if ("passthrough" in adapter && adapter.passthrough && !routedCompaction) {
1542
+ const imageGenCallAliases = route.provider.authMode === "forward"
1543
+ ? new Map<string, { namespace: string; name: string }>()
1544
+ : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
1545
+ // Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
1546
+ // previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
1547
+ // REST backend rejects the parameter — the adapter strips it in forward mode, so the ONLY
1548
+ // way a chained turn keeps its earlier context is the local replay expansion. Record
1549
+ // completed passthrough responses (force bypasses Codex's blanket store:false) so the next
1550
+ // turn's expansion hits. Never record a body whose own previous_response_id failed to
1551
+ // expand: its input is a delta, and storing it would replay a truncated conversation.
1552
+ // Compaction turns are excluded: _rawBody still carries the full pre-compaction history and
1553
+ // recording it would let a later expansion rehydrate the chain Codex just replaced.
1554
+ const passthroughRecordEligible = parsed._compactionRequest !== true
1555
+ && (!parsed.previousResponseId || parsed._previousResponseInputExpanded === true);
1556
+ const rememberPassthroughResponse = passthroughRecordEligible
1557
+ ? (response: { id?: unknown; output?: unknown; status?: unknown }) =>
1558
+ rememberResponseState(parsed._rawBody, response, undefined, { force: true })
1559
+ : undefined;
1560
+ if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
1561
+ console.warn(
1562
+ `[responses] previous_response_id ${parsed.previousResponseId} not found in local replay state `
1563
+ + `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
1564
+ );
1565
+ }
1566
+ let request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
1567
+ recordAdapterReasoning(logCtx, request);
1568
+ const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
1569
+ ? request.usageLog.inputTokens
1570
+ : undefined;
1571
+ if (passthroughEstimate !== undefined) {
1572
+ logCtx.usageLogInputTokens = passthroughEstimate;
1573
+ }
1574
+ // Abort the upstream if the client disconnects. A directly-relayed body does not propagate the
1575
+ // consumer's cancel to a signalled fetch, so we pass the signal and relay through relayWithAbort,
1576
+ // whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
1577
+ const upstream = new AbortController();
1578
+ linkAbortSignal(upstream, options.abortSignal);
1579
+ const connectMs = config.connectTimeoutMs ?? 200_000;
1580
+ let upstreamResponse: Response;
1581
+ const transportFailureResponse = (err: unknown): Response => {
1582
+ upstream.abort();
1583
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
1584
+ const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
1585
+ if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1586
+ recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
1587
+ threadId: req.headers.get("x-codex-parent-thread-id"),
1588
+ modelId: route.modelId,
1589
+ probeLeaseId: codexProbeLeaseId(authCtx),
1590
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
1591
+ writerGeneration: authCtx.writerGeneration,
1592
+ });
1593
+ }
1594
+ const msg = outcome === "timeout"
1595
+ ? `Provider connect timeout after ${connectMs}ms`
1596
+ : describeUpstreamConnectFailure(err, connectMs);
1597
+ return formatErrorResponse(502, "upstream_error", msg);
1598
+ };
1599
+ try {
1600
+ // Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
1601
+ // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
1602
+ // Body is a replayable string; nothing has streamed to the client yet.
1603
+ upstreamResponse = await fetchWithTransientRetry(
1604
+ recovery => {
1605
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery);
1606
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
1607
+ method: request.method,
1608
+ headers: request.headers,
1609
+ body: request.body,
1610
+ }, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
1611
+ },
1612
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
1613
+ );
1614
+ } catch (err) {
1615
+ return transportFailureResponse(err);
1616
+ } finally {
1617
+ request.releaseBodyObservation?.();
1618
+ }
1619
+
1620
+ if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1621
+ let poolRetryOutcome: number | undefined;
1622
+ if (await shouldRetryCodexPoolAccountModel400(
1623
+ upstreamResponse,
1624
+ route.modelId,
1625
+ options.abortSignal,
1626
+ )) {
1627
+ poolRetryOutcome = 400;
1628
+ } else if (shouldRetryCodexPoolAccountQuota(upstreamResponse)) {
1629
+ // Pre-stream only: once SSE has begun, mid-stream quota stays terminal.
1630
+ poolRetryOutcome = upstreamResponse.status;
1631
+ }
1632
+
1633
+ if (poolRetryOutcome !== undefined) {
1634
+ const retry = await retryCodexPoolOnAlternateAccount({
1635
+ req,
1636
+ config,
1637
+ route,
1638
+ parsed,
1639
+ logCtx,
1640
+ options,
1641
+ firstAuthCtx: authCtx,
1642
+ firstResponse: upstreamResponse,
1643
+ outcomeStatus: poolRetryOutcome,
1644
+ upstream,
1645
+ connectMs,
1646
+ passthroughEstimate,
1647
+ stream: parsed.stream,
1648
+ });
1649
+ if (retry.kind === "transport") {
1650
+ authCtx = retry.authCtx;
1651
+ return transportFailureResponse(retry.error);
1652
+ }
1653
+ if (retry.kind === "retried") {
1654
+ authCtx = retry.authCtx;
1655
+ request = retry.request;
1656
+ upstreamResponse = retry.upstreamResponse;
1657
+ selectedForwardHeaders = retry.selectedForwardHeaders;
1658
+ // Keep subagent quota-failure health keyed to the account that actually served.
1659
+ subagentFallbackAccountId = retry.authCtx.accountId;
1660
+ }
1661
+ }
1662
+ }
1663
+ const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
1664
+ const resolvedModel = headers.get("openai-model")?.trim();
1665
+ if (resolvedModel) logCtx.resolvedModel = resolvedModel;
1666
+ if (isUsageDebugEnabled()) {
1667
+ const upstreamContentType = upstreamResponse.headers.get("content-type");
1668
+ if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
1669
+ }
1670
+ // The chatgpt backend may omit Content-Type on SSE responses. Fall back to
1671
+ // treating a successful body as SSE when the caller requested streaming.
1672
+ const passthroughCt = headers.get("content-type")?.toLowerCase();
1673
+ const isEventStream = passthroughCt?.includes("text/event-stream")
1674
+ || (upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
1675
+ const terminalRecorder = codexForwardTerminalOutcomeRecorder(
1676
+ config,
1677
+ authCtx,
1678
+ route.provider,
1679
+ route.modelId,
1680
+ logCtx,
1681
+ req.headers.get("x-codex-parent-thread-id"),
1682
+ );
1683
+ const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
1684
+ // Capture quota from upstream response for multi-account tracking
1685
+ if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1686
+ // primary was the 5h window; it now carries weekly data for GPT plans.
1687
+ // Prefer primary when present, fall back to secondary for compatibility.
1688
+ const quotaMeta = codexQuotaOutcomeMeta(upstreamResponse);
1689
+ const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
1690
+ applyAccountQuotaFromUpstreamHeaders(
1691
+ authCtx.accountId,
1692
+ upstreamResponse.headers,
1693
+ authCtx.writerGeneration,
1694
+ );
1695
+ if (terminalBodyWillRecord) {
1696
+ options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
1697
+ terminalRecorder(status, httpStatusOverride);
1698
+ if (status === "failed") {
1699
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
1700
+ || logCtx.terminalHttpStatus === 429
1701
+ || logCtx.terminalHttpStatus === 402
1702
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1703
+ : undefined;
1704
+ if (quotaFailureMessage !== undefined) {
1705
+ recordSubagentQuotaFailureForThreadSpawn(
1706
+ req.headers,
1707
+ subagentQuotaFailureModel,
1708
+ quotaFailureMessage,
1709
+ config,
1710
+ subagentFallbackAccountId,
1711
+ );
1712
+ }
1713
+ }
1714
+ options.onNativePassthroughTerminal?.(status);
1715
+ });
1716
+ } else if (!shouldDeferCodexResetDerivedCooldown(
1717
+ upstreamResponse,
1718
+ options.deferCodexResetDerivedCooldown,
1719
+ )) {
1720
+ recordCodexUpstreamOutcome(config, authCtx.accountId, upstreamResponse.status, {
1721
+ ...quotaMeta,
1722
+ threadId: req.headers.get("x-codex-parent-thread-id"),
1723
+ modelId: route.modelId,
1724
+ probeLeaseId: codexProbeLeaseId(authCtx),
1725
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
1726
+ writerGeneration: authCtx.writerGeneration,
1727
+ });
1728
+ }
1729
+ }
1730
+
1731
+ // Non-2xx passthrough failures must never reach Codex as an empty body —
1732
+ // Codex renders that as the opaque "Unknown error" (#452). Combo attempts
1733
+ // keep their typed failure envelope. Non-empty bodies are relayed verbatim
1734
+ // (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
1735
+ if (!upstreamResponse.ok) {
1736
+ if (options.comboAttempt) {
1737
+ const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
1738
+ options.onConsumedComboFailure?.(failure);
1739
+ return failure.response;
1740
+ }
1741
+ const errorText = await upstreamResponse.text().catch(() => "");
1742
+ return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
1743
+ statusText: upstreamResponse.statusText,
1744
+ headers,
1745
+ });
1746
+ }
1747
+
1748
+ // Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
1749
+ // async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
1750
+ // native relay, never enters JS Sink.write); branch[1] is consumed in the
1751
+ // background for terminal-outcome/quota inspection only.
1752
+ // #314 alternative shape: win32 no-rewrite traffic follows the runtime/config
1753
+ // gate; darwin no-rewrite traffic joins it only for explicit
1754
+ // `streamMode: "eager-relay"` opt-in. Darwin `auto` always stays tee. The
1755
+ // eager shape skips tee and uses one bounded reader with inline inspection
1756
+ // (src/server/relay-eager.ts; policy:
1757
+ // devlog/_plan/260731_macos_rss_retention/100_darwin_eager_optin.md).
1758
+ // The bundled known-bad runtime remains on tee by default on both platforms.
1759
+ if (isEventStream && upstreamResponse.body) {
1760
+ const repairConfig = route.provider.responsesItemIdRepair;
1761
+ const needsClientRewrite = imageGenCallAliases.size > 0 || hasResponsesItemIdRepair(repairConfig);
1762
+ // Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
1763
+ const payloadRewrites = [
1764
+ createImageGenCallRestoreRewrite(imageGenCallAliases),
1765
+ hasResponsesItemIdRepair(repairConfig)
1766
+ ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
1767
+ : undefined,
1768
+ ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
1769
+ // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
1770
+ // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
1771
+ // lost). The eager single reader applies the same rewrites inline.
1772
+ const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
1773
+ const eagerPath = selectEagerPath(
1774
+ process.platform,
1775
+ needsClientRewrite,
1776
+ config.streamMode ?? "auto",
1777
+ );
1778
+ if (eagerPath?.useEagerRelay || win32EagerRewrite) {
1779
+ const turnAc = new AbortController();
1780
+ linkAbortSignal(upstream, turnAc.signal);
1781
+ registerTurn(turnAc, options.turnAdmissionLease);
1782
+ const reportNativeTerminal = recordTerminalOutcomes
1783
+ ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
1784
+ terminalRecorder?.(status, httpStatusOverride);
1785
+ if (status === "failed") {
1786
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
1787
+ || logCtx.terminalHttpStatus === 429
1788
+ || logCtx.terminalHttpStatus === 402
1789
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1790
+ : undefined;
1791
+ if (quotaFailureMessage !== undefined) {
1792
+ recordSubagentQuotaFailureForThreadSpawn(
1793
+ req.headers,
1794
+ subagentQuotaFailureModel,
1795
+ quotaFailureMessage,
1796
+ config,
1797
+ subagentFallbackAccountId,
1798
+ );
1799
+ }
1800
+ }
1801
+ options.onNativePassthroughTerminal?.(status);
1802
+ }
1803
+ : undefined;
1804
+ const inspector = createSseInspector({
1805
+ onTerminal: reportNativeTerminal,
1806
+ logCtx,
1807
+ onCompletedResponse: rememberPassthroughResponse,
1808
+ onFirstOutput: options.onFirstOutput,
1809
+ });
1810
+ const eagerBody = relaySseEagerBounded(upstreamResponse.body, turnAc, {
1811
+ inspectChunk: chunk => inspector.feed(chunk),
1812
+ finishInspection: () => inspector.finish(),
1813
+ disposeInspection: () => inspector.dispose(),
1814
+ sawTerminal: () => inspector.reported(),
1815
+ ...(win32EagerRewrite
1816
+ ? { rewritePayload: composeSsePayloadRewrites(...payloadRewrites) }
1817
+ : {}),
1818
+ onSynthetic: kind => {
1819
+ if (!reportNativeTerminal) return;
1820
+ if (kind === "incomplete") {
1821
+ logCtx.terminalSource = "synthetic";
1822
+ reportNativeTerminal("incomplete");
1823
+ } else {
1824
+ logCtx.transportPhase = "mid_stream";
1825
+ logCtx.terminalSource = "synthetic";
1826
+ reportNativeTerminal("failed", 502);
1827
+ }
1828
+ },
1829
+ onClientCancel: () => options.onNativePassthroughCancel?.(),
1830
+ onDone: () => unregisterTurn(turnAc),
1831
+ }, win32EagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
1832
+ // selectEagerPath admits only no-rewrite traffic on both eligible platforms;
1833
+ // win32 rewrite traffic reaches this relay too, but with the payload rewrite
1834
+ // applied inline — never via an image/item-id JS pull wrapper (#32111, #864).
1835
+ if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
1836
+ return markEagerRelaySseResponse(
1837
+ markNativePassthroughSseResponse(new Response(eagerBody, {
1838
+ status: upstreamResponse.status,
1839
+ headers,
1840
+ })),
1841
+ );
1842
+ }
1843
+ const [nativeBody, inspectBody] = upstreamResponse.body.tee();
1844
+ const turnAc = new AbortController();
1845
+ const clientGone = new AbortController();
1846
+ linkAbortSignal(upstream, turnAc.signal);
1847
+ registerTurn(turnAc, options.turnAdmissionLease);
1848
+ const inspectionConsumerOptions = {
1849
+ clientGoneSignal: clientGone.signal,
1850
+ drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
1851
+ upstream,
1852
+ };
1853
+ if (recordTerminalOutcomes) {
1854
+ // A real terminal was parsed from the (teed) inspection stream — record it as the outcome
1855
+ // even if the client has already disconnected: the turn genuinely reached that terminal, so
1856
+ // it must log as completed/failed, not be dropped or downgraded to a cancel (#44). A pure
1857
+ // client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
1858
+ const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
1859
+ terminalRecorder?.(status, httpStatusOverride);
1860
+ if (status === "failed") {
1861
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
1862
+ || logCtx.terminalHttpStatus === 429
1863
+ || logCtx.terminalHttpStatus === 402
1864
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1865
+ : undefined;
1866
+ if (quotaFailureMessage !== undefined) {
1867
+ recordSubagentQuotaFailureForThreadSpawn(
1868
+ req.headers,
1869
+ subagentQuotaFailureModel,
1870
+ quotaFailureMessage,
1871
+ config,
1872
+ subagentFallbackAccountId,
1873
+ );
1874
+ }
1875
+ }
1876
+ options.onNativePassthroughTerminal?.(status);
1877
+ };
1878
+ consumeForInspection(
1879
+ inspectBody,
1880
+ reportNativeTerminal,
1881
+ turnAc.signal,
1882
+ () => unregisterTurn(turnAc),
1883
+ logCtx,
1884
+ () => options.onNativePassthroughCancel?.(),
1885
+ rememberPassthroughResponse,
1886
+ options.onFirstOutput,
1887
+ inspectionConsumerOptions,
1888
+ );
1889
+ } else {
1890
+ consumeForResponseLogMetadata(
1891
+ inspectBody,
1892
+ logCtx,
1893
+ turnAc.signal,
1894
+ () => unregisterTurn(turnAc),
1895
+ rememberPassthroughResponse,
1896
+ options.onFirstOutput,
1897
+ inspectionConsumerOptions,
1898
+ );
1899
+ }
1900
+ if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
1901
+ // win32 must keep the pure native relay (Bun#32111 JS-sink segfault); elsewhere a JS pull
1902
+ // relay is established practice (relayWithAbort, relaySseWithHeartbeat) and lets a
1903
+ // mid-stream reset end with a clean response.failed terminal instead of a raw socket error.
1904
+ const rewrittenBody = payloadRewrites.length > 0
1905
+ ? relaySseWithPayloadRewrite(nativeBody, composeSsePayloadRewrites(...payloadRewrites), translatorBudget)
1906
+ : nativeBody;
1907
+ const clientBody = process.platform === "win32" && !needsClientRewrite
1908
+ ? nativeBody
1909
+ : relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason));
1910
+ return markNativePassthroughSseResponse(new Response(clientBody, {
1911
+ status: upstreamResponse.status,
1912
+ headers,
1913
+ }));
1914
+ }
1915
+ if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
1916
+ const text = await upstreamResponse.text();
1917
+ inspectResponseLogJson(logCtx, text);
1918
+ if (rememberPassthroughResponse) {
1919
+ try {
1920
+ rememberPassthroughResponse(JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown });
1921
+ } catch { /* non-JSON despite content-type; recording is best-effort */ }
1922
+ }
1923
+ return new Response(restoreImageGenCallsInJson(text, imageGenCallAliases), {
1924
+ status: upstreamResponse.status,
1925
+ statusText: upstreamResponse.statusText,
1926
+ headers,
1927
+ });
1928
+ }
1929
+ const body = relayWithAbort(upstreamResponse.body, upstream);
1930
+ const turnAc = new AbortController();
1931
+ const tracked = body ? trackStreamLifetime(body, turnAc, undefined, options.turnAdmissionLease) : null;
1932
+ return new Response(tracked, {
1933
+ status: upstreamResponse.status,
1934
+ headers,
1935
+ });
1936
+ }
1937
+
1938
+ // Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
1939
+ // Routed-compaction turns must NOT hit the image bridge: compaction clears tools/_webSearch but
1940
+ // leaves _imageGeneration, so planImageBridge would activate and return a normal Responses
1941
+ // completion instead of the synthetic compaction item Codex expects (#424).
1942
+ //
1943
+ // Web-search's loop only supports buildRequest/fetch/parseStream — NOT adapter.runTurn. Sending
1944
+ // Cursor/runTurn requests into runWithWebSearch produces empty HTTP failures. So:
1945
+ // - non-runTurn: web-search wins over image when both eligible (documented priority)
1946
+ // - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
1947
+ // can proceed for web-search-only turns
1948
+ const wsPlan = !routedCompaction
1949
+ ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar)
1950
+ : undefined;
1951
+ const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
1952
+ const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
1953
+ const canRunWebSearch = !!wsPlan && !adapter.runTurn;
1954
+ if ((imgPlan || vidPlan) && canRunWebSearch) {
1955
+ // Web search takes priority when both are active — the media bridge cannot run
1956
+ // alongside runWithWebSearch. Surface a runtime signal so the user knows their
1957
+ // configured video/image bridge was skipped for this turn, rather than silently
1958
+ // dropping a paid capability.
1959
+ if (vidPlan) console.warn("[videos] video bridge skipped: web search is active for this turn");
1960
+ if (imgPlan) console.warn("[images] image bridge skipped: web search is active for this turn");
1961
+ }
1962
+ if ((imgPlan || vidPlan) && (!wsPlan || adapter.runTurn)) {
1963
+ // The image bridge detects a hosted image_generation tool and requires streaming.
1964
+ // The video bridge activates from config and injects a tool — it also needs streaming
1965
+ // (the loop returns SSE). For video-only (no imgPlan) on a non-streaming request, skip
1966
+ // the bridge entirely so enabling the feature doesn't break ordinary non-streaming traffic.
1967
+ if (!parsed.stream) {
1968
+ if (imgPlan) {
1969
+ return formatErrorResponse(400, "invalid_request_error", "image bridge requires stream=true");
1970
+ }
1971
+ // Video-only: skip bridge for non-streaming requests
1972
+ } else {
1973
+ // Replace any pre-existing image_gen/video_gen aliases instead of appending duplicate wire names.
1974
+ const priorTools = parsed.context.tools ?? [];
1975
+ const bridgeTools = [...priorTools.filter(t => {
1976
+ if (t.imageGeneration) return false;
1977
+ if (t.videoGeneration) return false;
1978
+ if (imgPlan && imgPlan.toolNames.has(t.name)) return false;
1979
+ if (imgPlan && t.namespace && imgPlan.toolNames.has(namespacedToolName(t.namespace, t.name))) return false;
1980
+ // Only strip unnamespaced video_gen aliases — a namespaced MCP video_gen is left alone.
1981
+ if (vidPlan && !t.namespace && vidPlan.toolNames.has(t.name)) return false;
1982
+ return true;
1983
+ })];
1984
+ const existingNames = new Set(bridgeTools.map(t => t.name));
1985
+ if (imgPlan && !existingNames.has(IMAGE_GEN_TOOL_NAME)) bridgeTools.push(buildImageTool());
1986
+ if (vidPlan && !existingNames.has(VIDEO_GEN_TOOL_NAME)) bridgeTools.push(buildVideoTool());
1987
+ parsed.context.tools = bridgeTools;
1988
+ // Hosted image_generation tool_choice / allowed_tools must target the synthetic function name.
1989
+ // Gate on imgPlan — in a video-only turn buildImageTool() was never injected, so rewriting
1990
+ // image_generation/image_gen aliases would add an undeclared tool that strict upstreams reject.
1991
+ const tc = parsed.options.toolChoice;
1992
+ if (imgPlan && tc && typeof tc === "object" && "allowedTools" in tc && Array.isArray(tc.allowedTools)) {
1993
+ const mapped = tc.allowedTools.map(name =>
1994
+ name === "image_generation" || name === "image_gen" || (imgPlan.toolNames.has(name) ?? false)
1995
+ ? IMAGE_GEN_TOOL_NAME
1996
+ : name,
1997
+ );
1998
+ parsed.options.toolChoice = { ...tc, allowedTools: [...new Set(mapped)] };
1999
+ } else if (imgPlan && tc && typeof tc === "object" && "name" in tc && typeof tc.name === "string"
2000
+ && (tc.name === "image_generation" || imgPlan.toolNames.has(tc.name))) {
2001
+ parsed.options.toolChoice = { ...tc, name: IMAGE_GEN_TOOL_NAME };
2002
+ }
2003
+ const imgResponse = await runWithImageBridge({
2004
+ parsed, adapter,
2005
+ incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
2006
+ ...(imgPlan ? { plan: imgPlan } : {}),
2007
+ ...(vidPlan ? { videoPlan: vidPlan } : {}),
2008
+ forwardHeaders: selectedForwardHeaders,
2009
+ onAttemptSend: () => noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens),
2010
+ abortSignal: options.abortSignal,
2011
+ maxRounds: imgPlan && vidPlan
2012
+ ? clampImageMaxRounds(Math.min(config.images?.maxRounds ?? 3, config.images?.videoMaxRounds ?? 2))
2013
+ : imgPlan
2014
+ ? clampImageMaxRounds(config.images?.maxRounds)
2015
+ : clampImageMaxRounds(config.images?.videoMaxRounds ?? 2),
2016
+ connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
2017
+ stallTimeoutSec: config.stallTimeoutSec,
2018
+ fetchImpl: providerFetch(route.provider),
2019
+ onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
2020
+ ...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
2021
+ onUsage: usage => {
2022
+ // Cursor may assign _cursorConversationId inside the image loop's first runTurn;
2023
+ // backfill so Logs can filter/total that opening request (parity with the normal
2024
+ // runTurn branch).
2025
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
2026
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
2027
+ }
2028
+ logCtx.usageFromBridge = true;
2029
+ if (usage) {
2030
+ logCtx.usage = usage;
2031
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2032
+ }
2033
+ },
2034
+ on429: retryAfter => {
2035
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
2036
+ retryAfter,
2037
+ now: Date.now(),
2038
+ attemptedKey: route.provider.apiKey,
2039
+ promptCacheKey: parsed.options.promptCacheKey,
2040
+ });
2041
+ if (!rotated) return null;
2042
+ route.provider = rotated;
2043
+ return resolveAdapter(
2044
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2045
+ config.cacheRetention,
2046
+ );
2047
+ },
2048
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2049
+ ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
2050
+ onCompletedResponse: (response, providerState) =>
2051
+ rememberResponseState(
2052
+ parsed._rawBody,
2053
+ response,
2054
+ continuationStateForResponse(providerState),
2055
+ adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2056
+ ),
2057
+ });
2058
+ if (imgResponse.body) {
2059
+ const imgTurnAc = new AbortController();
2060
+ return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, undefined, options.turnAdmissionLease), {
2061
+ status: imgResponse.status,
2062
+ headers: imgResponse.headers,
2063
+ });
2064
+ }
2065
+ return imgResponse;
2066
+ } // end else (streaming bridge)
2067
+ }
2068
+
2069
+ // Web-search sidecar: Codex enabled web_search but this is a routed (non-OpenAI) model that can't
2070
+ // run it server-side. Expose web_search as a function tool and run searches via the gpt-mini sidecar
2071
+ // through the ChatGPT passthrough, looping until the model answers. Otherwise take the normal path.
2072
+ // Placed BEFORE the runTurn early-return for non-runTurn adapters so dual-tool turns dispatch
2073
+ // through web-search instead of being swallowed. runTurn adapters never enter this branch.
2074
+ if (canRunWebSearch && wsPlan) {
2075
+ parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
2076
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
2077
+ const wsResponse = await runWithWebSearch({
2078
+ parsed, adapter,
2079
+ incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
2080
+ backend: wsPlan.backend,
2081
+ forwardProvider: wsPlan.forwardSidecar?.provider,
2082
+ anthropicSidecar: wsPlan.anthropicSidecar,
2083
+ hostedTool: wsPlan.hostedTool,
2084
+ selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? selectedForwardHeaders,
2085
+ settings: wsPlan.settings,
2086
+ maxSearches: wsPlan.maxSearches,
2087
+ forceEmptyResponseId: true,
2088
+ abortSignal: options.abortSignal,
2089
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2090
+ onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
2091
+ onUsage: usage => {
2092
+ logCtx.usageFromBridge = true;
2093
+ if (usage) {
2094
+ logCtx.usage = usage;
2095
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2096
+ }
2097
+ },
2098
+ recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
2099
+ connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
2100
+ routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
2101
+ stallTimeoutSec: wsPlan.stallTimeoutSec,
2102
+ on429: retryAfter => {
2103
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
2104
+ retryAfter,
2105
+ now: Date.now(),
2106
+ attemptedKey: route.provider.apiKey,
2107
+ promptCacheKey: parsed.options.promptCacheKey,
2108
+ });
2109
+ if (!rotated) return null;
2110
+ route.provider = rotated;
2111
+ return resolveAdapter(
2112
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2113
+ config.cacheRetention,
2114
+ );
2115
+ },
2116
+ });
2117
+ // Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
2118
+ // in-flight web-search turns instead of skipping them during graceful shutdown.
2119
+ if (wsResponse.body) {
2120
+ const wsTurnAc = new AbortController();
2121
+ return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, undefined, options.turnAdmissionLease), {
2122
+ status: wsResponse.status,
2123
+ headers: wsResponse.headers,
2124
+ });
2125
+ }
2126
+ return wsResponse;
2127
+ }
2128
+
2129
+ if (adapter.runTurn) {
2130
+ const runTurnAbort = new AbortController();
2131
+ linkAbortSignal(runTurnAbort, options.abortSignal);
2132
+ const queue = createAdapterEventQueue({
2133
+ onBacklogExceeded: () => runTurnAbort.abort(),
2134
+ });
2135
+ const runTurn = async (): Promise<void> => {
2136
+ try {
2137
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
2138
+ await adapter.runTurn?.(
2139
+ parsed,
2140
+ { headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal, translatorBudget },
2141
+ queue.push,
2142
+ );
2143
+ } catch (err) {
2144
+ queue.push({
2145
+ type: "error",
2146
+ message: err instanceof Error ? err.message : String(err),
2147
+ });
2148
+ } finally {
2149
+ // Cursor assigns a stable conversation id inside runTurn on the first headerless
2150
+ // turn; backfill so Logs can filter/total that opening request (#330 / #522).
2151
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
2152
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
2153
+ }
2154
+ queue.close();
2155
+ }
2156
+ };
2157
+
2158
+ const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
2159
+ if (parsed.stream) {
2160
+ void runTurn();
2161
+ let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
2162
+ if (options.comboAttempt) {
2163
+ const preflight = await preflightAdapterEvents(eventSource);
2164
+ if (preflight.error || preflight.empty) {
2165
+ runTurnAbort.abort();
2166
+ queue.close();
2167
+ const message = preflight.error?.message ?? "Adapter ended before producing a response";
2168
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
2169
+ }
2170
+ eventSource = preflight.stream;
2171
+ }
2172
+ const sseStream = bridgeToResponsesSSE(
2173
+ eventSource, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
2174
+ () => {
2175
+ runTurnAbort.abort();
2176
+ queue.close();
2177
+ }, 2_000,
2178
+ {
2179
+ translatorBudget,
2180
+ ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
2181
+ stallTimeoutSec: config.stallTimeoutSec,
2182
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
2183
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2184
+ ...(routedCompaction ? { compaction: true } : {}),
2185
+ onUsage: usage => {
2186
+ // Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
2187
+ // zero-default detail objects, so provenance must come from here (cache_detail_missing).
2188
+ logCtx.usageFromBridge = true;
2189
+ if (usage) {
2190
+ logCtx.usage = usage;
2191
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2192
+ }
2193
+ },
2194
+ ...(routedCompaction ? {} : {
2195
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
2196
+ rememberResponseState(
2197
+ parsed._rawBody,
2198
+ response,
2199
+ continuationStateForResponse(providerState),
2200
+ adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2201
+ ),
2202
+ }),
2203
+ },
2204
+ );
2205
+ const bridgeTurnAc = new AbortController();
2206
+ const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
2207
+ return new Response(trackedSse, {
2208
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
2209
+ });
2210
+ }
2211
+
2212
+ await runTurn();
2213
+ const events = await queue.collect();
2214
+ if (options.comboAttempt) {
2215
+ const firstMeaningful = events.find(event => event.type !== "heartbeat");
2216
+ if (!firstMeaningful || firstMeaningful.type === "error") {
2217
+ const message = firstMeaningful?.type === "error"
2218
+ ? firstMeaningful.message
2219
+ : "Adapter ended before producing a response";
2220
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
2221
+ }
2222
+ }
2223
+ let providerState: OcxProviderContinuationState | undefined;
2224
+ const json = buildResponseJSON(events, parsed.modelId, {
2225
+ translatorBudget,
2226
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
2227
+ toolNsMap,
2228
+ freeformToolNames,
2229
+ toolSearchToolNames,
2230
+ ...(routedCompaction ? { compaction: true } : {}),
2231
+ onProviderState: state => { providerState = state; },
2232
+ onUsage: usage => {
2233
+ logCtx.usageFromBridge = true;
2234
+ if (usage) {
2235
+ logCtx.usage = usage;
2236
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2237
+ }
2238
+ },
2239
+ });
2240
+ if (!routedCompaction) {
2241
+ rememberResponseState(
2242
+ parsed._rawBody,
2243
+ json,
2244
+ continuationStateForResponse(providerState),
2245
+ adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2246
+ );
2247
+ }
2248
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
2249
+ }
2250
+
2251
+ const upstream = new AbortController();
2252
+ const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
2253
+ const connectMs = config.connectTimeoutMs ?? 200_000;
2254
+ let activeAdapter = adapter;
2255
+
2256
+ const request = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
2257
+ recordAdapterReasoning(logCtx, request);
2258
+ const inputTokenEstimate = typeof request.usageLog?.inputTokens === "number"
2259
+ ? request.usageLog.inputTokens
2260
+ : undefined;
2261
+ if (inputTokenEstimate !== undefined) logCtx.usageLogInputTokens = inputTokenEstimate;
2262
+ let upstreamResponse: Response;
2263
+ try {
2264
+ if (activeAdapter.fetchResponse) {
2265
+ noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
2266
+ upstreamResponse = await activeAdapter.fetchResponse(request, {
2267
+ abortSignal: upstream.signal,
2268
+ timeoutMs: connectMs,
2269
+ stream: parsed.stream,
2270
+ });
2271
+ } else {
2272
+ upstreamResponse = await fetchWithResetRetry(
2273
+ recovery => {
2274
+ noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery);
2275
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
2276
+ method: request.method,
2277
+ headers: request.headers,
2278
+ body: request.body,
2279
+ }, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
2280
+ },
2281
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
2282
+ );
2283
+ }
2284
+ } catch (err) {
2285
+ cleanupUpstreamAbort();
2286
+ upstream.abort();
2287
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
2288
+ const msg = describeUpstreamConnectFailure(err, connectMs);
2289
+ return formatErrorResponse(502, "upstream_error", msg);
2290
+ } finally {
2291
+ request.releaseBodyObservation?.();
2292
+ }
2293
+
2294
+ if (!upstreamResponse.ok) {
2295
+ // Recovery loop: multi-key 429 failover + at most ONE anthropic 413 tightened retry
2296
+ // (devlog/260714_image_normalization_pipeline/030). One mutable activeAdapter serves
2297
+ // both paths so a 429→413 sequence never rebuilds against a stale pre-rotation
2298
+ // adapter, and imageTierBias — once armed — rides EVERY subsequent rebuild so a
2299
+ // 413→429 rotation cannot silently undo the tightening.
2300
+ let imageTierBias = 0;
2301
+ let imageRetryAttempted = false;
2302
+ let oauth401ReplayAttempted = false;
2303
+ const rebuildAndRefetch = async (
2304
+ recovery: AttemptRecoveryKind,
2305
+ ): Promise<Response | { failed: Response }> => {
2306
+ const retryRequest = await activeAdapter.buildRequest(parsed, {
2307
+ headers: selectedForwardHeaders,
2308
+ translatorBudget,
2309
+ ...(imageTierBias > 0 ? { imageTierBias } : {}),
2310
+ });
2311
+ recordAdapterReasoning(logCtx, retryRequest);
2312
+ const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
2313
+ ? retryRequest.usageLog.inputTokens
2314
+ : undefined;
2315
+ if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
2316
+ logCtx.providerAdapter = activeAdapter.name;
2317
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
2318
+ noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
2319
+ try {
2320
+ try {
2321
+ return activeAdapter.fetchResponse
2322
+ ? await activeAdapter.fetchResponse(retryRequest, { abortSignal: upstream.signal, timeoutMs: connectMs, stream: parsed.stream })
2323
+ : await fetchWithHeaderTimeout(retryRequest.url, {
2324
+ method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
2325
+ }, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
2326
+ } finally {
2327
+ retryRequest.releaseBodyObservation?.();
2328
+ }
2329
+ } catch (err) {
2330
+ cleanupUpstreamAbort();
2331
+ upstream.abort();
2332
+ if (options.abortSignal?.aborted) {
2333
+ return { failed: clientCancelledResponse() };
2334
+ }
2335
+ const msg = describeUpstreamConnectFailure(err, connectMs);
2336
+ return { failed: formatErrorResponse(502, "upstream_error", msg) };
2337
+ }
2338
+ };
2339
+ recovery: for (;;) {
2340
+ if (
2341
+ upstreamResponse.status === 401
2342
+ && isOAuth401ReplayProvider
2343
+ && sentOAuthSnapshot
2344
+ && !oauth401ReplayAttempted
2345
+ ) {
2346
+ oauth401ReplayAttempted = true;
2347
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
2348
+ let refreshed: OAuthAccessSnapshot;
2349
+ try {
2350
+ refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
2351
+ } catch (err) {
2352
+ cleanupUpstreamAbort();
2353
+ return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
2354
+ }
2355
+ sentOAuthSnapshot = refreshed;
2356
+ if (route.providerName === "kiro") {
2357
+ parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
2358
+ }
2359
+ const refreshedProvider = resolveProviderTransport(
2360
+ route.providerName,
2361
+ { ...route.provider, apiKey: refreshed.accessToken },
2362
+ parsed.options.promptCacheKey,
2363
+ route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
2364
+ );
2365
+ route.provider = refreshedProvider;
2366
+ activeAdapter = resolveAdapter(
2367
+ resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
2368
+ config.cacheRetention,
2369
+ );
2370
+ const result = await rebuildAndRefetch("oauth-401");
2371
+ if ("failed" in result) return result.failed;
2372
+ upstreamResponse = result;
2373
+ continue recovery;
2374
+ }
2375
+
2376
+ // Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
2377
+ // SAME request once per remaining key. OAuth/forward providers and single-key pools
2378
+ // return null immediately, so this stays a no-op for them (src/providers/key-failover.ts).
2379
+ while (upstreamResponse.status === 429 && hasKeyPoolFailover(route.provider)) {
2380
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
2381
+ retryAfter: upstreamResponse.headers.get("retry-after"),
2382
+ now: Date.now(),
2383
+ attemptedKey: route.provider.apiKey,
2384
+ promptCacheKey: parsed.options.promptCacheKey,
2385
+ });
2386
+ if (!rotated) break;
2387
+ // Release the failed response's socket before retrying; unread bodies otherwise linger
2388
+ // until runtime cleanup (one per rotated key under a rate-limit storm).
2389
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
2390
+ route.provider = rotated;
2391
+ activeAdapter = resolveAdapter(
2392
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2393
+ config.cacheRetention,
2394
+ );
2395
+ const result = await rebuildAndRefetch("key-429");
2396
+ if ("failed" in result) return result.failed;
2397
+ upstreamResponse = result;
2398
+ }
2399
+
2400
+ // Opt-in Anthropic OAuth account pool (#294): cool the failed account and retry
2401
+ // with another eligible OAuth account (bounded per request). Disabled by default.
2402
+ while (
2403
+ upstreamResponse.status === 429
2404
+ && anthropicPoolAccountId
2405
+ && isAnthropicAccountPoolEnabled(config)
2406
+ && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
2407
+ ) {
2408
+ const nextAccountId = rotateAnthropicAccountOn429(
2409
+ config,
2410
+ anthropicPoolAccountId,
2411
+ upstreamResponse.headers.get("retry-after"),
2412
+ anthropicSessionKey,
2413
+ );
2414
+ if (!nextAccountId) break;
2415
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
2416
+ try {
2417
+ const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
2418
+ anthropicPoolAccountId = nextAccountId;
2419
+ anthropicPoolFailovers += 1;
2420
+ route.provider = { ...route.provider, apiKey: accessToken };
2421
+ promoteAnthropicActiveAccount(nextAccountId);
2422
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
2423
+ activeAdapter = resolveAdapter(
2424
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2425
+ config.cacheRetention,
2426
+ );
2427
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
2428
+ const result = await rebuildAndRefetch("anthropic-oauth-429");
2429
+ if ("failed" in result) return result.failed;
2430
+ upstreamResponse = result;
2431
+ } catch {
2432
+ break;
2433
+ }
2434
+ }
2435
+ // Anthropic 413 request_too_large: rebuild once with every image one tier lower
2436
+ // (spiral guard: single attempt). The biased response re-enters the 429 check above.
2437
+ if (shouldAttemptImageTierRetry({
2438
+ status: upstreamResponse.status,
2439
+ adapterName: activeAdapter.name,
2440
+ parsed,
2441
+ alreadyAttempted: imageRetryAttempted,
2442
+ })) {
2443
+ imageRetryAttempted = true;
2444
+ imageTierBias = 1;
2445
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
2446
+ const result = await rebuildAndRefetch("image-413");
2447
+ if ("failed" in result) return result.failed;
2448
+ upstreamResponse = result;
2449
+ continue recovery;
2450
+ }
2451
+ break;
2452
+ }
2453
+ if (!upstreamResponse.ok) {
2454
+ if (options.comboAttempt) {
2455
+ const failure = await consumeComboFailure(upstreamResponse, options.abortSignal)
2456
+ .finally(cleanupUpstreamAbort);
2457
+ options.onConsumedComboFailure?.(failure);
2458
+ return failure.response;
2459
+ }
2460
+ const errorText = await upstreamResponse.text().catch(() => "unknown error");
2461
+ cleanupUpstreamAbort();
2462
+ recordSubagentQuotaFailureForThreadSpawn(
2463
+ req.headers,
2464
+ subagentQuotaFailureModel,
2465
+ upstreamResponse.status === 429 || upstreamResponse.status === 402
2466
+ ? upstreamResponse.status
2467
+ : `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
2468
+ config,
2469
+ subagentFallbackAccountId,
2470
+ );
2471
+ // Upstreams occasionally echo request details in error bodies — scrub token-shaped
2472
+ // material before it reaches the client-facing error surface.
2473
+ const message = `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`;
2474
+ const retryAfter = resolveClientRetryAfter({
2475
+ status: upstreamResponse.status,
2476
+ message,
2477
+ upstreamRetryAfter: upstreamResponse.headers.get("retry-after"),
2478
+ });
2479
+ return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
2480
+ ...(retryAfter !== undefined ? { retryAfter } : {}),
2481
+ });
2482
+ }
2483
+ }
2484
+
2485
+ cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
2486
+
2487
+ // Claude can return a clean end_turn after announcing an edit without emitting any tool call.
2488
+ // Keep the normal request/recovery path above intact, and use this bounded callback only for the
2489
+ // one internal continuation pass. A continuation failure becomes an in-stream adapter error so
2490
+ // the client never sees a second hidden HTTP response or an unbounded retry loop.
2491
+ const terminalGuardEnabled = activeAdapter.name === "anthropic" && !options.comboAttempt && !routedCompaction;
2492
+ const fetchTerminalGuardContinuation = async function* (nextParsed: OcxParsedRequest): AsyncGenerator<AdapterEvent> {
2493
+ let imageTierBias = 0;
2494
+ let response: Response | undefined;
2495
+ while (true) {
2496
+ try {
2497
+ const continuationRequest = await activeAdapter.buildRequest(nextParsed, {
2498
+ headers: selectedForwardHeaders,
2499
+ translatorBudget,
2500
+ ...(imageTierBias > 0 ? { imageTierBias } : {}),
2501
+ });
2502
+ recordAdapterReasoning(logCtx, continuationRequest);
2503
+ const continuationEstimate = typeof continuationRequest.usageLog?.inputTokens === "number"
2504
+ ? continuationRequest.usageLog.inputTokens
2505
+ : undefined;
2506
+ if (continuationEstimate !== undefined) logCtx.usageLogInputTokens = continuationEstimate;
2507
+ try {
2508
+ if (activeAdapter.fetchResponse) {
2509
+ noteAttemptSend(logCtx.activeAttempt, continuationEstimate);
2510
+ response = await activeAdapter.fetchResponse(continuationRequest, {
2511
+ abortSignal: upstream.signal,
2512
+ timeoutMs: connectMs,
2513
+ stream: nextParsed.stream,
2514
+ });
2515
+ } else {
2516
+ response = await fetchWithResetRetry(
2517
+ recovery => {
2518
+ noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery);
2519
+ return fetchWithHeaderTimeout(
2520
+ continuationRequest.url,
2521
+ applyUpstreamRecoveryInit({
2522
+ method: continuationRequest.method,
2523
+ headers: continuationRequest.headers,
2524
+ body: continuationRequest.body,
2525
+ }, recovery),
2526
+ upstream.signal,
2527
+ connectMs,
2528
+ nextParsed.stream,
2529
+ providerFetch(route.provider),
2530
+ );
2531
+ },
2532
+ { abortSignal: upstream.signal, label: safeHostLabel(continuationRequest.url) },
2533
+ );
2534
+ }
2535
+ } finally {
2536
+ continuationRequest.releaseBodyObservation?.();
2537
+ }
2538
+ } catch (error) {
2539
+ if (options.abortSignal?.aborted) {
2540
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
2541
+ } else {
2542
+ yield { type: "error", message: `Provider continuation failed: ${error instanceof Error ? error.message : String(error)}` };
2543
+ }
2544
+ return;
2545
+ }
2546
+
2547
+ if (response.status === 429 && hasKeyPoolFailover(route.provider)) {
2548
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
2549
+ retryAfter: response.headers.get("retry-after"),
2550
+ now: Date.now(),
2551
+ attemptedKey: route.provider.apiKey,
2552
+ promptCacheKey: nextParsed.options.promptCacheKey,
2553
+ });
2554
+ if (rotated) {
2555
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
2556
+ route.provider = rotated;
2557
+ activeAdapter = resolveAdapter(
2558
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2559
+ config.cacheRetention,
2560
+ );
2561
+ continue;
2562
+ }
2563
+ }
2564
+ if (
2565
+ response.status === 429
2566
+ && anthropicPoolAccountId
2567
+ && isAnthropicAccountPoolEnabled(config)
2568
+ && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
2569
+ ) {
2570
+ const nextAccountId = rotateAnthropicAccountOn429(
2571
+ config,
2572
+ anthropicPoolAccountId,
2573
+ response.headers.get("retry-after"),
2574
+ anthropicSessionKey,
2575
+ );
2576
+ if (nextAccountId) {
2577
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
2578
+ try {
2579
+ const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
2580
+ anthropicPoolAccountId = nextAccountId;
2581
+ anthropicPoolFailovers += 1;
2582
+ route.provider = { ...route.provider, apiKey: accessToken };
2583
+ promoteAnthropicActiveAccount(nextAccountId);
2584
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
2585
+ activeAdapter = resolveAdapter(
2586
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2587
+ config.cacheRetention,
2588
+ );
2589
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
2590
+ continue;
2591
+ } catch {
2592
+ // fall through to emit continuation error below
2593
+ }
2594
+ }
2595
+ }
2596
+ if (shouldAttemptImageTierRetry({
2597
+ status: response.status,
2598
+ adapterName: activeAdapter.name,
2599
+ parsed: nextParsed,
2600
+ alreadyAttempted: imageTierBias > 0,
2601
+ })) {
2602
+ imageTierBias = 1;
2603
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
2604
+ continue;
2605
+ }
2606
+ break;
2607
+ }
2608
+
2609
+ if (!response.ok) {
2610
+ const errorText = await response.text().catch(() => "unknown error");
2611
+ yield {
2612
+ type: "error",
2613
+ status: response.status,
2614
+ message: `Provider continuation error ${response.status}: ${redactSecretString(errorText.slice(0, 500))}`,
2615
+ };
2616
+ return;
2617
+ }
2618
+
2619
+ try {
2620
+ // Protect the continuation body against a client abort landing between fetch resolution and
2621
+ // reader attach, exactly as the initial response is guarded above (#390/366e3053). Without
2622
+ // this, a client cancel during the continuation reopens the Bun fetch-to-reader abort race.
2623
+ const detachContinuationBodyGuard = cancelBodyOnAbort(response.body, upstream.signal);
2624
+ try {
2625
+ if (nextParsed.stream) {
2626
+ yield* activeAdapter.parseStream(response, translatorBudget);
2627
+ } else if (activeAdapter.parseResponse) {
2628
+ yield* await activeAdapter.parseResponse(response, translatorBudget);
2629
+ } else {
2630
+ yield { type: "error", message: "Provider continuation does not support response parsing" };
2631
+ }
2632
+ } finally {
2633
+ detachContinuationBodyGuard();
2634
+ }
2635
+ } catch (error) {
2636
+ if (options.abortSignal?.aborted) {
2637
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
2638
+ } else {
2639
+ yield { type: "error", message: `Provider continuation parse failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
2640
+ }
2641
+ }
2642
+ };
2643
+
2644
+ if (parsed.stream) {
2645
+ const initialEventStream = activeAdapter.parseStream(upstreamResponse, translatorBudget);
2646
+ const eventStream = terminalGuardEnabled
2647
+ ? guardTerminalEventStream({
2648
+ parsed,
2649
+ firstEvents: initialEventStream,
2650
+ adapterName: activeAdapter.name,
2651
+ maxAutoContinuations: 1,
2652
+ continuation: fetchTerminalGuardContinuation,
2653
+ })
2654
+ : initialEventStream;
2655
+ const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
2656
+ const sseStream = bridgeToResponsesSSE(
2657
+ eventStream, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
2658
+ () => upstream.abort(), 2_000,
2659
+ {
2660
+ translatorBudget,
2661
+ ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
2662
+ stallTimeoutSec: config.stallTimeoutSec,
2663
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
2664
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2665
+ ...(routedCompaction ? { compaction: true } : {}),
2666
+ onUsage: usage => {
2667
+ // Raw adapter usage, pre wire-normalization (see the runTurn branch above).
2668
+ logCtx.usageFromBridge = true;
2669
+ if (usage) {
2670
+ logCtx.usage = usage;
2671
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2672
+ }
2673
+ },
2674
+ // Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
2675
+ // PRE-compaction history, and a later previous_response_id expansion would rehydrate the
2676
+ // giant stale chain Codex just replaced.
2677
+ ...(routedCompaction ? {} : {
2678
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
2679
+ rememberResponseState(
2680
+ parsed._rawBody,
2681
+ response,
2682
+ continuationStateForResponse(providerState),
2683
+ activeAdapter.name === "kiro" ? { force: true } : undefined,
2684
+ ),
2685
+ }),
2686
+ },
2687
+ );
2688
+ const bridgeTurnAc = new AbortController();
2689
+ const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, cleanupUpstreamAbort, options.turnAdmissionLease);
2690
+ return new Response(trackedSse, {
2691
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
2692
+ });
2693
+ }
2694
+
2695
+ if (activeAdapter.parseResponse) {
2696
+ let events: AdapterEvent[];
2697
+ try {
2698
+ const initialEvents = await activeAdapter.parseResponse(upstreamResponse, translatorBudget);
2699
+ if (terminalGuardEnabled) {
2700
+ events = [];
2701
+ for await (const event of guardTerminalEventStream({
2702
+ parsed,
2703
+ firstEvents: (async function* () { yield* initialEvents; })(),
2704
+ adapterName: activeAdapter.name,
2705
+ maxAutoContinuations: 1,
2706
+ continuation: fetchTerminalGuardContinuation,
2707
+ })) events.push(event);
2708
+ } else {
2709
+ events = initialEvents;
2710
+ }
2711
+ } finally {
2712
+ cleanupUpstreamAbort();
2713
+ }
2714
+ const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
2715
+ let providerState: OcxProviderContinuationState | undefined;
2716
+ const json = buildResponseJSON(events, parsed.modelId, {
2717
+ translatorBudget,
2718
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
2719
+ toolNsMap,
2720
+ freeformToolNames,
2721
+ toolSearchToolNames,
2722
+ ...(routedCompaction ? { compaction: true } : {}),
2723
+ onProviderState: state => { providerState = state; },
2724
+ onUsage: usage => {
2725
+ logCtx.usageFromBridge = true;
2726
+ if (usage) {
2727
+ logCtx.usage = usage;
2728
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2729
+ }
2730
+ },
2731
+ });
2732
+ // See the streaming branch: compaction turns skip the continuation cache.
2733
+ if (!routedCompaction) {
2734
+ rememberResponseState(
2735
+ parsed._rawBody,
2736
+ json,
2737
+ continuationStateForResponse(providerState),
2738
+ activeAdapter.name === "kiro" ? { force: true } : undefined,
2739
+ );
2740
+ }
2741
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
2742
+ }
2743
+
2744
+ return formatErrorResponse(400, "invalid_request_error", "Non-streaming not supported by this adapter");
2745
+ }
2746
+
2747
+
2748
+
2749
+ export function linkAbortSignal(upstream: AbortController, signal?: AbortSignal): () => void {
2750
+ if (!signal) return () => {};
2751
+ if (signal.aborted) {
2752
+ upstream.abort(signal.reason);
2753
+ return () => {};
2754
+ }
2755
+ const onAbort = () => upstream.abort(signal.reason);
2756
+ signal.addEventListener("abort", onAbort, { once: true });
2757
+ return () => signal.removeEventListener("abort", onAbort);
2758
+ }