@iislee/opencodex 2.11.0 → 2.31.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 (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,27 +1,64 @@
1
1
  import type { Server } from "bun";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
3
4
  import { formatPassthroughUpstreamError } from "./passthrough-error";
5
+ import {
6
+ createResponsesFieldBackfillBlockRewrite,
7
+ backfillResponsesFieldsJson,
8
+ } from "./responses-field-backfill";
9
+ import { checkInputAdmission } from "./input-admission";
10
+ import { nativeContextLimits } from "../../codex/catalog";
4
11
  import { describeUpstreamConnectFailure } from "./upstream-error";
5
12
  import {
6
- getConfigPath,
7
13
  multiAgentGuidanceEnabled,
8
14
  resolveEnvValue,
9
15
  } from "../../config";
10
16
  import { parseRequest } from "../../responses/parser";
17
+ import {
18
+ bindReasoningReplayScope,
19
+ commitReasoningReplayServingIdentity,
20
+ reasoningReplayCodexCredentialIdentity,
21
+ reasoningReplayDestinationIdentity,
22
+ durableReplayDestinationIdentity,
23
+ durableReplayCredentialIdentity,
24
+ reasoningReplayKeyCredentialIdentity,
25
+ reasoningReplayOAuthCredentialIdentity,
26
+ reasoningReplayServingIdentityChanged,
27
+ } from "../../responses/reasoning-replay-cache";
28
+ import { awaitThoughtSignatureDurability, thoughtSignatureReplaySalt } from "../../responses/thought-signature-replay";
11
29
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
12
30
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
13
31
  import {
32
+ copyPreviousResponseReplayProvenance,
14
33
  expandPreviousResponseInput,
34
+ markBodyNonPersistable,
15
35
  previousResponseProviderState,
16
36
  previousResponseReplayFailure,
37
+ previousResponseScopeMismatch,
17
38
  rememberResponseState,
18
39
  } from "../../responses/state";
19
- import { routeModel, type RouteResult } from "../../router";
40
+ import {
41
+ isValidProviderContinuationOwner,
42
+ mergeProviderContinuationPayload,
43
+ providerContinuationOwnerFromReplayIdentity,
44
+ providerContinuationRouteScope,
45
+ sameProviderContinuationOwner,
46
+ } from "../../responses/provider-continuation";
47
+ import {
48
+ comboRouteDecisionTrace,
49
+ NoEligiblePolicyCandidateError,
50
+ routeConcreteModel,
51
+ routeModel,
52
+ type RouteResult,
53
+ } from "../../router";
54
+ import { evidenceFromBody } from "../../routing/request-evidence";
55
+ import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
20
56
  import {
21
57
  advanceComboAfterFailure,
22
58
  comboDefaultEffort,
23
59
  comboFailureDecision,
24
60
  comboIdFromRawBody,
61
+ comboRequestHasImageInput,
25
62
  concreteComboRequestBody,
26
63
  getCombo,
27
64
  isComboTargetInCooldown,
@@ -34,14 +71,25 @@ import {
34
71
  import { isInjectionDebugEnabled } from "../../lib/debug-settings";
35
72
  import { injectionDebugLog } from "../../lib/injection-debug-log";
36
73
  import { resolveClientRetryAfter } from "../../lib/retry-after";
74
+ import { enrichOpenCodeZenRateLimitMessage } from "../../providers/opencode-zen-rate-limit";
37
75
  import { modelInList, namespacedToolName } from "../../types";
38
- import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
76
+ import type {
77
+ AdapterEvent,
78
+ OcxConfig,
79
+ OcxParsedRequest,
80
+ OcxProviderConfig,
81
+ OcxProviderContinuationOwner,
82
+ OcxProviderContinuationState,
83
+ OcxReasoningReplayIdentity,
84
+ OcxUsage,
85
+ TierDecision,
86
+ } from "../../types";
39
87
  import {
40
88
  forceRefreshOAuthAccessSnapshot,
41
89
  getOAuthCredentialApiBaseUrl,
42
- getOAuthCredentialProjectId,
43
90
  getValidAccessTokenForAccount,
44
91
  getValidAccessTokenSnapshot,
92
+ publicOAuthAuthenticationErrorMessage,
45
93
  type OAuthAccessSnapshot,
46
94
  UnsupportedOAuthProviderError,
47
95
  } from "../../oauth";
@@ -59,17 +107,22 @@ import {
59
107
  } from "../../oauth/anthropic-routing";
60
108
  import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
61
109
  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";
110
+ import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
111
+ import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
64
112
  import {
65
113
  applyCodexAuthContextToProvider,
114
+ codexPoolAffinityKey,
66
115
  CodexAccountCooldownError,
116
+ codexMainProfileDrainingResponse,
67
117
  cooldownErrorResponse,
68
118
  CodexAuthContextError,
69
119
  CodexDirectAuthenticationError,
120
+ CodexMainProfileDrainingError,
70
121
  CodexPoolAuthenticationError,
71
122
  CodexThreadAffinityExpiredError,
72
123
  headersForCodexAuthContext,
124
+ materializeCodexUpstreamAuth,
125
+ CodexMainSubstitutionUnavailableError,
73
126
  isCodexAuthContextUsable,
74
127
  resolveCodexAuthContext,
75
128
  codexProbeLeaseId,
@@ -78,30 +131,72 @@ import {
78
131
  stripCodexRuntimeProviderFields,
79
132
  type CodexAuthContext,
80
133
  } from "../../codex/auth-context";
134
+ import {
135
+ entitledCodexAccountIdsForModel,
136
+ invalidateCodexModelEntitlementsForAccount,
137
+ resolveCodexModelEntitlements,
138
+ } from "../../codex/model-entitlements";
139
+ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
140
+ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
81
141
  import {
82
142
  computeQuotaCooldown,
143
+ codexQuotaScopeForModel,
83
144
  formatCodexProviderForLog,
84
145
  previewCodexAccountForRequest,
85
146
  recordCodexUpstreamOutcome,
86
147
  type CodexUpstreamOutcome,
87
148
  } from "../../codex/routing";
88
- import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../../lib/upstream-retry";
149
+ import { codexAuthContextLogLabel } from "../../codex/account-label";
150
+ import {
151
+ applyUpstreamRecoveryInit,
152
+ fetchWithResetRetry,
153
+ fetchWithTransientRetry,
154
+ prepareSameTarget429Wait,
155
+ } from "../../lib/upstream-retry";
89
156
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
157
+ import type { DataPlaneAdmission } from "../auth-cors";
90
158
  import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
91
159
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
92
- import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
160
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
161
+ import { providerContextCap } from "../../providers/context-cap";
162
+ import {
163
+ fastPolicyForModel,
164
+ serviceTierSupportFromPolicy,
165
+ SERVICE_TIER_ADAPTERS,
166
+ } from "../../providers/service-tier";
167
+ import {
168
+ canonicalFastTierMarker,
169
+ decideTier,
170
+ tierObservationContext,
171
+ tierValueAfterDecision,
172
+ type ResolvedFastPolicy,
173
+ } from "../../providers/fastwire";
174
+ import {
175
+ RequestPacingQueueOverloadError,
176
+ waitForProviderRequestSlot,
177
+ } from "../../providers/request-pacing";
93
178
  import { slugsEquivalent } from "../../providers/slug-codec";
94
179
  import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
95
180
  import { isUsageDebugEnabled } from "../../usage/debug";
96
181
  import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
97
182
  import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
98
- import type { InboundWire } from "../../providers/registry";
99
- import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover";
183
+ import {
184
+ providerModelResponsesTerminalRepair,
185
+ providerModelResponsesUpstreamStreaming,
186
+ type InboundWire,
187
+ } from "../../providers/registry";
188
+ import type { AdapterRequest } from "../../adapters/base";
189
+ import {
190
+ hasKeyPoolFailover,
191
+ rateLimitRetryDelayMs,
192
+ rateLimitRetryPolicyFor,
193
+ rotateProviderTransportOn429,
194
+ } from "../../providers/key-failover";
100
195
  import { shouldAttemptImageTierRetry } from "../image-retry";
101
196
  import { resolveProviderTransport } from "../../providers/xai-transport";
102
197
  import type { WsData } from "../ws-bridge";
103
- import { registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
104
- import { redactSecretString } from "../../lib/redact";
198
+ import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
199
+ import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
105
200
  import { readBoundedResponseBody } from "../../lib/bounded-body";
106
201
  import type { AdmissionLease } from "../../lib/admission";
107
202
  import { supportedLadderFor } from "../effort-policy";
@@ -111,14 +206,16 @@ import {
111
206
  maybePrimeSubagentQuota,
112
207
  recordSubagentQuotaFailureForThreadSpawn,
113
208
  } from "../../codex/subagent-model-fallback";
209
+ import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
114
210
  import {
115
211
  beginRequestAttempt,
116
- catalogModelSupportsServiceTier,
117
212
  finishRequestAttempt,
118
213
  inspectResponseLogJson,
119
214
  noteAttemptSend,
120
215
  readConfiguredCodexServiceTier,
121
216
  recordAdapterReasoning,
217
+ recordAdapterTier,
218
+ recordAdapterTierMetadata,
122
219
  recordAttemptRequestedEffort,
123
220
  requestLogSpeedLabel,
124
221
  sealRequestAttemptIdentity,
@@ -143,25 +240,85 @@ import {
143
240
  relayWithAbort,
144
241
  sanitizePassthroughHeaders,
145
242
  } from "../relay";
243
+ import {
244
+ agentTaskRecoveryConfig,
245
+ recoverEncryptedAgentTask,
246
+ } from "./agent-task-recovery";
146
247
  import { relaySseEagerBounded } from "../relay-eager";
248
+ import {
249
+ relayResponsesSseWithTerminalRepair,
250
+ type ResponsesTerminalRepairScheduler,
251
+ } from "../responses-terminal-repair";
147
252
  import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
148
253
  import { cancelBodyOnAbort } from "../../lib/abort";
254
+ import { isCodexWsUpstreamResponse, type BunRuntimeGateInput } from "./ws-upstream";
149
255
  import {
150
256
  createResponsesItemIdPayloadRewrite,
151
257
  hasResponsesItemIdRepair,
258
+ repairResponsesJsonItemIds,
152
259
  } from "../responses-item-id-repair";
260
+ import {
261
+ createReasoningSummaryChannelPayloadRewrite,
262
+ rewriteReasoningSummaryInJsonString,
263
+ routeUsesContentChannelReasoning,
264
+ } from "../responses-reasoning-summary-rewrite";
153
265
  import {
154
266
  createImageGenCallRestoreRewrite,
155
267
  imageGenToolCallAliases,
156
268
  restoreImageGenCallsInJson,
157
269
  } from "../responses-image-gen-repair";
158
- import { composeSsePayloadRewrites, relaySseWithPayloadRewrite } from "../sse-payload-rewrite";
270
+ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
159
271
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
160
272
 
161
273
  import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
162
274
  import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
163
- import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./fetch-helpers";
275
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
276
+ import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
277
+ import {
278
+ acquireUpstreamHostAdmission,
279
+ disableUpstreamHostCircuitForKey,
280
+ normalizeUpstreamHostCircuitThreshold,
281
+ recordUpstreamHostFailure,
282
+ releaseUpstreamHostAdmission,
283
+ resetUpstreamHostHealth,
284
+ upstreamHostHealthKey,
285
+ type UpstreamHostAdmissionLease,
286
+ } from "../../codex/upstream-host-health";
287
+ import {
288
+ createResponsesSnapshotBlockRewrite,
289
+ hasResponsesSnapshotRepair,
290
+ repairResponsesSnapshotJson,
291
+ } from "../responses-snapshot-repair";
292
+ import {
293
+ composeSseBlockRewrites,
294
+ composeSsePayloadRewrites,
295
+ payloadRewriteAsBlockRewrite,
296
+ relaySseWithBlockRewrite,
297
+ } from "../sse-payload-rewrite";
298
+ import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
299
+ import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
300
+ import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
301
+ import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
302
+ import {
303
+ createRoutedNamespaceCallRestoreRewrite,
304
+ NamespaceToolCollisionError,
305
+ restoreRoutedNamespaceCallsInJson,
306
+ type RoutedNamespaceToolAliases,
307
+ } from "../../responses/namespace-tool-compat";
308
+ import {
309
+ collectDeclaredWireToolNames,
310
+ createUndeclaredToolCallGuardBlockRewrite,
311
+ undeclaredToolCallMessage,
312
+ undeclaredToolCallName,
313
+ undeclaredToolCallNameInResponse,
314
+ } from "../responses-undeclared-tool-guard";
315
+ import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
316
+ import { responsesJsonToSseStream } from "../responses-json-events";
164
317
  import { guardTerminalEventStream } from "./terminal-guard";
318
+ import {
319
+ emptyCompletionRetryEnabled,
320
+ guardEmptyCompletionEventStream,
321
+ } from "./empty-completion-guard";
165
322
 
166
323
  /**
167
324
  * Adapters whose continuation state must survive Codex's store:false requests.
@@ -173,12 +330,13 @@ export function adapterNeedsForcedContinuation(name: string): boolean {
173
330
  export function sidecarOutcomeRecorder(
174
331
  config: OcxConfig,
175
332
  authCtx: CodexAuthContext,
176
- threadId?: string | null,
177
333
  ): ((outcome: CodexUpstreamOutcome) => void) | undefined {
178
334
  return authCtx.kind === "pool" || authCtx.kind === "main-pool"
179
335
  ? outcome => recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
180
- threadId,
336
+ threadId: authCtx.affinityKey,
337
+ fixedAccount: authCtx.fixedAccount,
181
338
  probeLeaseId: authCtx.probeLeaseId,
339
+ probeQuotaScope: authCtx.probeQuotaScope,
182
340
  writerGeneration: authCtx.writerGeneration,
183
341
  })
184
342
  : undefined;
@@ -186,7 +344,7 @@ export function sidecarOutcomeRecorder(
186
344
 
187
345
 
188
346
 
189
- import { isShadowSourceModel } from "../../lib/shadow-call";
347
+ import { isShadowSourceModel, shadowSourceModelPrefix, shouldInterceptShadowCall } from "../../lib/shadow-call";
190
348
 
191
349
  export { DEFAULT_SHADOW_SOURCE_MODELS, isShadowSourceModel, shadowSourceModels } from "../../lib/shadow-call";
192
350
 
@@ -196,7 +354,338 @@ export function codexLogAccountId(authCtx: CodexAuthContext): string | null {
196
354
  return authCtx.kind === "pool" || authCtx.kind === "main-pool" ? authCtx.accountId : null;
197
355
  }
198
356
 
357
+ type ContinuationOwnerRead =
358
+ | { kind: "missing" }
359
+ | { kind: "invalid" }
360
+ | { kind: "valid"; owner: OcxProviderContinuationOwner };
361
+
362
+ function readProviderContinuationOwner(
363
+ state: OcxProviderContinuationState | undefined,
364
+ ): ContinuationOwnerRead {
365
+ if (!state || state.__ocxOwner === undefined) return { kind: "missing" };
366
+ const owner = state.__ocxOwner;
367
+ if (!isValidProviderContinuationOwner(owner)) return { kind: "invalid" };
368
+ return { kind: "valid", owner: { ...owner } };
369
+ }
370
+
371
+ function providerContinuationPayload(
372
+ state: OcxProviderContinuationState | undefined,
373
+ ): OcxProviderContinuationState | undefined {
374
+ if (!state) return undefined;
375
+ const cloned = structuredClone(state);
376
+ delete cloned.__ocxOwner;
377
+ return Object.keys(cloned).length > 0 ? cloned : undefined;
378
+ }
379
+
380
+ function bindProviderContinuationForRoute(
381
+ parsed: OcxParsedRequest,
382
+ currentOwner: OcxProviderContinuationOwner | undefined,
383
+ ): void {
384
+ const candidate = parsed._providerContinuationCandidate;
385
+ const storedOwner = readProviderContinuationOwner(candidate);
386
+ const mayRestore = storedOwner.kind === "valid"
387
+ && !!currentOwner
388
+ && sameProviderContinuationOwner(storedOwner.owner, currentOwner);
389
+ const restored = mayRestore ? providerContinuationPayload(candidate) : undefined;
390
+ if (restored) parsed._providerContinuation = restored;
391
+ else delete parsed._providerContinuation;
392
+ const cursorConversationId = restored?.cursor?.conversationId;
393
+ if (cursorConversationId) parsed._cursorConversationId = cursorConversationId;
394
+ else delete parsed._cursorConversationId;
395
+ if (currentOwner) parsed._providerContinuationOwner = { ...currentOwner };
396
+ else delete parsed._providerContinuationOwner;
397
+ }
398
+
399
+ function providerContinuationDestinationIdentity(
400
+ parsed: OcxParsedRequest,
401
+ provider: OcxProviderConfig,
402
+ ): string | undefined {
403
+ const kiroContext = parsed._kiroAuthContext;
404
+ return reasoningReplayDestinationIdentity(JSON.stringify([
405
+ provider.baseUrl.trim().replace(/\/+$/, ""),
406
+ provider.responsesPath ?? "",
407
+ kiroContext?.profileArn ?? "",
408
+ kiroContext?.apiRegion ?? "",
409
+ kiroContext?.ssoRegion ?? "",
410
+ ]));
411
+ }
412
+
413
+ function bindRouteReasoningReplayScope(args: {
414
+ parsed: OcxParsedRequest;
415
+ providerName: string;
416
+ provider: OcxProviderConfig;
417
+ adapterName: string;
418
+ oauthCredentialSnapshot?: Pick<OAuthAccessSnapshot, "accountId" | "generation">;
419
+ codexAuthContext?: CodexAuthContext;
420
+ forwardHeaders?: Headers;
421
+ }): void {
422
+ const { parsed, providerName, provider, adapterName } = args;
423
+ let credentialIdentity: string | undefined;
424
+ let credentialDurableIdentity: string | undefined;
425
+ const durableSalt = thoughtSignatureReplaySalt();
426
+ if (provider.authMode === "oauth") {
427
+ credentialIdentity = reasoningReplayOAuthCredentialIdentity(
428
+ args.oauthCredentialSnapshot,
429
+ provider.headers,
430
+ );
431
+ // The persisted account-slot id survives token refresh and restarts; the rotating
432
+ // generation deliberately does NOT participate (#1926 design: rotation-safe).
433
+ credentialDurableIdentity = durableReplayCredentialIdentity(
434
+ "oauth",
435
+ args.oauthCredentialSnapshot?.accountId,
436
+ provider.headers,
437
+ durableSalt,
438
+ );
439
+ } else if (provider.authMode === "forward") {
440
+ const poolContext = args.codexAuthContext?.kind === "pool"
441
+ || args.codexAuthContext?.kind === "main-pool"
442
+ ? args.codexAuthContext
443
+ : undefined;
444
+ credentialIdentity = reasoningReplayCodexCredentialIdentity({
445
+ authorization: poolContext
446
+ ? `Bearer ${poolContext.accessToken}`
447
+ : args.forwardHeaders?.get("authorization"),
448
+ chatgptAccountId: poolContext?.chatgptAccountId
449
+ ?? args.forwardHeaders?.get("chatgpt-account-id"),
450
+ accountId: poolContext?.accountId,
451
+ credentialGeneration: poolContext?.kind === "pool"
452
+ ? poolContext.generation
453
+ : undefined,
454
+ writerGeneration: poolContext?.writerGeneration,
455
+ headers: provider.headers,
456
+ });
457
+ // Durable identity requires a STABLE, TRUSTED account handle. Pool context comes from
458
+ // our own account store; a client-supplied chatgpt-account-id header is attacker
459
+ // -influenceable bucket selection and a bearer alone is rotating material — both are
460
+ // refused, so direct-forward turns get no durable scope (fail closed; the in-process
461
+ // cache still covers same-process replay).
462
+ const codexDurableHandle = poolContext?.accountId
463
+ ?? poolContext?.chatgptAccountId
464
+ ?? undefined;
465
+ credentialDurableIdentity = durableReplayCredentialIdentity(
466
+ "codex",
467
+ codexDurableHandle ?? undefined,
468
+ provider.headers,
469
+ durableSalt,
470
+ );
471
+ } else if (provider.authMode !== "local") {
472
+ credentialIdentity = reasoningReplayKeyCredentialIdentity(provider);
473
+ credentialDurableIdentity = durableReplayCredentialIdentity(
474
+ "key",
475
+ nonEmptyProviderApiKey(provider),
476
+ provider.headers,
477
+ durableSalt,
478
+ );
479
+ }
480
+ const providerDestinationIdentity = reasoningReplayDestinationIdentity(provider.baseUrl);
481
+ const replayIdentity: OcxReasoningReplayIdentity | undefined = credentialIdentity && providerDestinationIdentity
482
+ ? {
483
+ providerName,
484
+ providerDestinationIdentity,
485
+ providerDestinationDurableIdentity: durableReplayDestinationIdentity(provider.baseUrl),
486
+ adapterName,
487
+ modelId: parsed.modelId,
488
+ credentialIdentity,
489
+ ...(credentialDurableIdentity ? { credentialDurableIdentity } : {}),
490
+ }
491
+ : undefined;
492
+ const continuationDestinationIdentity = providerContinuationDestinationIdentity(parsed, provider);
493
+ const continuationOwner = providerContinuationOwnerFromReplayIdentity(
494
+ replayIdentity && continuationDestinationIdentity
495
+ ? { ...replayIdentity, providerDestinationIdentity: continuationDestinationIdentity }
496
+ : undefined,
497
+ );
498
+ if (adapterName === "cursor") {
499
+ // The final route owner is authoritative for Cursor and supersedes the account-derived
500
+ // seed assigned before route binding. A Cursor conversation must be scoped to the exact
501
+ // provider/destination/adapter/model/credential that serves it.
502
+ if (continuationOwner) parsed._cursorIdentityScope = providerContinuationRouteScope(continuationOwner);
503
+ else if (!parsed._cursorIdentityScope?.startsWith("cursor-unowned:")) {
504
+ // Prevent the adapter's token-only fallback from recreating a provider-private id after the
505
+ // route owner failed closed. The sentinel is per parsed request and contains no credential.
506
+ parsed._cursorIdentityScope = `cursor-unowned:${randomUUID()}`;
507
+ }
508
+ }
509
+ bindReasoningReplayScope(
510
+ parsed._reasoningReplayScope,
511
+ replayIdentity,
512
+ );
513
+ // Keep this sticky for the whole outbound request: a later auth/key rebind may compare equal
514
+ // after the first mismatch, but it cannot make history minted by the prior route decodable.
515
+ if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
516
+ parsed._stripReasoningEncryptedContent = true;
517
+ }
518
+ bindProviderContinuationForRoute(parsed, continuationOwner);
519
+ }
520
+
521
+ function adapterResponseReachedServingTerminal(
522
+ events: readonly AdapterEvent[],
523
+ response: Readonly<Record<string, unknown>>,
524
+ ): boolean {
525
+ return (response.status === "completed" || response.status === "incomplete")
526
+ && events.some(event => event.type === "done" || event.type === "incomplete");
527
+ }
528
+
529
+ const OPAQUE_RESPONSES_INPUT_TYPES = new Set([
530
+ "reasoning",
531
+ "compaction",
532
+ "compaction_summary",
533
+ "context_compaction",
534
+ ]);
535
+
536
+ function outboundResponsesBodyCarriesOpaqueBlob(bodyText: string | undefined): boolean {
537
+ if (!bodyText) return false;
538
+ try {
539
+ const body = JSON.parse(bodyText) as unknown;
540
+ if (!body || typeof body !== "object" || Array.isArray(body)) return false;
541
+ const input = (body as { input?: unknown }).input;
542
+ if (!Array.isArray(input)) return false;
543
+ return input.some(item => {
544
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
545
+ const candidate = item as { type?: unknown; encrypted_content?: unknown };
546
+ return typeof candidate.type === "string"
547
+ && OPAQUE_RESPONSES_INPUT_TYPES.has(candidate.type)
548
+ && typeof candidate.encrypted_content === "string"
549
+ && candidate.encrypted_content.length > 0;
550
+ });
551
+ } catch {
552
+ return false;
553
+ }
554
+ }
555
+
556
+ function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
557
+ try {
558
+ const payload = JSON.parse(bodyText) as unknown;
559
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
560
+ const record = payload as { code?: unknown; error?: unknown };
561
+
562
+ if (record.error && typeof record.error === "object" && !Array.isArray(record.error)) {
563
+ const error = record.error as { type?: unknown; code?: unknown; message?: unknown };
564
+ if (error.type === "invalid_request_error") {
565
+ if (error.code === "invalid_encrypted_content") return true;
566
+ if (
567
+ (error.code === null || error.code === undefined)
568
+ && typeof error.message === "string"
569
+ && error.message.startsWith("The encrypted content ")
570
+ && error.message.endsWith(
571
+ " could not be verified. Reason: Encrypted content could not be decrypted or parsed.",
572
+ )
573
+ ) return true;
574
+ }
575
+ }
576
+
577
+ if (record.code !== "invalid-argument" || typeof record.error !== "string") return false;
578
+ return record.error.startsWith("Could not decode the compaction blob")
579
+ || record.error.startsWith("Could not decrypt the provided encrypted_content");
580
+ } catch {
581
+ return false;
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Whether an upstream Responses 4xx authoritatively rejected opaque replay state.
587
+ *
588
+ * The outbound-body check is intentional: the inbound transcript may contain a proxy envelope or
589
+ * compaction blob that the adapter already lowered, in which case a replay would be byte-identical.
590
+ * OpenAI usually exposes a dedicated nested code; ChatGPT also emits one exact code-less
591
+ * unverifiable-ciphertext message. xAI's code is generic, so its two concrete decoder error
592
+ * identities are also required. Unrelated error prose must never gain a hidden resend.
593
+ */
594
+ export function shouldAttemptOpaqueBlobRecovery(args: {
595
+ status: number;
596
+ adapterName: string;
597
+ outboundBody?: string;
598
+ errorBody: string;
599
+ alreadyAttempted: boolean;
600
+ }): boolean {
601
+ return args.status >= 400
602
+ && args.status < 500
603
+ && args.adapterName === "openai-responses"
604
+ && !args.alreadyAttempted
605
+ && outboundResponsesBodyCarriesOpaqueBlob(args.outboundBody)
606
+ && isSelfIdentifiedOpaqueBlobRejection(args.errorBody);
607
+ }
608
+
609
+ async function opaqueBlobRejectionBodyForRecovery(
610
+ response: Response,
611
+ outboundBody: string | undefined,
612
+ adapterName: string,
613
+ alreadyAttempted: boolean,
614
+ signal: AbortSignal,
615
+ ): Promise<string | undefined> {
616
+ if (
617
+ response.status < 400
618
+ || response.status >= 500
619
+ || adapterName !== "openai-responses"
620
+ || alreadyAttempted
621
+ || !outboundResponsesBodyCarriesOpaqueBlob(outboundBody)
622
+ ) return undefined;
623
+ try {
624
+ const body = await readBoundedResponseBody(response.clone(), { signal });
625
+ return body.displaySafe && !body.truncated ? body.text : undefined;
626
+ } catch {
627
+ return undefined;
628
+ }
629
+ }
630
+
631
+ function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
632
+ parsed._stripReasoningEncryptedContent = true;
633
+ }
634
+
635
+ type OpaqueBlobRecoveryGuard = { attempted: boolean };
636
+
637
+ type OpaqueBlobRecoveryResult =
638
+ | { kind: "skipped" }
639
+ | { kind: "recovered"; response: Response }
640
+ | { kind: "failed"; response: Response };
641
+
642
+ async function attemptOpaqueBlobRecovery(
643
+ args: {
644
+ response: Response;
645
+ outboundBody?: string;
646
+ adapterName: string;
647
+ parsed: OcxParsedRequest;
648
+ guard: OpaqueBlobRecoveryGuard;
649
+ signal: AbortSignal;
650
+ },
651
+ rebuild: (kind: AttemptRecoveryKind) => Promise<Response | { failed: Response }>,
652
+ ): Promise<OpaqueBlobRecoveryResult> {
653
+ const errorBody = await opaqueBlobRejectionBodyForRecovery(
654
+ args.response,
655
+ args.outboundBody,
656
+ args.adapterName,
657
+ args.guard.attempted,
658
+ args.signal,
659
+ );
660
+ if (errorBody === undefined || !shouldAttemptOpaqueBlobRecovery({
661
+ status: args.response.status,
662
+ adapterName: args.adapterName,
663
+ outboundBody: args.outboundBody,
664
+ errorBody,
665
+ alreadyAttempted: args.guard.attempted,
666
+ })) {
667
+ return { kind: "skipped" };
668
+ }
669
+
670
+ args.guard.attempted = true;
671
+ prepareOpaqueBlobRecovery(args.parsed);
672
+ try { void args.response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
673
+ const result = await rebuild("opaque-blob-rejection");
674
+ return "failed" in result
675
+ ? { kind: "failed", response: result.failed }
676
+ : { kind: "recovered", response: result };
677
+ }
678
+
679
+ function nonEmptyProviderApiKey(provider: OcxProviderConfig): string | undefined {
680
+ return typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0
681
+ ? provider.apiKey
682
+ : undefined;
683
+ }
199
684
 
685
+ function isFixedCodexAccount(authCtx: CodexAuthContext): boolean {
686
+ return (authCtx.kind === "pool" || authCtx.kind === "main-pool")
687
+ && authCtx.fixedAccount === true;
688
+ }
200
689
 
201
690
  export function usesCodexForwardPoolAuth(
202
691
  authCtx: CodexAuthContext,
@@ -206,6 +695,30 @@ export function usesCodexForwardPoolAuth(
206
695
  && provider.authMode === "forward" && provider.adapter === "openai-responses";
207
696
  }
208
697
 
698
+ export function preAuthUpstreamHostCircuitKey(
699
+ route: Pick<RouteResult, "provider" | "providerName" | "codexAccountMode" | "codexAccountId">,
700
+ config: OcxConfig,
701
+ options: { requireResponsesAdapter?: boolean } = {},
702
+ ): string | null {
703
+ if (
704
+ normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) === 0
705
+ || route.codexAccountMode !== "pool"
706
+ || route.codexAccountId !== undefined
707
+ || route.provider.authMode !== "forward"
708
+ || (options.requireResponsesAdapter !== false && route.provider.adapter !== "openai-responses")
709
+ ) return null;
710
+ return upstreamHostHealthKey(route.providerName, safeOriginLabel(route.provider.baseUrl ?? ""));
711
+ }
712
+
713
+ export function upstreamHostCircuitOpenResponse(retryAfterSeconds: number): Response {
714
+ return formatErrorResponse(
715
+ 503,
716
+ "upstream_host_circuit_open",
717
+ "Provider host is temporarily unavailable",
718
+ { retryAfter: String(retryAfterSeconds) },
719
+ );
720
+ }
721
+
209
722
  function normalizeCodexUnsupportedModelDetail(value: string): string {
210
723
  return value.trim().replace(/\s+/gu, " ").toLocaleLowerCase("en-US");
211
724
  }
@@ -246,8 +759,8 @@ async function shouldRetryCodexPoolAccountModel400(
246
759
  }
247
760
 
248
761
  /** 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;
762
+ export function shouldRetryCodexPoolAccountQuota(response: Response): boolean {
763
+ return response.status === 402 || response.status === 429;
251
764
  }
252
765
 
253
766
  interface CodexPoolAccountRetryArgs {
@@ -264,7 +777,9 @@ interface CodexPoolAccountRetryArgs {
264
777
  // needs the inbound scope or the retry could land on a different wire than the
265
778
  // first attempt.
266
779
  inboundWire?: InboundWire;
780
+ codexWsRuntimeIdentity?: BunRuntimeGateInput;
267
781
  translatorBudget: TranslatorBudget;
782
+ turnAdmissionLease?: AdmissionLease;
268
783
  };
269
784
  firstAuthCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
270
785
  firstResponse: Response;
@@ -273,6 +788,11 @@ interface CodexPoolAccountRetryArgs {
273
788
  connectMs: number;
274
789
  passthroughEstimate?: number;
275
790
  stream: boolean;
791
+ onResponse?: (
792
+ response: Response,
793
+ authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>,
794
+ request: Awaited<ReturnType<ReturnType<typeof resolveAdapter>["buildRequest"]>>,
795
+ ) => void;
276
796
  }
277
797
 
278
798
  type CodexPoolAccountRetryResult =
@@ -290,6 +810,53 @@ type CodexPoolAccountRetryResult =
290
810
  authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
291
811
  };
292
812
 
813
+ const CODEX_ACCOUNT_GATED_CANONICAL_WIRE_MODELS: ReadonlyMap<string, string> = new Map([
814
+ // The authenticated catalog currently advertises Daybreak Blue, while successful responses
815
+ // identify the serving model as gpt-5.6-sol. Sending the selector itself is shard-dependent:
816
+ // live traffic can receive the exact unsupported-model 400 repeatedly from the same entitled
817
+ // account. Keep Daybreak as the admission/catalog identity, but use the stable serving id on
818
+ // the credential-bearing wire after entitlement selection has completed.
819
+ ["gpt-daybreak-blue-latest", "gpt-5.6-sol"],
820
+ ]);
821
+
822
+ export function codexAccountGatedCanonicalWireModel(modelId: string): string | undefined {
823
+ const exact = CODEX_ACCOUNT_GATED_CANONICAL_WIRE_MODELS.get(modelId);
824
+ if (exact) return exact;
825
+ for (const [selector, wireModel] of CODEX_ACCOUNT_GATED_CANONICAL_WIRE_MODELS) {
826
+ if (slugsEquivalent(modelId, selector)) return wireModel;
827
+ }
828
+ return undefined;
829
+ }
830
+
831
+ function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult): void {
832
+ if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
833
+ const wireModel = codexAccountGatedCanonicalWireModel(route.modelId);
834
+ if (!wireModel) return;
835
+
836
+ parsed.modelId = wireModel;
837
+ if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
838
+ const raw = parsed._rawBody as Record<string, unknown>;
839
+ raw.model = wireModel;
840
+ // Daybreak's authenticated catalog does not advertise retention support, and the upstream
841
+ // rejects this optional Codex hint before model execution. Removing it preserves request
842
+ // semantics while avoiding an otherwise terminal pre-stream 400.
843
+ delete raw.prompt_cache_retention;
844
+ }
845
+
846
+ /**
847
+ * Workspace-denial evidence for a 403, read from the upstream body.
848
+ *
849
+ * #1789: a valid K12 credential gets 403 `codex_workspace_access_denied` on a routed prompt.
850
+ * Without this the account is quarantined for reauthentication, which cannot fix a workspace
851
+ * grant and loops forever. Fails closed: an unreadable body keeps the historical handling.
852
+ */
853
+ async function codexDenialOutcomeMeta(response: Response): Promise<{ denial?: "workspace" | "entitlement" }> {
854
+ if (response.status !== 403) return {};
855
+ const { classifyCodexPreStreamRejection } = await import("../../codex/quota-rejection");
856
+ const rejection = await classifyCodexPreStreamRejection(response);
857
+ return rejection.denial ? { denial: rejection.denial } : {};
858
+ }
859
+
293
860
  function codexQuotaOutcomeMeta(response: Response): {
294
861
  retryAfter: string | null;
295
862
  resetAt: string[];
@@ -328,25 +895,43 @@ async function retryCodexPoolOnAlternateAccount(
328
895
  } = args;
329
896
  const inboundWire = options.inboundWire ?? "responses";
330
897
  let retryAuthCtx: CodexAuthContext | undefined;
898
+ if (outcomeStatus === 400 && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)) {
899
+ invalidateCodexModelEntitlementsForAccount(firstAuthCtx.accountId);
900
+ const refreshed = await resolveCodexModelEntitlements(config);
901
+ if (entitledCodexAccountIdsForModel(refreshed, route.modelId)?.has(firstAuthCtx.accountId)) {
902
+ // The authenticated roster still grants this exact model. Retry on the same account:
903
+ // upstream shards can briefly disagree during a gated-model rollout, but a pre-stream 400
904
+ // proves no output was committed and keeps this replay bounded.
905
+ retryAuthCtx = firstAuthCtx;
906
+ }
907
+ }
908
+ // Exact account selectors may retry the same confirmed account above, but must never resolve
909
+ // an alternate. Quota failures and a refreshed entitlement miss remain terminal.
910
+ if (!retryAuthCtx && firstAuthCtx.fixedAccount) return { kind: "no-alternate" };
331
911
  try {
332
- retryAuthCtx = await resolveCodexAuthContext(
333
- req.headers,
334
- config,
335
- "pool",
336
- { excludeAccountId: firstAuthCtx.accountId, modelId: route.modelId },
337
- );
912
+ retryAuthCtx ??= await resolveCodexAuthContext(
913
+ req.headers,
914
+ config,
915
+ "pool",
916
+ {
917
+ excludeAccountId: firstAuthCtx.accountId,
918
+ modelId: route.modelId,
919
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
920
+ },
921
+ );
338
922
  } catch (error) {
339
923
  if (
340
924
  !(error instanceof CodexPoolAuthenticationError)
341
925
  && !(error instanceof CodexAuthContextError)
342
926
  && !(error instanceof CodexAccountCooldownError)
927
+ && !(error instanceof CodexMainProfileDrainingError)
343
928
  ) throw error;
344
929
  }
345
930
  if (retryAuthCtx?.kind !== "pool" && retryAuthCtx?.kind !== "main-pool") {
346
931
  return { kind: "no-alternate" };
347
932
  }
348
933
 
349
- const quotaMeta = codexQuotaOutcomeMeta(firstResponse);
934
+ const quotaMeta = { ...codexQuotaOutcomeMeta(firstResponse), ...(await codexDenialOutcomeMeta(firstResponse)) };
350
935
  if (outcomeStatus === 429 || outcomeStatus === 402) {
351
936
  const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
352
937
  applyAccountQuotaFromUpstreamHeaders(
@@ -355,10 +940,14 @@ async function retryCodexPoolOnAlternateAccount(
355
940
  firstAuthCtx.writerGeneration,
356
941
  );
357
942
  }
358
- if (!shouldDeferCodexResetDerivedCooldown(firstResponse, options.deferCodexResetDerivedCooldown)) {
943
+ const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
944
+ firstResponse,
945
+ options.deferCodexResetDerivedCooldown,
946
+ );
947
+ const recordFirstOutcome = (): void => {
359
948
  recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
360
949
  ...quotaMeta,
361
- threadId: req.headers.get("x-codex-parent-thread-id"),
950
+ threadId: firstAuthCtx.affinityKey,
362
951
  modelId: route.modelId,
363
952
  probeLeaseId: codexProbeLeaseId(firstAuthCtx),
364
953
  probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
@@ -366,8 +955,10 @@ async function retryCodexPoolOnAlternateAccount(
366
955
  // Retry already advanced the RR ring via excludeAccountId — reuse for promotion.
367
956
  ...(retryAuthCtx.accountId ? { promoteAccountId: retryAuthCtx.accountId } : {}),
368
957
  });
369
- }
370
-
958
+ };
959
+ // Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
960
+ // ordinary requests must block the first account before the alternate send.
961
+ if (!deferFirstOutcome) recordFirstOutcome();
371
962
  const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx);
372
963
  const retryProvider = applyCodexAuthContextToProvider(
373
964
  stripCodexRuntimeProviderFields(route.provider),
@@ -378,11 +969,20 @@ async function retryCodexPoolOnAlternateAccount(
378
969
  resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
379
970
  config.cacheRetention,
380
971
  );
972
+ bindRouteReasoningReplayScope({
973
+ parsed,
974
+ providerName: route.providerName,
975
+ provider: retryProvider,
976
+ adapterName: retryAdapter.name,
977
+ codexAuthContext: retryAuthCtx,
978
+ forwardHeaders: retryHeaders,
979
+ });
381
980
  const request = await retryAdapter.buildRequest(parsed, {
382
981
  headers: retryHeaders,
383
982
  translatorBudget: options.translatorBudget,
384
983
  });
385
984
  recordAdapterReasoning(logCtx, request);
985
+ recordAdapterTier(logCtx, request);
386
986
 
387
987
  await firstResponse.body?.cancel().catch(() => undefined);
388
988
  options.onCodexAuthContextResolved?.(retryAuthCtx);
@@ -392,34 +992,86 @@ async function retryCodexPoolOnAlternateAccount(
392
992
  retryAuthCtx.accountId,
393
993
  config,
394
994
  );
995
+ logCtx.accountLogLabel = codexAuthContextLogLabel(retryAuthCtx, config);
996
+ sealRequestAttemptIdentity(
997
+ logCtx.activeAttempt,
998
+ logCtx.provider,
999
+ retryAdapter.name,
1000
+ logCtx.accountLogLabel,
1001
+ );
395
1002
 
396
- noteAttemptSend(logCtx.activeAttempt, passthroughEstimate);
1003
+ const retrySameConfirmedAccount = outcomeStatus === 400
1004
+ && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)
1005
+ && retryAuthCtx.accountId === firstAuthCtx.accountId;
1006
+ // Live Daybreak traffic has produced long runs of unsupported-model 400s from different
1007
+ // upstream shards even while the authenticated roster continues to grant the model. Permit
1008
+ // seven additional same-account sends (eight total including the original), re-checking the
1009
+ // exact allow-listed body and fresh entitlement before every later send. Alternate-account and
1010
+ // quota recovery retain their historical one-send bound.
1011
+ const maxRetrySends = retrySameConfirmedAccount ? 7 : 1;
1012
+ let retrySendCount = 0;
1013
+ let upstreamResponse: Response;
397
1014
  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
- };
1015
+ while (true) {
1016
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate);
1017
+ upstreamResponse = await fetchWithHeaderTimeout(
1018
+ request.url,
1019
+ {
1020
+ method: request.method,
1021
+ headers: request.headers,
1022
+ body: request.body,
1023
+ },
1024
+ upstream.signal,
1025
+ connectMs,
1026
+ stream,
1027
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1028
+ providerName: route.providerName,
1029
+ modelId: route.modelId,
1030
+ }),
1031
+ // Credential-bearing forward send: never follow a redirect into a
1032
+ // dead-host rejection after the credential was seen (#914).
1033
+ route.provider.authMode === "forward",
1034
+ );
1035
+ retrySendCount += 1;
1036
+ args.onResponse?.(upstreamResponse, retryAuthCtx, request);
1037
+ if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
1038
+ if (!await shouldRetryCodexPoolAccountModel400(
1039
+ upstreamResponse,
1040
+ route.modelId,
1041
+ options.abortSignal,
1042
+ )) break;
1043
+ invalidateCodexModelEntitlementsForAccount(retryAuthCtx.accountId);
1044
+ const refreshed = await resolveCodexModelEntitlements(config);
1045
+ if (!entitledCodexAccountIdsForModel(refreshed, route.modelId)?.has(retryAuthCtx.accountId)) break;
1046
+ await upstreamResponse.body?.cancel().catch(() => undefined);
1047
+ }
417
1048
  } catch (error) {
418
1049
  // Attribute the transport failure to the alternate account (already selected).
419
1050
  return { kind: "transport", error, authCtx: retryAuthCtx };
420
1051
  } finally {
421
1052
  request.releaseBodyObservation?.();
422
1053
  }
1054
+ // A real HTTP response proves the host was reached (#914).
1055
+ const retryHostKey = upstreamHostHealthKey(route.providerName, safeOriginLabel(request.url));
1056
+ if (normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0) {
1057
+ resetUpstreamHostHealth(retryHostKey, null);
1058
+ } else {
1059
+ resetUpstreamHostHealth(retryHostKey);
1060
+ }
1061
+ if (deferFirstOutcome && upstreamResponse.ok) {
1062
+ // Deferral keeps the first account eligible for a later combo model while an
1063
+ // alternate attempt is still fallible. Commit its quota outcome only once the
1064
+ // alternate account returns a successful HTTP response; otherwise the combo may
1065
+ // still need the first account for its next target.
1066
+ recordFirstOutcome();
1067
+ }
1068
+ return {
1069
+ kind: "retried",
1070
+ authCtx: retryAuthCtx,
1071
+ request,
1072
+ upstreamResponse,
1073
+ selectedForwardHeaders: retryHeaders,
1074
+ };
423
1075
  }
424
1076
 
425
1077
 
@@ -430,7 +1082,6 @@ export function codexForwardTerminalOutcomeRecorder(
430
1082
  provider: OcxProviderConfig,
431
1083
  modelId?: string,
432
1084
  logCtx?: RequestLogContext,
433
- threadId?: string | null,
434
1085
  ): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
435
1086
  if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
436
1087
  return (status, httpStatusOverride) => {
@@ -439,7 +1090,8 @@ export function codexForwardTerminalOutcomeRecorder(
439
1090
  // request. Don't penalize account health; record success to clear any
440
1091
  // prior soft-avoid so a healthy account isn't stuck avoided.
441
1092
  recordCodexUpstreamOutcome(config, authCtx.accountId, 200, {
442
- threadId,
1093
+ threadId: authCtx.affinityKey,
1094
+ fixedAccount: authCtx.fixedAccount,
443
1095
  modelId,
444
1096
  probeLeaseId: codexProbeLeaseId(authCtx),
445
1097
  probeQuotaScope: codexProbeQuotaScope(authCtx),
@@ -460,7 +1112,8 @@ export function codexForwardTerminalOutcomeRecorder(
460
1112
  ? 200
461
1113
  : (httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
462
1114
  recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
463
- threadId,
1115
+ threadId: authCtx.affinityKey,
1116
+ fixedAccount: authCtx.fixedAccount,
464
1117
  modelId,
465
1118
  probeLeaseId: codexProbeLeaseId(authCtx),
466
1119
  probeQuotaScope: codexProbeQuotaScope(authCtx),
@@ -515,6 +1168,17 @@ export interface ConsumedComboFailure {
515
1168
 
516
1169
  export interface HandleResponsesOptions {
517
1170
  turnAdmissionLease?: AdmissionLease;
1171
+ /**
1172
+ * How the caller proved data-plane admission (#1686).
1173
+ *
1174
+ * A bearer-presented admission secret is one of OUR OWN secrets, so a Direct turn must
1175
+ * SUBSTITUTE the stored main credential rather than forward it. Without this fact at the
1176
+ * decision point, Direct cannot tell an admission bearer from the user own ChatGPT bearer,
1177
+ * which is why it refused the whole env_key flow instead of serving it.
1178
+ */
1179
+ admission?: DataPlaneAdmission;
1180
+ /** Called at most once after the complete client body is read and accepted for dispatch. */
1181
+ onRequestBodyRead?: () => void;
518
1182
  forceEmptyResponseId?: boolean;
519
1183
  abortSignal?: AbortSignal;
520
1184
  /** One-shot TTFT callback: first non-empty model output observed (WP4). */
@@ -524,6 +1188,10 @@ export interface HandleResponsesOptions {
524
1188
  setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
525
1189
  onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
526
1190
  onNativePassthroughCancel?: () => void;
1191
+ /** Internal deterministic clock/timer seam for provider terminal repair. */
1192
+ responsesTerminalRepairScheduler?: ResponsesTerminalRepairScheduler;
1193
+ /** Internal deterministic runtime-identity seam for Codex upstream WS selection tests. */
1194
+ codexWsRuntimeIdentity?: BunRuntimeGateInput;
527
1195
  /**
528
1196
  * When true, body `prompt_cache_key` is a Claude Desktop shared cache cohort
529
1197
  * (system/tools hash), not a per-session id — do not use it for Anthropic pool affinity.
@@ -537,18 +1205,45 @@ export interface HandleResponsesOptions {
537
1205
  * it. Omitted means a genuine Responses inbound.
538
1206
  */
539
1207
  inboundWire?: InboundWire;
1208
+ /** Internal transport identity for route-scoped upstream compatibility policy. */
1209
+ inboundTransport?: "websocket";
1210
+ /**
1211
+ * Claude replay may add native-main auth so OpenAI sidecars remain available.
1212
+ * Strip only that internal credential when the final route is a noncanonical
1213
+ * forward destination; final routing can differ from Claude's preflight route.
1214
+ */
1215
+ stripClaudeMainAuthForNoncanonicalForward?: boolean;
540
1216
  /** Internal recursion guard; callers outside this module must not set it. */
541
1217
  comboAttempt?: boolean;
1218
+ /** Internal combo handoff for one parent-validated continuation snapshot. */
1219
+ comboReplaySnapshot?: {
1220
+ sourceBody: unknown;
1221
+ previousResponseInputExpanded: boolean;
1222
+ providerContinuation: OcxProviderContinuationState | undefined;
1223
+ };
542
1224
  /** Internal combo handoff: allow a later same-provider model after a reset-derived 429/402. */
543
1225
  deferCodexResetDerivedCooldown?: boolean;
544
1226
  /** 030-owned handoff when a child consumed the original failure under bounds. */
545
1227
  onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
546
1228
  /** Caller-owned for Chat/Claude replay; omitted only at genuine Responses ingress. */
547
1229
  translatorBudget?: TranslatorBudget;
1230
+ /**
1231
+ * Terminal vision-describe marker (roadmap 180): true when the inbound
1232
+ * request IS the vision sidecar's own loopback describe call. The plan site
1233
+ * then STRIPS images instead of planning another describe — a depth cap of 1
1234
+ * that holds under predicate drift and combo re-resolution. The Chat surface
1235
+ * detects the raw `x-opencodex-vision-describe` header before its bridge
1236
+ * rebuilds headers and carries the fact through this flag.
1237
+ */
1238
+ visionDescribeTerminal?: boolean;
548
1239
  }
549
1240
 
550
1241
 
551
1242
 
1243
+ /**
1244
+ * Build the 499 JSON error the proxy returns when the client disconnects before the
1245
+ * response completes (`client_cancelled`).
1246
+ */
552
1247
  export function clientCancelledResponse(): Response {
553
1248
  return formatErrorResponse(499, "client_cancelled", "Client cancelled request");
554
1249
  }
@@ -685,6 +1380,40 @@ export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
685
1380
  const UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE =
686
1381
  "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
1382
 
1383
+ // Whole-body policy for non-streaming upstream JSON responses (see the application/json
1384
+ // branch of the passthrough return path). 32 MiB matches the continuation snapshot read
1385
+ // bound and is far above any legitimate non-streaming completion, including base64 image
1386
+ // payloads. The stall deadlines only govern the body transfer — generation time before
1387
+ // the response headers is untouched. Generation after early/chunked headers but before
1388
+ // the first body byte previously used the 30-second inactivity deadline; this call site
1389
+ // gives it the full body deadline instead.
1390
+ const MAX_UPSTREAM_JSON_BODY_BYTES = 32 * 1024 * 1024;
1391
+ const UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS = 180_000;
1392
+ const UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS = 30_000;
1393
+ const MAX_FAST_WIRE_CAPABILITY_WARNINGS = 256;
1394
+ const warnedFastWireCapabilityGaps = new Set<string>();
1395
+
1396
+ function warnFastWireCapabilityGap(providerName: string, modelId: string): void {
1397
+ const safeProvider = sanitizeLogMetadataString(providerName) ?? "unknown";
1398
+ const safeModel = sanitizeLogMetadataString(modelId) ?? "unknown";
1399
+ const key = `${safeProvider}\0${safeModel}`;
1400
+ if (warnedFastWireCapabilityGaps.has(key)) return;
1401
+ if (warnedFastWireCapabilityGaps.size >= MAX_FAST_WIRE_CAPABILITY_WARNINGS) {
1402
+ const oldest = warnedFastWireCapabilityGaps.values().next().value;
1403
+ if (oldest !== undefined) warnedFastWireCapabilityGaps.delete(oldest);
1404
+ }
1405
+ warnedFastWireCapabilityGaps.add(key);
1406
+ console.warn(
1407
+ `[opencodex] Fast policy for ${safeProvider}/${safeModel} has service-tier capability but no Fast wire; preserving only caller-permitted tier behavior`,
1408
+ );
1409
+ }
1410
+ export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
1411
+ maxBytes: MAX_UPSTREAM_JSON_BODY_BYTES,
1412
+ totalTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
1413
+ inactivityTimeoutMs: UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS,
1414
+ firstByteTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
1415
+ };
1416
+
688
1417
  function unreadableEncryptedAgentTaskResponse(): Response {
689
1418
  return new Response(
690
1419
  JSON.stringify({
@@ -699,7 +1428,7 @@ function unreadableEncryptedAgentTaskResponse(): Response {
699
1428
  }
700
1429
 
701
1430
  type ResponsesAuthResolution =
702
- | { ok: true; authCtx: CodexAuthContext; headers: Headers }
1431
+ | { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean }
703
1432
  | { ok: false; response: Response };
704
1433
 
705
1434
  /**
@@ -713,10 +1442,40 @@ async function resolveResponsesCodexAuth(
713
1442
  options: HandleResponsesOptions,
714
1443
  ): Promise<ResponsesAuthResolution> {
715
1444
  try {
716
- if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
1445
+ // #1686: a caller that proved admission with a BEARER presented one of our own secrets.
1446
+ // Refusing it here is what made the codex-cli `env_key` contract unusable against Direct.
1447
+ // Admitting it is only safe because the stored main credential is substituted below, so
1448
+ // the admission secret still never leaves this process.
1449
+ //
1450
+ // #2132: substitution answers "does THIS ROUTE need our stored ChatGPT credential", not
1451
+ // "how did the caller authenticate". Only a native Codex route reaches the ChatGPT backend
1452
+ // and can consume that credential; a key-authenticated routed provider carries its own and
1453
+ // never touches it. Keying on the caller alone made an install that deliberately never
1454
+ // logged into ChatGPT fail every routed request with "No usable Codex main credential".
1455
+ //
1456
+ // But ask that question the way the ADAPTER asks it. `codexAccountMode` is derived from the
1457
+ // provider NAME (`providerCodexAccountMode`), while the passthrough adapter decides whether
1458
+ // to forward caller credentials from the TRANSPORT — adapter, auth mode, and base URL
1459
+ // (`isCanonicalOpenAiForwardProvider`). A row the operator named anything other than
1460
+ // `openai`, pointed at the canonical ChatGPT backend with `authMode: "forward"`, satisfies
1461
+ // the adapter's test and fails this one, so substitution was skipped and the adapter then
1462
+ // forwarded our own admission secret upstream. Two predicates answering one question is the
1463
+ // bug; the transport is the authority, because the transport is what actually carries the
1464
+ // header. A key-authenticated routed provider is still not canonical-forward, so #2132's
1465
+ // no-ChatGPT-login install keeps working.
1466
+ const substituteMainCredential = options.admission?.source === "bearer"
1467
+ && (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
1468
+ if (route.codexAccountMode === "direct" && !substituteMainCredential) {
1469
+ validateForwardAdmissionCredential(req.headers, config);
1470
+ }
717
1471
  let authCtx: CodexAuthContext;
718
1472
  if (route.codexAccountMode) {
719
- authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, { modelId: route.modelId });
1473
+ authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
1474
+ accountId: route.codexAccountId,
1475
+ modelId: route.modelId,
1476
+ substituteMainCredentialForDirect: substituteMainCredential,
1477
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
1478
+ });
720
1479
  options.onCodexAuthContextResolved?.(authCtx);
721
1480
  } else {
722
1481
  authCtx = { kind: "main", accountId: null };
@@ -732,11 +1491,15 @@ async function resolveResponsesCodexAuth(
732
1491
  return {
733
1492
  ok: true,
734
1493
  authCtx,
735
- headers: headersForCodexAuthContext(req.headers, authCtx),
1494
+ headers: materializeCodexUpstreamAuth(req.headers, authCtx, { substituteMainCredential }),
1495
+ substituteMainCredential,
736
1496
  };
737
1497
  } catch (err) {
738
1498
  if (err instanceof CodexAccountCooldownError) {
739
- return { ok: false, response: cooldownErrorResponse(err) };
1499
+ return { ok: false, response: cooldownErrorResponse(err, Date.now(), route.codexAccountNamespace) };
1500
+ }
1501
+ if (err instanceof CodexMainProfileDrainingError) {
1502
+ return { ok: false, response: codexMainProfileDrainingResponse() };
740
1503
  }
741
1504
  if (err instanceof CodexThreadAffinityExpiredError) {
742
1505
  return {
@@ -745,7 +1508,9 @@ async function resolveResponsesCodexAuth(
745
1508
  };
746
1509
  }
747
1510
  if (err instanceof CodexAuthContextError) {
748
- const safeAccountLabel = formatCodexProviderForLog(route.providerName, err.accountId, config);
1511
+ const safeAccountLabel = route.codexAccountNamespace
1512
+ ? `${route.providerName}-${route.codexAccountNamespace}`
1513
+ : formatCodexProviderForLog(route.providerName, err.accountId, config);
749
1514
  console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
750
1515
  return {
751
1516
  ok: false,
@@ -761,6 +1526,13 @@ async function resolveResponsesCodexAuth(
761
1526
  if (err instanceof ForwardAdmissionCredentialError) {
762
1527
  return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
763
1528
  }
1529
+ if (err instanceof CodexMainSubstitutionUnavailableError) {
1530
+ // Fail BEFORE any upstream I/O. The alternative is forwarding the admission secret.
1531
+ return {
1532
+ ok: false,
1533
+ response: formatErrorResponse(401, "authentication_error", "No usable Codex main credential to serve this request"),
1534
+ };
1535
+ }
764
1536
  throw err;
765
1537
  }
766
1538
  }
@@ -776,8 +1548,15 @@ async function applyFinalRouteRequestNormalization(args: {
776
1548
  req: Request;
777
1549
  logCtx: RequestLogContext;
778
1550
  inboundWire: InboundWire;
1551
+ inboundTransport?: "websocket";
779
1552
  }): Promise<void> {
780
- const { parsed, route, config, req, logCtx, inboundWire } = args;
1553
+ const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
1554
+
1555
+ // Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
1556
+ // their existing response.model contract even when their public and wire model ids differ.
1557
+ const responseModelId = parsed.modelId;
1558
+ const preserveAnthropicResponseModel = route.providerName === "anthropic"
1559
+ || route.provider.adapter === "anthropic";
781
1560
 
782
1561
  // Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
783
1562
  if (route.modelId !== parsed.modelId) {
@@ -786,32 +1565,88 @@ async function applyFinalRouteRequestNormalization(args: {
786
1565
  }
787
1566
  parsed.modelId = route.modelId;
788
1567
  }
1568
+ // Transport-neutral reliability policy (#875): applies to any Responses
1569
+ // upstream whose final adapter is openai-responses, not only WS turns.
1570
+ const responsesUpstreamStreaming = providerModelResponsesUpstreamStreaming(
1571
+ route.providerName,
1572
+ route.provider,
1573
+ route.modelId,
1574
+ );
1575
+
789
1576
  // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
790
1577
  // this request will actually use (#404).
791
1578
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
1579
+ if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
792
1580
  logCtx.model = route.modelId;
793
1581
  logCtx.provider = route.providerName;
794
1582
  logCtx.providerAdapter = route.provider.adapter;
1583
+ logCtx.routeDecision = route.routeDecision;
1584
+
1585
+ if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
1586
+ parsed.stream = false;
1587
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
1588
+ (parsed._rawBody as Record<string, unknown>).stream = false;
1589
+ }
1590
+ }
1591
+
1592
+ // Generic Responses clients (e.g. AI-SDK apps) omit `store`, but the canonical
1593
+ // forward Codex backend rejects a native request without an explicit store:false.
1594
+ // Default it only there — every other Responses upstream (key-auth providers and
1595
+ // custom forward gateways) intentionally keeps the omitted-store server-side
1596
+ // default for previous_response_id reuse — and never override an explicit value.
1597
+ if (
1598
+ isCanonicalOpenAiForwardProvider(route.provider)
1599
+ && parsed._rawBody && typeof parsed._rawBody === "object"
1600
+ && (parsed._rawBody as Record<string, unknown>).store === undefined
1601
+ ) {
1602
+ (parsed._rawBody as Record<string, unknown>).store = false;
1603
+ }
795
1604
 
796
1605
  // Final selected model before virtual wire-model rewriting (Pro aliases).
797
1606
  const finalSelectedModelId = route.modelId;
798
1607
 
799
1608
  // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
800
1609
  applyOpenAiVirtualModel(parsed, route, logCtx);
1610
+ if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
1611
+ logCtx.resolvedModel = route.modelId;
1612
+ logCtx.preserveResolvedModelFromRoute = true;
1613
+ }
801
1614
 
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;
1615
+ // Resolve Fast policy after the final route/wire settles. A1 records the decision on parsed
1616
+ // options; the Responses adapter owns the final outbound body write.
1617
+ const fastPolicy = fastPolicyForModel(
1618
+ route.provider,
1619
+ route.modelId,
1620
+ route.providerName,
1621
+ inboundWire,
1622
+ config.providers[route.providerName],
1623
+ );
1624
+ const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
1625
+ const callerTier = parsed.options.serviceTier;
1626
+ parsed.options.tierObservation = tierObservationContext(fastPolicy, config.fastMode, callerTier);
1627
+ parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
1628
+ parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
1629
+ if (fastPolicy.capability === true && fastPolicy.fastWire === null) {
1630
+ warnFastWireCapabilityGap(route.providerName, route.modelId);
1631
+ }
1632
+ applyServiceTierGate(
1633
+ route.provider,
1634
+ parsed._rawBody,
1635
+ parsed.options,
1636
+ route.modelId,
1637
+ route.providerName,
1638
+ inboundWire,
1639
+ fastPolicy,
1640
+ );
1641
+ if (modelServiceTierSupport === false) {
1642
+ logCtx.requestedServiceTier = undefined;
1643
+ logCtx.requestedSpeedLabel = undefined;
810
1644
  }
811
1645
 
812
1646
  {
813
1647
  const guidance = await multiAgentGuidanceText(parsed, {
814
1648
  multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
1649
+ codexAccountNamespace: route.codexAccountNamespace,
815
1650
  injectionModel: config.injectionModel,
816
1651
  injectionEffort: config.injectionEffort,
817
1652
  subagentModels: config.subagentModels,
@@ -857,10 +1692,9 @@ async function applyFinalRouteRequestNormalization(args: {
857
1692
  }
858
1693
  }
859
1694
  recordAttemptRequestedEffort(logCtx);
860
- logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
861
- route.modelId,
862
- logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
863
- );
1695
+ logCtx.modelSupportsServiceTier = SERVICE_TIER_ADAPTERS.has(route.provider.adapter)
1696
+ ? modelServiceTierSupport
1697
+ : undefined;
864
1698
  }
865
1699
 
866
1700
 
@@ -886,6 +1720,49 @@ export async function handleComboResponses(
886
1720
  if (!combo) {
887
1721
  return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
888
1722
  }
1723
+ // Expand previous_response_id before image policy and child dispatch so a
1724
+ // continuation that only references prior images still fails closed when
1725
+ // imageInput is disabled (and so targets see the full replayed input).
1726
+ const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
1727
+ const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
1728
+ const scopeMismatch = previousResponseScopeMismatch(body);
1729
+ if (scopeMismatch) {
1730
+ console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
1731
+ }
1732
+ if (previousResponseReplayFailure(body)) {
1733
+ return formatErrorResponse(
1734
+ 400,
1735
+ "previous_response_not_found",
1736
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
1737
+ );
1738
+ }
1739
+ // Missing state returns the original body without a failure marker. Reject
1740
+ // that unresolved continuation for image-disabled combos so a target cannot
1741
+ // resolve prior images out of band. A successful expansion yields a new
1742
+ // object (still carrying previous_response_id) and must not be treated as
1743
+ // unresolved — text-only stored continuations remain allowed.
1744
+ const requestedPreviousId = typeof (rawBody as { previous_response_id?: unknown } | null)?.previous_response_id === "string"
1745
+ ? (rawBody as { previous_response_id: string }).previous_response_id.trim()
1746
+ : "";
1747
+ const unresolvedPrevious = requestedPreviousId.length > 0 && body === rawBody;
1748
+ if (combo.imageInput === "disabled" && unresolvedPrevious) {
1749
+ return formatErrorResponse(
1750
+ 400,
1751
+ "previous_response_not_found",
1752
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
1753
+ );
1754
+ }
1755
+ if (combo.imageInput === "disabled" && comboRequestHasImageInput(body)) {
1756
+ return formatErrorResponse(400, "invalid_request_error", `Combo "${comboId}" does not accept image input`);
1757
+ }
1758
+ const comboReplaySnapshot = {
1759
+ sourceBody: body,
1760
+ previousResponseInputExpanded: body !== rawBody
1761
+ && typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
1762
+ providerContinuation: !scopeMismatch && body !== rawBody && requestedPreviousId
1763
+ ? previousResponseProviderState(requestedPreviousId)
1764
+ : undefined,
1765
+ };
889
1766
  const adoptFailedChildLog = (childLog: RequestLogContext): void => {
890
1767
  // Attempts remain the complete physical history; the logical row mirrors the most recent
891
1768
  // failed target so an exhausted combo still has useful top-level reasoning diagnostics.
@@ -894,6 +1771,7 @@ export async function handleComboResponses(
894
1771
  model: requestedModel,
895
1772
  provider: "combo",
896
1773
  comboId,
1774
+ routeDecision: logCtx.routeDecision,
897
1775
  attempts: logCtx.attempts,
898
1776
  activeAttempt: undefined,
899
1777
  activeAttemptStartedAt: undefined,
@@ -901,13 +1779,13 @@ export async function handleComboResponses(
901
1779
  };
902
1780
 
903
1781
  const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
904
- (rawBody as { input?: unknown } | undefined)?.input,
1782
+ (body as { input?: unknown } | undefined)?.input,
905
1783
  );
906
1784
  const canDecryptUnreadableAgentTask = (target: (typeof combo.targets)[number]): boolean => {
907
1785
  const provider = config.providers[target.provider];
908
1786
  if (!provider || provider.disabled === true) return false;
909
1787
  try {
910
- const route = routeModel(config, `${target.provider}/${target.model}`);
1788
+ const route = routeConcreteModel(config, `${target.provider}/${target.model}`);
911
1789
  return isCanonicalOpenAiForwardProvider(route.provider);
912
1790
  } catch {
913
1791
  return false;
@@ -928,6 +1806,9 @@ export async function handleComboResponses(
928
1806
  if (!pick) {
929
1807
  return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
930
1808
  }
1809
+ // One immutable combo selection trace, before any child dispatch; child
1810
+ // adoption below must never replace it with a concrete child route trace.
1811
+ logCtx.routeDecision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
931
1812
 
932
1813
  let lastFailure: Response | null = null;
933
1814
  while (pick) {
@@ -938,9 +1819,9 @@ export async function handleComboResponses(
938
1819
  ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
939
1820
  ...(logCtx.surface ? { surface: logCtx.surface } : {}),
940
1821
  };
941
- const targetRoute = routeModel(config, `${pick.target.provider}/${pick.target.model}`);
1822
+ const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
942
1823
  const childBody = concreteComboRequestBody(
943
- rawBody,
1824
+ body,
944
1825
  pick.target,
945
1826
  comboDefaultEffort(config, comboId),
946
1827
  supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
@@ -968,6 +1849,7 @@ export async function handleComboResponses(
968
1849
  attempt,
969
1850
  childLog.provider,
970
1851
  childLog.providerAdapter ?? attempt.adapter,
1852
+ childLog.accountLogLabel,
971
1853
  );
972
1854
  finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
973
1855
  (logCtx.attempts ??= []).push(attempt);
@@ -987,6 +1869,7 @@ export async function handleComboResponses(
987
1869
  response = await handleResponses(childRequest, config, childLog, {
988
1870
  ...options,
989
1871
  comboAttempt: true,
1872
+ comboReplaySnapshot,
990
1873
  deferCodexResetDerivedCooldown,
991
1874
  // Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
992
1875
  // Object.assign(logCtx, childLog) would overwrite the request-relative value).
@@ -1022,6 +1905,7 @@ export async function handleComboResponses(
1022
1905
  attempt,
1023
1906
  childLog.provider,
1024
1907
  childLog.providerAdapter ?? attempt.adapter,
1908
+ childLog.accountLogLabel,
1025
1909
  );
1026
1910
  (logCtx.attempts ??= []).push(attempt);
1027
1911
  attemptRetained = true;
@@ -1031,6 +1915,7 @@ export async function handleComboResponses(
1031
1915
  model: requestedModel,
1032
1916
  provider: "combo",
1033
1917
  comboId,
1918
+ routeDecision: logCtx.routeDecision,
1034
1919
  attempts: logCtx.attempts,
1035
1920
  activeAttempt: attempt,
1036
1921
  activeAttemptStartedAt: started,
@@ -1066,6 +1951,7 @@ export async function handleComboResponses(
1066
1951
  attempt,
1067
1952
  childLog.provider,
1068
1953
  childLog.providerAdapter ?? attempt.adapter,
1954
+ childLog.accountLogLabel,
1069
1955
  );
1070
1956
  finishRequestAttempt(
1071
1957
  attempt,
@@ -1143,6 +2029,57 @@ function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBud
1143
2029
  return finalizedResponse;
1144
2030
  }
1145
2031
 
2032
+ /**
2033
+ * Service-tier capability gate, applied after the final route/wire is settled. A
2034
+ * provider explicitly documented as NOT supporting `service_tier` must never
2035
+ * receive it: strip the field and clear the logging value even when the caller
2036
+ * supplied one (fail closed). A policy-produced canonical Fast decision has
2037
+ * already passed capability validation and cannot be vetoed by Chat's caller
2038
+ * forwarding permission. On unclassified routes every caller tier remains subject
2039
+ * to `forwardCallerTier`.
2040
+ */
2041
+ export function applyServiceTierGate(
2042
+ provider: OcxProviderConfig,
2043
+ rawBody: unknown,
2044
+ options: { serviceTier?: string; tierDecision?: TierDecision },
2045
+ modelId?: string,
2046
+ providerName?: string,
2047
+ inbound: InboundWire = "responses",
2048
+ resolvedPolicy?: ResolvedFastPolicy,
2049
+ ): void {
2050
+ // A direct unit caller without a model id retains the historical tri-state behavior for
2051
+ // adapters outside the OpenAI service-tier family. Once a model is known, resolve the final
2052
+ // model adapter as well: an explicit override to Anthropic (or another non-OpenAI wire) must
2053
+ // not carry a caller-supplied `service_tier` through a route that cannot forward it.
2054
+ if (modelId === undefined && !SERVICE_TIER_ADAPTERS.has(provider.adapter)) return;
2055
+ const policy = modelId === undefined
2056
+ ? undefined
2057
+ : resolvedPolicy ?? fastPolicyForModel(provider, modelId, providerName, inbound);
2058
+ const forwardCallerTier = modelId === undefined
2059
+ ? provider.supportsServiceTier !== false
2060
+ : policy!.forwardCallerTier;
2061
+ const rawTier = rawBody && typeof rawBody === "object"
2062
+ ? (rawBody as Record<string, unknown>).service_tier
2063
+ : undefined;
2064
+ const canonicalDecision = options.tierDecision?.kind === "set";
2065
+ const callerTierIsForeign = rawTier !== undefined
2066
+ && (typeof rawTier !== "string" || canonicalFastTierMarker(rawTier) === undefined);
2067
+ const dropForeignCallerTier = policy?.capability === true
2068
+ && policy.fastWire?.kind === "service-tier"
2069
+ && policy.fastWire?.foreignCallerTiers === "drop"
2070
+ && callerTierIsForeign;
2071
+ if (policy && policy.capability !== false && canonicalDecision) return;
2072
+ if (forwardCallerTier && !dropForeignCallerTier) return;
2073
+ if (rawBody && typeof rawBody === "object") {
2074
+ delete (rawBody as Record<string, unknown>).service_tier;
2075
+ }
2076
+ options.serviceTier = undefined;
2077
+ }
2078
+
2079
+ /**
2080
+ * Route one `/v1/responses` request through the adapter pipeline: recovery loop, passthrough
2081
+ * wire, image/web-search bridges, and the terminal-guard continuation.
2082
+ */
1146
2083
  export async function handleResponses(
1147
2084
  req: Request,
1148
2085
  config: OcxConfig,
@@ -1160,39 +2097,66 @@ export async function handleResponses(
1160
2097
  }
1161
2098
  }
1162
2099
 
2100
+ /**
2101
+ * Inner implementation of `handleResponses`; owns the pre-stream recovery loop and the
2102
+ * per-request same-target 429 retry budgets.
2103
+ */
1163
2104
  async function handleResponsesInner(
1164
2105
  req: Request,
1165
2106
  config: OcxConfig,
1166
2107
  logCtx: RequestLogContext,
1167
2108
  options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
1168
2109
  ): Promise<Response> {
2110
+ let pendingHostAdmissionLease: UpstreamHostAdmissionLease | null = null;
2111
+ let authCtx: CodexAuthContext = { kind: "main", accountId: null };
2112
+ try {
1169
2113
  // The Chat and Anthropic surfaces replay through here with a Responses-shaped body,
1170
2114
  // so an omitted value means a genuine Responses inbound.
1171
2115
  const inboundWire = options.inboundWire ?? "responses";
1172
2116
  const translatorBudget = options.translatorBudget;
2117
+ const agentTaskRecovery = agentTaskRecoveryConfig(config);
1173
2118
  let body: unknown;
1174
2119
  try {
1175
2120
  body = await readJsonRequestBody(req, translatorBudget);
1176
2121
  } catch (err) {
2122
+ if (options.abortSignal?.aborted || req.signal.aborted) {
2123
+ return clientCancelledResponse();
2124
+ }
1177
2125
  return decodeRequestErrorResponse(err, "responses");
1178
2126
  }
1179
2127
  const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
1180
2128
  if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
1181
- return handleComboResponses(req, body, comboId, config, logCtx, options);
2129
+ options.onRequestBodyRead?.();
2130
+ return handleComboResponses(req, body, comboId, config, logCtx, {
2131
+ ...options,
2132
+ // The original request body was accepted above. Combo children are synthetic
2133
+ // replays and must not repeat the caller-owned timeout transition.
2134
+ onRequestBodyRead: undefined,
2135
+ });
1182
2136
  }
1183
- const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
2137
+ let unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
1184
2138
  (body as { input?: unknown } | undefined)?.input,
1185
2139
  );
2140
+ const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
1186
2141
  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
- );
2142
+ if (options.comboReplaySnapshot) {
2143
+ copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
2144
+ } else {
2145
+ body = expandPreviousResponseInput(body, inboundClientThreadId);
2146
+ if (previousResponseScopeMismatch(body)) {
2147
+ console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
2148
+ }
2149
+ if (previousResponseReplayFailure(body)) {
2150
+ return formatErrorResponse(
2151
+ 400,
2152
+ "previous_response_not_found",
2153
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
2154
+ );
2155
+ }
1194
2156
  }
1195
- const previousResponseInputExpanded = body !== originalBody;
2157
+ const previousResponseInputExpanded = options.comboReplaySnapshot?.previousResponseInputExpanded
2158
+ ?? (body !== originalBody
2159
+ && typeof (body as { previous_response_id?: unknown }).previous_response_id === "string");
1196
2160
 
1197
2161
  // Spawn-message compatibility (both directions): agent_message task payloads ride in
1198
2162
  // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
@@ -1209,16 +2173,41 @@ async function handleResponsesInner(
1209
2173
  );
1210
2174
  }
1211
2175
 
1212
- let parsed;
2176
+ let parsed: OcxParsedRequest;
1213
2177
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
1214
2178
  try {
1215
2179
  parsed = parseRequest(body);
1216
2180
  toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
1217
2181
  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;
2182
+ const providerContinuationCandidate = options.comboReplaySnapshot
2183
+ ? options.comboReplaySnapshot.providerContinuation
2184
+ : previousResponseProviderState(parsed.previousResponseId);
2185
+ if (providerContinuationCandidate) parsed._providerContinuationCandidate = providerContinuationCandidate;
2186
+ if (inboundClientThreadId) {
2187
+ parsed._clientThreadId = inboundClientThreadId;
2188
+ parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId };
2189
+ } else if (
2190
+ options.inboundWire === "anthropic"
2191
+ && options.promptCacheKeyIsSharedCohort !== true
2192
+ && typeof parsed.options.promptCacheKey === "string"
2193
+ && parsed.options.promptCacheKey.trim().length > 0
2194
+ ) {
2195
+ // Claude Code has no Codex parent-thread header, but its metadata.user_id is
2196
+ // translated into a stable per-session prompt_cache_key. Use it as the replay
2197
+ // thread identity so Gemini thought signatures are remembered by call_id for
2198
+ // Anthropic Messages clients too (#1735/#1926). Keep `_clientThreadId` unset so
2199
+ // existing provider session-id derivation (first-user-text fallback) is unchanged.
2200
+ // Normalize through anthropicSessionKeyFromParts so overlong keys are hashed and
2201
+ // trimming matches the affinity/session-key path exactly (no raw >128-char ids).
2202
+ const normalizedCacheKey = anthropicSessionKeyFromParts({
2203
+ promptCacheKey: parsed.options.promptCacheKey,
2204
+ // The enclosing branch already proves this is not the shared cohort.
2205
+ promptCacheKeyIsSharedCohort: false,
2206
+ });
2207
+ if (normalizedCacheKey) {
2208
+ parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
2209
+ }
2210
+ }
1222
2211
  } catch (err) {
1223
2212
  if (isTranslatorBudgetExceededError(err)) {
1224
2213
  return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
@@ -1227,6 +2216,11 @@ async function handleResponsesInner(
1227
2216
  }
1228
2217
  return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
1229
2218
  }
2219
+ options.onRequestBodyRead?.();
2220
+ const responseStateOptions = (force = false): { force?: boolean; clientThreadId?: string } => ({
2221
+ ...(force ? { force: true } : {}),
2222
+ ...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}),
2223
+ });
1230
2224
  // Prefer a pre-populated id (routed Claude) over Responses headers that may be
1231
2225
  // absent or synthetically injected (session_id from prompt_cache_key).
1232
2226
  if (!logCtx.conversationId) {
@@ -1239,15 +2233,19 @@ async function handleResponsesInner(
1239
2233
  }
1240
2234
  logCtx.requestedModel = parsed.modelId;
1241
2235
  logCtx.requestedEffort = parsed.options.reasoning;
2236
+ logCtx.callerServiceTier = sanitizeLogMetadataString(parsed.options.serviceTier);
1242
2237
  logCtx.requestedServiceTier = parsed.options.serviceTier;
1243
2238
  logCtx.requestedSpeedLabel = requestLogSpeedLabel(parsed.options.serviceTier);
1244
2239
  logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
1245
2240
  logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
1246
2241
 
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+)
2242
+ // Shadow call intercept: rewrite Codex 0.145.0+ helper calls (gpt-5.6-luna).
2243
+ // Ancient clients using gpt-5.4-mini remain configurable via sourceModels.
1249
2244
  const _sci = config.shadowCallIntercept;
1250
- if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
2245
+ if (_sci?.enabled && _sci.model && shouldInterceptShadowCall(
2246
+ parsed.modelId,
2247
+ _sci.sourceModels,
2248
+ )) {
1251
2249
  const _sciOriginal = parsed.modelId;
1252
2250
  parsed.modelId = _sci.model;
1253
2251
  if (parsed._rawBody && typeof parsed._rawBody === "object") {
@@ -1258,7 +2256,14 @@ async function handleResponsesInner(
1258
2256
  if (parsed._rawBody && typeof parsed._rawBody === "object") {
1259
2257
  (parsed._rawBody as Record<string, unknown>).reasoning = { effort: "low" };
1260
2258
  }
1261
- (logCtx as unknown as Record<string, unknown>).shadowCallRewrittenFrom = _sciOriginal;
2259
+ // Record the operator-configured prefix that matched, NOT the caller's raw model string.
2260
+ // Matching is by prefix, so a caller can append arbitrary text and still intercept; that
2261
+ // raw value would then land in usage.jsonl and /api/logs behind a pattern-based redactor
2262
+ // that does not recognize every credential family. The prefix is a value the operator
2263
+ // configured, so no caller-controlled string is persisted.
2264
+ logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
2265
+ shadowSourceModelPrefix(_sciOriginal, _sci.sourceModels),
2266
+ );
1262
2267
  // Helpers must not resume/append into the parent thread's Cursor conversation.
1263
2268
  parsed._cursorIsolateConversation = true;
1264
2269
  }
@@ -1266,37 +2271,71 @@ async function handleResponsesInner(
1266
2271
 
1267
2272
  let route: RouteResult;
1268
2273
  try {
1269
- route = routeModel(config, parsed.modelId);
2274
+ route = options.comboAttempt
2275
+ ? routeConcreteModel(config, parsed.modelId)
2276
+ : routeModel(config, parsed.modelId, evidenceFromBody(parsed._rawBody));
2277
+ logCtx.routeDecision = route.routeDecision;
1270
2278
  } catch (err) {
1271
2279
  if (err instanceof NoAvailableComboTargetsError) {
1272
2280
  return comboUnavailableResponse(err.message);
1273
2281
  }
2282
+ if (err instanceof NoEligiblePolicyCandidateError) {
2283
+ // Persist the evaluation trace (per-candidate exclusions + the
2284
+ // no-eligible reason) so failed policy requests stay auditable.
2285
+ logCtx.routeDecision = err.trace;
2286
+ }
1274
2287
  return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
1275
2288
  }
1276
2289
 
1277
2290
  const hasUnexpandedPreviousResponse = !!parsed.previousResponseId
1278
2291
  && 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 };
2292
+ // Exact account selectors are isolated from Pool-wide quota work. A canonical replay miss must
2293
+ // also fail closed without polling quota upstream. Cached fallback state can still select a
2294
+ // provider with native continuation support below.
2295
+ const threadSpawn = isThreadSpawnRequest(req.headers);
2296
+ const previewSelectionAdmission = threadSpawn && route.codexAccountId === undefined
2297
+ ? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
2298
+ : undefined;
2299
+ const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
2300
+ const nativeMainReadsForbidden = nativeMainRecoveryBlocked
2301
+ || previewSelectionAdmission?.mainProfileDraining === true;
2302
+ const previewSelectionOptions = {
2303
+ nativeMainSelectionOnly: !nativeMainRecoveryBlocked
2304
+ && previewSelectionAdmission?.mainProfileDraining === true,
2305
+ };
1289
2306
  let selectedForwardHeaders = req.headers;
1290
2307
  let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
2308
+ let subagentFallbackPreviewAccountId: string | null | undefined;
1291
2309
  let subagentQuotaFailureModel = parsed.modelId;
2310
+ const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
2311
+ const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
1292
2312
 
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
2313
+ try {
2314
+ if (
2315
+ threadSpawn
2316
+ && route.codexAccountId === undefined
2317
+ && !(hasUnexpandedPreviousResponse && isCanonicalOpenAiForwardProvider(route.provider))
2318
+ ) {
2319
+ await maybePrimeSubagentQuota(config, Date.now(), { nativeMainReadsForbidden });
2320
+ }
2321
+
2322
+ // Subagent fallback must settle the final model/provider BEFORE route-dependent
2323
+ // normalization (virtual models, effort caps, service tier, wire protocol).
2324
+ // Preview the preferred Codex account without acquiring a probe lease or refreshing
1296
2325
  // 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);
2326
+ if (threadSpawn && !options.comboAttempt && route.codexAccountId === undefined) {
2327
+ // The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
2328
+ // so the preview must read the same scope slot — an undefined scope would map to the
2329
+ // "legacy" affinity bucket and never find a binding made under "shared" or a native
2330
+ // model scope, making the preview diverge from the account that actually authenticates.
2331
+ const previewAccountId = previewCodexAccountForRequest(
2332
+ poolAffinityKey,
2333
+ config,
2334
+ Date.now(),
2335
+ codexQuotaScopeForModel(route.modelId),
2336
+ previewSelectionOptions,
2337
+ );
2338
+ subagentFallbackPreviewAccountId = previewAccountId;
1300
2339
  subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
1301
2340
  const fallback = applySubagentModelFallback(
1302
2341
  parsed,
@@ -1305,6 +2344,7 @@ async function handleResponsesInner(
1305
2344
  previewAccountId,
1306
2345
  Date.now(),
1307
2346
  unreadableEncryptedAgentTask,
2347
+ previewSelectionOptions,
1308
2348
  );
1309
2349
  if (fallback) {
1310
2350
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
@@ -1317,15 +2357,120 @@ async function handleResponsesInner(
1317
2357
 
1318
2358
  if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
1319
2359
  try {
1320
- route = routeModel(config, fallback.to);
2360
+ route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
2361
+ logCtx.routeDecision = route.routeDecision;
1321
2362
  } catch (err) {
1322
2363
  if (err instanceof NoAvailableComboTargetsError) {
1323
2364
  return comboUnavailableResponse(err.message);
1324
2365
  }
2366
+ if (err instanceof NoEligiblePolicyCandidateError) {
2367
+ logCtx.routeDecision = err.trace;
2368
+ }
1325
2369
  return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
1326
2370
  }
1327
2371
  }
1328
2372
  }
2373
+ } finally {
2374
+ previewSelectionAdmission?.release();
2375
+ }
2376
+
2377
+ // Native fallback can consume ciphertext, so recover only after final route selection.
2378
+ if (
2379
+ inboundWire === "responses"
2380
+ &&
2381
+ threadSpawn
2382
+ && unreadableEncryptedAgentTask
2383
+ && agentTaskRecovery
2384
+ && !isCanonicalOpenAiForwardProvider(route.provider)
2385
+ && !options.comboAttempt
2386
+ ) {
2387
+ let recovered = false;
2388
+ try {
2389
+ recovered = await recoverEncryptedAgentTask(
2390
+ req,
2391
+ (body as { input?: unknown } | undefined)?.input,
2392
+ agentTaskRecovery,
2393
+ config,
2394
+ { parentThreadId, abortSignal: options.abortSignal },
2395
+ );
2396
+ } catch {
2397
+ recovered = false;
2398
+ }
2399
+ if (recovered) {
2400
+ unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
2401
+ (body as { input?: unknown } | undefined)?.input,
2402
+ );
2403
+ if (!unreadableEncryptedAgentTask) {
2404
+ try {
2405
+ const reparsed = parseRequest(body);
2406
+ const kept: Array<keyof OcxParsedRequest> = [
2407
+ "_previousResponseInputExpanded",
2408
+ "_providerContinuation",
2409
+ "_providerContinuationCandidate",
2410
+ "_providerContinuationOwner",
2411
+ "_cursorConversationId",
2412
+ "_clientThreadId",
2413
+ "_reasoningReplayScope",
2414
+ "_cursorIsolateConversation",
2415
+ ];
2416
+ for (const key of kept) {
2417
+ if (parsed[key] !== undefined) {
2418
+ (reparsed as unknown as Record<string, unknown>)[key] = parsed[key];
2419
+ }
2420
+ }
2421
+ parsed = reparsed;
2422
+ // The recovery mutated `body.input` in place, so `_rawBody` now carries decrypted task
2423
+ // text. Bar it from the continuation cache before any recording path can reach it —
2424
+ // that cache is persisted to disk, which would defeat the recovery cache's TTL.
2425
+ markBodyNonPersistable(parsed._rawBody);
2426
+
2427
+ // The ciphertext-only pass intentionally excludes routed candidates. Once recovery
2428
+ // makes the assignment readable, run selection again with the full configured chain
2429
+ // and keep the route in sync with any newly selected fallback.
2430
+ const fallback = applySubagentModelFallback(
2431
+ parsed,
2432
+ req.headers,
2433
+ config,
2434
+ subagentFallbackPreviewAccountId,
2435
+ Date.now(),
2436
+ false,
2437
+ previewSelectionOptions,
2438
+ );
2439
+ if (fallback) {
2440
+ (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
2441
+ (logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
2442
+ if (isInjectionDebugEnabled()) {
2443
+ injectionDebugLog(`[opencodex] subagent model fallback ${fallback.from} -> ${fallback.to}`);
2444
+ }
2445
+ }
2446
+ subagentQuotaFailureModel = fallback?.to ?? parsed.modelId;
2447
+
2448
+ if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
2449
+ try {
2450
+ route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
2451
+ logCtx.routeDecision = route.routeDecision;
2452
+ } catch (err) {
2453
+ if (err instanceof NoAvailableComboTargetsError) {
2454
+ return comboUnavailableResponse(err.message);
2455
+ }
2456
+ if (err instanceof NoEligiblePolicyCandidateError) {
2457
+ logCtx.routeDecision = err.trace;
2458
+ }
2459
+ return formatErrorResponse(
2460
+ 404,
2461
+ "invalid_request_error",
2462
+ err instanceof Error ? err.message : String(err),
2463
+ );
2464
+ }
2465
+ }
2466
+ } catch {
2467
+ unreadableEncryptedAgentTask = true;
2468
+ }
2469
+ }
2470
+ }
2471
+ }
2472
+
2473
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
1329
2474
 
1330
2475
  // Encrypted child tasks may only reach the canonical native backend. This check
1331
2476
  // runs against the FINAL route so native-only fallback can rescue a routed primary.
@@ -1347,19 +2492,80 @@ async function handleResponsesInner(
1347
2492
  );
1348
2493
  }
1349
2494
 
1350
- await applyFinalRouteRequestNormalization({ parsed, route, config, req, logCtx, inboundWire });
2495
+ // Captured before normalization: whether the CLIENT asked for SSE. The
2496
+ // transport-neutral upstream-streaming policy below may force a bounded JSON
2497
+ // upstream for reliability (#875); the answer must then be reframed to SSE
2498
+ // for streaming clients.
2499
+ const clientRequestedStream = parsed.stream;
2500
+ await applyFinalRouteRequestNormalization({
2501
+ parsed,
2502
+ route,
2503
+ config,
2504
+ req,
2505
+ logCtx,
2506
+ inboundWire,
2507
+ inboundTransport: options.inboundTransport,
2508
+ });
2509
+ // Attribute local auth/cooldown failures to the public selector too; exact auth may fail before
2510
+ // the normal post-resolution provider label is assigned.
2511
+ if (route.codexAccountNamespace) {
2512
+ logCtx.provider = `${route.providerName}-${route.codexAccountNamespace}`;
2513
+ }
1351
2514
 
2515
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
2516
+ // Refuse an input that cannot plausibly fit the model context window before spending auth,
2517
+ // circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
2518
+ //
2519
+ // Compaction turns are exempt: Codex sends compaction_trigger BECAUSE context is full, so
2520
+ // refusing the turn that shrinks the context would deadlock the client against the very
2521
+ // limit this gate reports — it would be told to compact and then denied the compaction.
2522
+ if (parsed._compactionRequest !== true) {
2523
+ const inputAdmission = checkInputAdmission(parsed, route.provider, route.providerName, parsed.modelId, nativeContextLimits(config));
2524
+ if (!inputAdmission.admitted) {
2525
+ // #1524: this is a LOCAL preflight refusal, not an upstream verdict. A policy or combo
2526
+ // fallback must be able to skip this candidate and try one whose context window fits,
2527
+ // instead of treating the first incompatible candidate as the end of the chain. The
2528
+ // distinct code is what lets the fallback layer tell the two apart -- an upstream
2529
+ // `context_length_exceeded` still stops, because retrying it elsewhere is guesswork.
2530
+ return formatErrorResponse(
2531
+ 413,
2532
+ "input_admission_refused",
2533
+ `Estimated input (~${inputAdmission.estimatedTokens} tokens) is far past the context window `
2534
+ + `of ${parsed.modelId} (${inputAdmission.ceiling} tokens). Start a new session or choose a `
2535
+ + `model with a larger context window.`,
2536
+ );
2537
+ }
2538
+ }
2539
+ const preAuthHostKey = preAuthUpstreamHostCircuitKey(route, config);
2540
+ if (preAuthHostKey) {
2541
+ const admission = acquireUpstreamHostAdmission(
2542
+ preAuthHostKey,
2543
+ config.upstreamHostCircuitThreshold,
2544
+ );
2545
+ if (admission.kind === "blocked") {
2546
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
2547
+ }
2548
+ pendingHostAdmissionLease = admission.lease;
2549
+ }
2550
+
2551
+ let substituteMainCredential = false;
1352
2552
  {
1353
2553
  const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
1354
2554
  if (!finalAuth.ok) return finalAuth.response;
1355
2555
  authCtx = finalAuth.authCtx;
1356
2556
  selectedForwardHeaders = finalAuth.headers;
2557
+ substituteMainCredential = finalAuth.substituteMainCredential;
1357
2558
  }
1358
2559
 
1359
2560
  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.
2561
+ applyCodexAccountGatedWireNormalization(parsed, route);
2562
+ logCtx.provider = route.codexAccountNamespace
2563
+ ? `${route.providerName}-${route.codexAccountNamespace}`
2564
+ : formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
2565
+ logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
2566
+ // Seed an account-derived scope before final adapter binding. Cursor never treats it as
2567
+ // authoritative: bindRouteReasoningReplayScope replaces it with the exact route owner or a
2568
+ // per-request fail-closed sentinel after the final provider and credential are known.
1363
2569
  const identityScope = codexLogAccountId(authCtx);
1364
2570
  if (identityScope) parsed._cursorIdentityScope = identityScope;
1365
2571
  subagentFallbackAccountId = authCtx.kind === "pool" || authCtx.kind === "main-pool"
@@ -1371,6 +2577,7 @@ async function handleResponsesInner(
1371
2577
  const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
1372
2578
  && route.provider.authMode === "oauth";
1373
2579
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
2580
+ let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
1374
2581
  let anthropicPoolAccountId: string | null = null;
1375
2582
  let anthropicPoolFailovers = 0;
1376
2583
  const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
@@ -1406,6 +2613,10 @@ async function handleResponsesInner(
1406
2613
  logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
1407
2614
  } else {
1408
2615
  const resolved = await getValidAccessTokenSnapshot(route.providerName);
2616
+ replayOAuthCredentialSnapshot = {
2617
+ accountId: resolved.accountId,
2618
+ generation: resolved.generation,
2619
+ };
1409
2620
  if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
1410
2621
  route.provider = { ...route.provider, apiKey: resolved.accessToken };
1411
2622
  if (route.providerName === "kiro") {
@@ -1414,21 +2625,23 @@ async function handleResponsesInner(
1414
2625
  parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
1415
2626
  }
1416
2627
  // 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.
2628
+ // CCA envelope. Keep it paired with the token snapshot so an account rotation cannot mix
2629
+ // a fresh token with project metadata re-read from a different credential generation.
1418
2630
  if (route.provider.googleMode === "cloud-code-assist" && !route.provider.project) {
1419
- const projectId = getOAuthCredentialProjectId(route.providerName);
2631
+ const projectId = resolved.projectId;
1420
2632
  if (projectId) route.provider = { ...route.provider, project: projectId };
1421
2633
  }
1422
2634
  }
1423
2635
  } catch (err) {
1424
2636
  if (err instanceof UnsupportedOAuthProviderError) {
2637
+ const safeProviderName = redactSecretString(route.providerName);
1425
2638
  return formatErrorResponse(
1426
2639
  400,
1427
2640
  "invalid_request_error",
1428
- `${err.message}. Remove or reconfigure provider '${route.providerName}' in ${getConfigPath()}.`,
2641
+ `${redactSecretString(err.message)}. Remove or reconfigure provider '${safeProviderName}' in the OpenCodex configuration.`,
1429
2642
  );
1430
2643
  }
1431
- return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
2644
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
1432
2645
  }
1433
2646
  }
1434
2647
  route.provider = resolveProviderTransport(
@@ -1437,10 +2650,70 @@ async function handleResponsesInner(
1437
2650
  parsed.options.promptCacheKey,
1438
2651
  route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
1439
2652
  );
1440
- const adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
2653
+ let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
2654
+ const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
2655
+ && adapterProvider.adapter === "openai-responses"
2656
+ && adapterProvider.authMode === "forward"
2657
+ && !isCanonicalOpenAiForwardProvider(adapterProvider);
2658
+ if (stripClaudeMainAuth) {
2659
+ releaseCodexAuthContextProbeLease(authCtx);
2660
+ authCtx = { kind: "main", accountId: null };
2661
+ route.provider = stripCodexRuntimeProviderFields(route.provider);
2662
+ adapterProvider = stripCodexRuntimeProviderFields(adapterProvider);
2663
+ selectedForwardHeaders = new Headers(selectedForwardHeaders);
2664
+ selectedForwardHeaders.delete("authorization");
2665
+ selectedForwardHeaders.delete("chatgpt-account-id");
2666
+ delete route.codexAccountMode;
2667
+ delete route.codexAccountId;
2668
+ delete route.codexAccountNamespace;
2669
+ logCtx.provider = route.providerName;
2670
+ delete logCtx.accountLogLabel;
2671
+ }
1441
2672
  const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
2673
+ bindRouteReasoningReplayScope({
2674
+ parsed,
2675
+ providerName: route.providerName,
2676
+ provider: adapterProvider,
2677
+ adapterName: adapter.name,
2678
+ oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
2679
+ codexAuthContext: authCtx,
2680
+ forwardHeaders: selectedForwardHeaders,
2681
+ });
2682
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
2683
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
2684
+ }
1442
2685
  logCtx.providerAdapter = adapter.name;
1443
- sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name);
2686
+ // Ordinary requests receive one durable attempt only after their final initial
2687
+ // adapter is resolved. Combo children own their attempt and retries keep it.
2688
+ if (!options.comboAttempt && !logCtx.activeAttempt) {
2689
+ const attempt = beginRequestAttempt(
2690
+ (logCtx.attempts?.length ?? 0) + 1,
2691
+ logCtx.provider,
2692
+ route.modelId,
2693
+ adapter.name,
2694
+ );
2695
+ logCtx.activeAttempt = attempt;
2696
+ logCtx.activeAttemptStartedAt = Date.now();
2697
+ (logCtx.attempts ??= []).push(attempt);
2698
+ }
2699
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
2700
+ if (adapter.runTurn) {
2701
+ recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
2702
+ }
2703
+ // Optional route-identity linkage for attempt correlation (CL-09 consumes it). The slot
2704
+ // resolves to null unless an opt-in subsystem registered a linker, so an install without
2705
+ // routing profiles does no work here and loads no additional module. The non-throwing
2706
+ // guarantee lives in the slot helper.
2707
+ if (logCtx.activeAttempt && !logCtx.activeAttempt.labRouteSubjectId) {
2708
+ const passiveSubjectId = resolvePassiveRouteSubjectId(
2709
+ config,
2710
+ route.providerName,
2711
+ route.modelId,
2712
+ route.provider,
2713
+ inboundWire,
2714
+ );
2715
+ if (passiveSubjectId) logCtx.activeAttempt.labRouteSubjectId = passiveSubjectId;
2716
+ }
1444
2717
  const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
1445
2718
 
1446
2719
  if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
@@ -1460,6 +2733,14 @@ async function handleResponsesInner(
1460
2733
  listOpenAiForwardSidecarCandidates(config),
1461
2734
  req.headers,
1462
2735
  config,
2736
+ {
2737
+ // Account-qualified native routes are passthrough, so their in-turn helper is vision.
2738
+ // Scope its cooldown and outcome to the helper model, not the routed text model.
2739
+ ...(route.codexAccountId !== undefined
2740
+ ? { exactAccount: { accountId: route.codexAccountId, modelId: resolveOpenAiVisionModel(config) } }
2741
+ : {}),
2742
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
2743
+ },
1463
2744
  );
1464
2745
  } catch (err) {
1465
2746
  // Sidecars are optional helpers for an otherwise independent routed turn.
@@ -1470,6 +2751,7 @@ async function handleResponsesInner(
1470
2751
  && !(err instanceof CodexAuthContextError)
1471
2752
  && !(err instanceof CodexAccountCooldownError)
1472
2753
  && !(err instanceof CodexThreadAffinityExpiredError)
2754
+ && !(err instanceof CodexMainProfileDrainingError)
1473
2755
  ) throw err;
1474
2756
  }
1475
2757
  }
@@ -1477,7 +2759,15 @@ async function handleResponsesInner(
1477
2759
  // Vision sidecar: the routed model can't see images (provider.noVisionModels). Describe each
1478
2760
  // attached image through the selected sidecar backend and replace it with text BEFORE the main
1479
2761
  // call, so the text-only model can reason about it.
1480
- const visionPlan = planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
2762
+ // Terminal describe fence (roadmap 180): the sidecar's OWN loopback describe
2763
+ // call must never plan another describe. The flag arrives from the Chat
2764
+ // surface (whose bridge rebuilds headers) or as the raw header for native
2765
+ // Responses callers. Marked + text-only routed model → strip, depth cap 1.
2766
+ const visionDescribeTerminal = options.visionDescribeTerminal === true
2767
+ || req.headers.get("x-opencodex-vision-describe") === "1";
2768
+ const visionPlan = visionDescribeTerminal
2769
+ ? undefined
2770
+ : planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
1481
2771
  const recordSidecarOutcome = openAiSidecar?.recordOutcome;
1482
2772
  if (visionPlan) {
1483
2773
  await describeImagesInPlace(
@@ -1488,7 +2778,7 @@ async function handleResponsesInner(
1488
2778
  recordSidecarOutcome,
1489
2779
  translatorBudget,
1490
2780
  );
1491
- } else if (modelInList(route.provider.noVisionModels, route.modelId)) {
2781
+ } else if (isModelTextOnly(route.provider, route.modelId)) {
1492
2782
  // Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar
1493
2783
  // disabled): fail closed — never forward raw images to a text-only upstream.
1494
2784
  stripImagesInPlace(parsed, translatorBudget);
@@ -1500,24 +2790,19 @@ async function handleResponsesInner(
1500
2790
  emitted?: OcxProviderContinuationState,
1501
2791
  ): OcxProviderContinuationState | undefined => {
1502
2792
  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
- };
2793
+ const inherited = providerContinuationPayload(parsed._providerContinuation);
2794
+ const emittedPayload = providerContinuationPayload(emitted);
2795
+ if (!emittedPayload && !inherited && !cursorConversationId) return undefined;
2796
+ const merged = mergeProviderContinuationPayload(
2797
+ inherited ?? {},
2798
+ emittedPayload ?? {},
2799
+ ) as OcxProviderContinuationState;
2800
+ if (cursorConversationId) {
2801
+ merged.cursor = { ...(merged.cursor ?? {}), conversationId: cursorConversationId };
2802
+ }
2803
+ return parsed._providerContinuationOwner
2804
+ ? { ...merged, __ocxOwner: { ...parsed._providerContinuationOwner } }
2805
+ : merged;
1521
2806
  };
1522
2807
 
1523
2808
  // Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
@@ -1530,18 +2815,40 @@ async function handleResponsesInner(
1530
2815
  // message, and leave Codex fataling on a missing compaction item (#422).
1531
2816
  const routedCompaction = parsed._compactionRequest === true
1532
2817
  && !isCanonicalOpenAiForwardProvider(route.provider);
2818
+ const commitReasoningReplayServingRoute = (): void => {
2819
+ commitReasoningReplayServingIdentity(parsed._reasoningReplayScope);
2820
+ };
1533
2821
  if (routedCompaction) {
1534
2822
  delete parsed.context.tools;
1535
2823
  delete parsed._webSearch;
1536
2824
  delete parsed.options.toolChoice;
1537
2825
  delete parsed.options.parallelToolCalls;
2826
+ // The compaction turn is a plain prose summary; a surviving structured-output format
2827
+ // would force schema-constrained JSON into the synthetic compaction item. The flag and
2828
+ // the raw `text` controls go too: Kiro's capability guard reads both and would reject
2829
+ // the turn outright, and the key-mode openai-responses adapter builds from _rawBody.
2830
+ delete parsed.options.textFormat;
2831
+ delete parsed._structuredOutput;
2832
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
2833
+ delete (parsed._rawBody as Record<string, unknown>).text;
2834
+ }
1538
2835
  parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() });
1539
2836
  }
1540
2837
 
2838
+ let routedNamespaceToolAliases: RoutedNamespaceToolAliases = new Map();
2839
+ const refreshRoutedNamespaceToolAliases = (builtRequest: AdapterRequest): void => {
2840
+ routedNamespaceToolAliases = builtRequest.convertedRoutedNamespaceToolAliases ?? new Map();
2841
+ };
2842
+
1541
2843
  if ("passthrough" in adapter && adapter.passthrough && !routedCompaction) {
2844
+ let hostAdmissionLease = pendingHostAdmissionLease;
2845
+ pendingHostAdmissionLease = null;
2846
+ try {
1542
2847
  const imageGenCallAliases = route.provider.authMode === "forward"
1543
2848
  ? new Map<string, { namespace: string; name: string }>()
1544
2849
  : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
2850
+ const routedCustomToolNames = new Set<string>();
2851
+ const routedToolSearchNames = new Set<string>();
1545
2852
  // Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
1546
2853
  // previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
1547
2854
  // REST backend rejects the parameter — the adapter strips it in forward mode, so the ONLY
@@ -1555,7 +2862,7 @@ async function handleResponsesInner(
1555
2862
  && (!parsed.previousResponseId || parsed._previousResponseInputExpanded === true);
1556
2863
  const rememberPassthroughResponse = passthroughRecordEligible
1557
2864
  ? (response: { id?: unknown; output?: unknown; status?: unknown }) =>
1558
- rememberResponseState(parsed._rawBody, response, undefined, { force: true })
2865
+ rememberResponseState(parsed._rawBody, response, undefined, responseStateOptions(true))
1559
2866
  : undefined;
1560
2867
  if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
1561
2868
  console.warn(
@@ -1563,9 +2870,142 @@ async function handleResponsesInner(
1563
2870
  + `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
1564
2871
  );
1565
2872
  }
1566
- let request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
2873
+ let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
2874
+ try {
2875
+ request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
2876
+ } catch (error) {
2877
+ releaseCodexAuthContextProbeLease(authCtx);
2878
+ // A tool catalog this proxy cannot lower onto one wire namespace is a client input error, and
2879
+ // the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
2880
+ // it here escaped every catch up to the Bun handler, so the same request produced an
2881
+ // unstructured 500 — and no request log — depending only on whether a rotation ran first.
2882
+ if (error instanceof NamespaceToolCollisionError) {
2883
+ return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
2884
+ }
2885
+ throw error;
2886
+ }
2887
+ if (!isCanonicalOpenAiForwardProvider(route.provider)) {
2888
+ for (const name of request.convertedRoutedCustomToolNames ?? []) {
2889
+ if (
2890
+ toolBridgeMaps.freeformToolNames.has(name)
2891
+ || toolBridgeMaps.toolNsMap.get(name)?.freeform === true
2892
+ ) routedCustomToolNames.add(name);
2893
+ }
2894
+ }
2895
+ for (const name of request.convertedRoutedToolSearchNames ?? []) {
2896
+ // The adapter already keeps this set empty when tool_choice forbids the private search.
2897
+ // Its wire name may be collision-aliased, so comparing it to the caller-facing name here
2898
+ // would incorrectly disable restoration for the exact ambiguous-name case the alias fixes.
2899
+ routedToolSearchNames.add(name);
2900
+ }
2901
+ refreshRoutedNamespaceToolAliases(request);
2902
+ // #1700: the bridged paths refuse a call to a tool the request never declared
2903
+ // (`declaredToolNames`, src/bridge.ts). The passthrough had no equivalent, so a routed
2904
+ // provider's top-level `apply_patch` — which under Codex code mode exists only as a nested
2905
+ // `tools.apply_patch(...)` helper inside `exec`, never as a wire tool — reached Codex as a
2906
+ // call it cannot execute, and the turn showed a bare `aborted` with the file untouched.
2907
+ // Forward auth is the canonical ChatGPT backend speaking Codex's own protocol rather than a
2908
+ // routed provider, so it keeps passing through unguarded, as it does for the rewrites above.
2909
+ // The guard needs a catalog to compare against, so it stands down when the request carries
2910
+ // none. That is not the same claim as "no tools means no tool may be called": a passthrough
2911
+ // request can legitimately omit `tools` entirely and still receive a tool call the client
2912
+ // understands — `tests/github-copilot-stream-contract.test.ts` sends `{model, input, stream}`
2913
+ // with no tools and Copilot answers with a `custom_tool_call` for `apply_patch`. Policing an
2914
+ // empty catalog truncates that turn. An unreadable body lands here too, since it yields no
2915
+ // names either.
2916
+ const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
2917
+ try {
2918
+ const body = JSON.parse(bodyText) as unknown;
2919
+ return body && typeof body === "object" && !Array.isArray(body)
2920
+ ? body as Record<string, unknown>
2921
+ : undefined;
2922
+ } catch {
2923
+ return undefined;
2924
+ }
2925
+ };
2926
+ let outboundRequestBody = parseOutboundRequestBody(request.body);
2927
+ const declaredWireToolNames = collectDeclaredWireToolNames(outboundRequestBody);
2928
+ // Union with the caller's own catalog, because the outbound body is not always a complete
2929
+ // record of it: hosted-tool preference REPLACES a client tool with its hosted form, so a
2930
+ // request declaring `image_gen.generate` ships `{type:"image_generation"}` upstream and gets
2931
+ // the client's name back. Widening only ever makes the guard fire less; a name declared in
2932
+ // neither place — #1700's `apply_patch` — is still refused.
2933
+ for (const name of toolBridgeMaps.declaredToolNames) declaredWireToolNames.add(name);
2934
+ const undeclaredToolGuardActive = declaredWireToolNames.size > 0
2935
+ && route.provider.authMode !== "forward";
2936
+ // A refused turn must not seed `previous_response_id` replay. The inspection branch reads the
2937
+ // untouched upstream stream, so it can still observe a `response.completed` the client never
2938
+ // received; checking the payload itself rather than a flag shared with the client relay keeps
2939
+ // this free of tee ordering races.
2940
+ //
2941
+ // Checking only the terminal snapshot is not enough. An upstream can announce the undeclared
2942
+ // call in `response.output_item.added`, which trips the client guard, and then close with a
2943
+ // `response.completed` whose `output` is empty. The client gets `response.failed`, the terminal
2944
+ // check sees nothing undeclared, and the refused turn enters continuation state anyway. So the
2945
+ // rejection is sticky for the whole turn, set from every parsed payload on the inspection side.
2946
+ let inspectionSawUndeclaredTool = false;
2947
+ const noteInspectedPayload = (payload: unknown) => {
2948
+ // Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth
2949
+ // provider) every name looks undeclared, and flipping this would stop recording continuation
2950
+ // state for exactly the passthrough traffic the guard deliberately stands down for.
2951
+ if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
2952
+ if (undeclaredToolCallName(payload, declaredWireToolNames) !== undefined) {
2953
+ inspectionSawUndeclaredTool = true;
2954
+ }
2955
+ };
2956
+ const rememberPassthroughResponseChecked = rememberPassthroughResponse
2957
+ ? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
2958
+ if (inspectionSawUndeclaredTool) return;
2959
+ if (
2960
+ undeclaredToolGuardActive
2961
+ && undeclaredToolCallNameInResponse(response, declaredWireToolNames) !== undefined
2962
+ ) {
2963
+ return;
2964
+ }
2965
+ rememberPassthroughResponse(response);
2966
+ }
2967
+ : undefined;
1567
2968
  recordAdapterReasoning(logCtx, request);
1568
- const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
2969
+ recordAdapterTier(logCtx, request);
2970
+ const actualHostKey = upstreamHostHealthKey(
2971
+ route.providerName,
2972
+ safeOriginLabel(request.url),
2973
+ );
2974
+ const hostKey = route.provider.authMode === "forward"
2975
+ ? actualHostKey
2976
+ : null;
2977
+ const hostCircuitEnabled = hostKey !== null
2978
+ && normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0;
2979
+ if (hostKey !== null && !hostCircuitEnabled) {
2980
+ disableUpstreamHostCircuitForKey(actualHostKey);
2981
+ }
2982
+ if (hostAdmissionLease && hostAdmissionLease.key !== hostKey) {
2983
+ return formatErrorResponse(502, "upstream_error", "Provider host changed after circuit admission");
2984
+ }
2985
+ if (options.abortSignal?.aborted) {
2986
+ releaseCodexAuthContextProbeLease(authCtx);
2987
+ return clientCancelledResponse();
2988
+ }
2989
+ if (!hostAdmissionLease && hostCircuitEnabled) {
2990
+ const admission = acquireUpstreamHostAdmission(
2991
+ hostKey!,
2992
+ config.upstreamHostCircuitThreshold,
2993
+ );
2994
+ if (admission.kind === "blocked") {
2995
+ releaseCodexAuthContextProbeLease(authCtx);
2996
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
2997
+ }
2998
+ hostAdmissionLease = admission.lease;
2999
+ }
3000
+ const settleObservedHostResponse = (): void => {
3001
+ if (hostCircuitEnabled) {
3002
+ resetUpstreamHostHealth(actualHostKey, hostAdmissionLease);
3003
+ } else {
3004
+ resetUpstreamHostHealth(actualHostKey);
3005
+ }
3006
+ hostAdmissionLease = null;
3007
+ };
3008
+ let passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
1569
3009
  ? request.usageLog.inputTokens
1570
3010
  : undefined;
1571
3011
  if (passthroughEstimate !== undefined) {
@@ -1580,11 +3020,35 @@ async function handleResponsesInner(
1580
3020
  let upstreamResponse: Response;
1581
3021
  const transportFailureResponse = (err: unknown): Response => {
1582
3022
  upstream.abort();
1583
- if (options.abortSignal?.aborted) return clientCancelledResponse();
1584
- const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
3023
+ if (options.abortSignal?.aborted) {
3024
+ releaseUpstreamHostAdmission(hostAdmissionLease);
3025
+ hostAdmissionLease = null;
3026
+ releaseCodexAuthContextProbeLease(authCtx);
3027
+ return clientCancelledResponse();
3028
+ }
3029
+ const outcome = classifyTransportFailureKind(err);
3030
+ // Host-level evidence stands regardless of pool membership: a direct
3031
+ // forward send has no pool accounting, but the reachability failure is
3032
+ // still host-wide, not account evidence (#914 review).
3033
+ if (outcome === "connect_neutral") {
3034
+ if (hostCircuitEnabled) {
3035
+ recordUpstreamHostFailure(actualHostKey, {
3036
+ code: transportErrorCode(err),
3037
+ threshold: config.upstreamHostCircuitThreshold,
3038
+ lease: hostAdmissionLease,
3039
+ });
3040
+ } else {
3041
+ recordUpstreamHostFailure(actualHostKey, { code: transportErrorCode(err) });
3042
+ }
3043
+ hostAdmissionLease = null;
3044
+ } else {
3045
+ releaseUpstreamHostAdmission(hostAdmissionLease);
3046
+ hostAdmissionLease = null;
3047
+ }
1585
3048
  if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1586
3049
  recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
1587
- threadId: req.headers.get("x-codex-parent-thread-id"),
3050
+ threadId: authCtx.affinityKey,
3051
+ fixedAccount: authCtx.fixedAccount,
1588
3052
  modelId: route.modelId,
1589
3053
  probeLeaseId: codexProbeLeaseId(authCtx),
1590
3054
  probeQuotaScope: codexProbeQuotaScope(authCtx),
@@ -1607,7 +3071,18 @@ async function handleResponsesInner(
1607
3071
  method: request.method,
1608
3072
  headers: request.headers,
1609
3073
  body: request.body,
1610
- }, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
3074
+ }, recovery), upstream.signal, connectMs, parsed.stream,
3075
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
3076
+ providerName: route.providerName,
3077
+ modelId: route.modelId,
3078
+ }),
3079
+ route.provider.authMode === "forward")
3080
+ // Every real attempt response — including an intermediate 5xx the
3081
+ // retry wrapper replaces — proves the host was reached (#914 review).
3082
+ .then(res => {
3083
+ settleObservedHostResponse();
3084
+ return res;
3085
+ });
1611
3086
  },
1612
3087
  { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
1613
3088
  );
@@ -1617,6 +3092,260 @@ async function handleResponsesInner(
1617
3092
  request.releaseBodyObservation?.();
1618
3093
  }
1619
3094
 
3095
+ const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
3096
+ let oauth401ReplayAttempted = false;
3097
+ const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
3098
+ let rateLimitRetries = 0;
3099
+ const rebuildAndRefetch = async (
3100
+ recovery: AttemptRecoveryKind,
3101
+ ): Promise<Response | { failed: Response }> => {
3102
+ const retryAdapter = resolveAdapter(
3103
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
3104
+ config.cacheRetention,
3105
+ );
3106
+ if (!("passthrough" in retryAdapter) || !retryAdapter.passthrough) {
3107
+ upstream.abort();
3108
+ return { failed: formatErrorResponse(502, "upstream_error", "Recovery changed the provider wire unexpectedly") };
3109
+ }
3110
+ try {
3111
+ request = await retryAdapter.buildRequest(parsed, {
3112
+ headers: selectedForwardHeaders,
3113
+ translatorBudget,
3114
+ });
3115
+ refreshRoutedNamespaceToolAliases(request);
3116
+ recordAdapterReasoning(logCtx, request);
3117
+ recordAdapterTier(logCtx, request);
3118
+ } catch (err) {
3119
+ upstream.abort();
3120
+ if (options.abortSignal?.aborted) return { failed: clientCancelledResponse() };
3121
+ const msg = err instanceof Error ? err.message : String(err);
3122
+ return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(msg)) };
3123
+ }
3124
+ passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
3125
+ ? request.usageLog.inputTokens
3126
+ : undefined;
3127
+ if (passthroughEstimate !== undefined) logCtx.usageLogInputTokens = passthroughEstimate;
3128
+ outboundRequestBody = parseOutboundRequestBody(request.body);
3129
+ logCtx.providerAdapter = retryAdapter.name;
3130
+ sealRequestAttemptIdentity(
3131
+ logCtx.activeAttempt,
3132
+ logCtx.provider,
3133
+ retryAdapter.name,
3134
+ logCtx.accountLogLabel,
3135
+ );
3136
+ try {
3137
+ return await fetchWithTransientRetry(
3138
+ innerRecovery => {
3139
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, innerRecovery ?? recovery);
3140
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
3141
+ method: request.method,
3142
+ headers: request.headers,
3143
+ body: request.body,
3144
+ }, innerRecovery), upstream.signal, connectMs, parsed.stream,
3145
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
3146
+ providerName: route.providerName,
3147
+ modelId: route.modelId,
3148
+ }),
3149
+ route.provider.authMode === "forward")
3150
+ .then(response => {
3151
+ settleObservedHostResponse();
3152
+ return response;
3153
+ });
3154
+ },
3155
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
3156
+ );
3157
+ } catch (err) {
3158
+ return { failed: transportFailureResponse(err) };
3159
+ } finally {
3160
+ request.releaseBodyObservation?.();
3161
+ }
3162
+ };
3163
+
3164
+ // Keep recovery kinds in sync with the generic `recovery:` loop below.
3165
+ passthroughRecovery: for (;;) {
3166
+
3167
+ // Native Responses providers return before the generic adapter recovery loop below. Keep
3168
+ // their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
3169
+ // rebuilt replay. xAI's current subscription models use this branch now that their official
3170
+ // Grok CLI catalog declares the Responses backend.
3171
+ if (
3172
+ upstreamResponse.status === 401
3173
+ && isOAuth401ReplayProvider
3174
+ && sentOAuthSnapshot
3175
+ && !oauth401ReplayAttempted
3176
+ ) {
3177
+ oauth401ReplayAttempted = true;
3178
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
3179
+ let refreshed: OAuthAccessSnapshot;
3180
+ try {
3181
+ refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
3182
+ } catch (err) {
3183
+ upstream.abort();
3184
+ releaseCodexAuthContextProbeLease(authCtx);
3185
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
3186
+ }
3187
+ sentOAuthSnapshot = refreshed;
3188
+ replayOAuthCredentialSnapshot = {
3189
+ accountId: refreshed.accountId,
3190
+ generation: refreshed.generation,
3191
+ };
3192
+ if (route.providerName === "kiro") {
3193
+ parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
3194
+ }
3195
+ const refreshedProvider = resolveProviderTransport(
3196
+ route.providerName,
3197
+ { ...route.provider, apiKey: refreshed.accessToken },
3198
+ parsed.options.promptCacheKey,
3199
+ route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
3200
+ );
3201
+ route.provider = refreshedProvider;
3202
+ const refreshedAdapter = resolveAdapter(
3203
+ resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
3204
+ config.cacheRetention,
3205
+ );
3206
+ if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
3207
+ upstream.abort();
3208
+ return formatErrorResponse(502, "upstream_error", "OAuth refresh changed the provider wire unexpectedly");
3209
+ }
3210
+ bindRouteReasoningReplayScope({
3211
+ parsed,
3212
+ providerName: route.providerName,
3213
+ provider: refreshedProvider,
3214
+ adapterName: refreshedAdapter.name,
3215
+ oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
3216
+ });
3217
+ logCtx.providerAdapter = refreshedAdapter.name;
3218
+ sealRequestAttemptIdentity(
3219
+ logCtx.activeAttempt,
3220
+ logCtx.provider,
3221
+ refreshedAdapter.name,
3222
+ logCtx.accountLogLabel,
3223
+ );
3224
+ try {
3225
+ request = await refreshedAdapter.buildRequest(parsed, {
3226
+ headers: selectedForwardHeaders,
3227
+ translatorBudget,
3228
+ });
3229
+ refreshRoutedNamespaceToolAliases(request);
3230
+ recordAdapterReasoning(logCtx, request);
3231
+ recordAdapterTier(logCtx, request);
3232
+ } catch (err) {
3233
+ upstream.abort();
3234
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
3235
+ const msg = err instanceof Error ? err.message : String(err);
3236
+ return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
3237
+ }
3238
+ try {
3239
+ upstreamResponse = await fetchWithTransientRetry(
3240
+ recovery => {
3241
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "oauth-401");
3242
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
3243
+ method: request.method,
3244
+ headers: request.headers,
3245
+ body: request.body,
3246
+ }, recovery), upstream.signal, connectMs, parsed.stream,
3247
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
3248
+ providerName: route.providerName,
3249
+ modelId: route.modelId,
3250
+ }),
3251
+ route.provider.authMode === "forward")
3252
+ .then(res => {
3253
+ settleObservedHostResponse();
3254
+ return res;
3255
+ });
3256
+ },
3257
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
3258
+ );
3259
+ } catch (err) {
3260
+ return transportFailureResponse(err);
3261
+ } finally {
3262
+ request.releaseBodyObservation?.();
3263
+ }
3264
+ }
3265
+
3266
+ // Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
3267
+ // passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
3268
+ // key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
3269
+ // immediately with no same-key replay. Pre-stream only — nothing has been relayed yet, so
3270
+ // the replay is lossless (same invariant as the recovery loop). Forward/OAuth providers
3271
+ // keep their pool logic below (rateLimitRetryPolicyFor returns null for them).
3272
+ while (
3273
+ upstreamResponse.status === 429
3274
+ && rateLimitPolicy !== null
3275
+ && rateLimitRetries < rateLimitPolicy.attempts
3276
+ ) {
3277
+ rateLimitRetries += 1;
3278
+ // Release unread body + deliberate wait via the shared same-target helper.
3279
+ const retryAfterHeader = upstreamResponse.headers.get("retry-after");
3280
+ try {
3281
+ for await (const _ of prepareSameTarget429Wait({
3282
+ body: upstreamResponse.body,
3283
+ signal: options.abortSignal,
3284
+ delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
3285
+ })) {
3286
+ // pre-stream: no stall watchdog to feed
3287
+ }
3288
+ } catch {
3289
+ upstream.abort();
3290
+ return clientCancelledResponse();
3291
+ }
3292
+ // Client cancellation wins over any stale timer edge: re-check before dispatching the
3293
+ // replay so the wire never starts work for a request the client already abandoned.
3294
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
3295
+ upstream.abort();
3296
+ return clientCancelledResponse();
3297
+ }
3298
+ try {
3299
+ upstreamResponse = await fetchWithTransientRetry(
3300
+ recovery => {
3301
+ // The first send of every replay is itself a rate-limit retry; inner transient-5xx
3302
+ // recoveries keep their own label (recovery is provided for those).
3303
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "rate-limit-429");
3304
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
3305
+ method: request.method,
3306
+ headers: request.headers,
3307
+ body: request.body,
3308
+ }, recovery), upstream.signal, connectMs, parsed.stream,
3309
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
3310
+ providerName: route.providerName,
3311
+ modelId: route.modelId,
3312
+ }),
3313
+ route.provider.authMode === "forward")
3314
+ .then(res => {
3315
+ settleObservedHostResponse();
3316
+ return res;
3317
+ });
3318
+ },
3319
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
3320
+ );
3321
+ } catch (err) {
3322
+ return transportFailureResponse(err);
3323
+ }
3324
+ }
3325
+
3326
+ const captureAffinityResponse = (
3327
+ response: Response,
3328
+ captureAuthCtx: CodexAuthContext = authCtx,
3329
+ captureRequest: Awaited<ReturnType<typeof adapter.buildRequest>> = request,
3330
+ credentialSubstituted = substituteMainCredential
3331
+ || captureAuthCtx.kind === "pool"
3332
+ || captureAuthCtx.kind === "main-pool",
3333
+ ): void => {
3334
+ if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
3335
+ captureCodexAffinityDiagnostic({
3336
+ inboundHeaders: req.headers,
3337
+ outboundHeaders: captureRequest.headers,
3338
+ authKind: captureAuthCtx.kind,
3339
+ accountMode: route.codexAccountMode,
3340
+ fixedAccount: isFixedCodexAccount(captureAuthCtx),
3341
+ credentialSubstituted,
3342
+ accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
3343
+ wireModelNormalized: parsed.modelId !== route.modelId,
3344
+ status: response.status,
3345
+ });
3346
+ };
3347
+ captureAffinityResponse(upstreamResponse);
3348
+
1620
3349
  if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1621
3350
  let poolRetryOutcome: number | undefined;
1622
3351
  if (await shouldRetryCodexPoolAccountModel400(
@@ -1625,7 +3354,7 @@ async function handleResponsesInner(
1625
3354
  options.abortSignal,
1626
3355
  )) {
1627
3356
  poolRetryOutcome = 400;
1628
- } else if (shouldRetryCodexPoolAccountQuota(upstreamResponse)) {
3357
+ } else if (!authCtx.fixedAccount && shouldRetryCodexPoolAccountQuota(upstreamResponse)) {
1629
3358
  // Pre-stream only: once SSE has begun, mid-stream quota stays terminal.
1630
3359
  poolRetryOutcome = upstreamResponse.status;
1631
3360
  }
@@ -1645,6 +3374,9 @@ async function handleResponsesInner(
1645
3374
  connectMs,
1646
3375
  passthroughEstimate,
1647
3376
  stream: parsed.stream,
3377
+ onResponse: (response, retryAuthCtx, retryRequest) => {
3378
+ captureAffinityResponse(response, retryAuthCtx, retryRequest, true);
3379
+ },
1648
3380
  });
1649
3381
  if (retry.kind === "transport") {
1650
3382
  authCtx = retry.authCtx;
@@ -1653,6 +3385,7 @@ async function handleResponsesInner(
1653
3385
  if (retry.kind === "retried") {
1654
3386
  authCtx = retry.authCtx;
1655
3387
  request = retry.request;
3388
+ refreshRoutedNamespaceToolAliases(request);
1656
3389
  upstreamResponse = retry.upstreamResponse;
1657
3390
  selectedForwardHeaders = retry.selectedForwardHeaders;
1658
3391
  // Keep subagent quota-failure health keyed to the account that actually served.
@@ -1660,6 +3393,25 @@ async function handleResponsesInner(
1660
3393
  }
1661
3394
  }
1662
3395
  }
3396
+ // The deterministic route record cannot classify history it never observed (restart, expiry,
3397
+ // eviction, or an older transcript). Inspect only a bounded clone of a 4xx whose exact outbound
3398
+ // Responses body still carries opaque state, then rebuild once through the ordinary adapter
3399
+ // sanitation path. A second rejection falls through unchanged because the guard stays armed.
3400
+ const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
3401
+ response: upstreamResponse,
3402
+ outboundBody: request.body,
3403
+ adapterName: adapter.name,
3404
+ parsed,
3405
+ guard: opaqueBlobRecoveryGuard,
3406
+ signal: upstream.signal,
3407
+ }, rebuildAndRefetch);
3408
+ if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
3409
+ if (opaqueBlobRecovery.kind === "recovered") {
3410
+ upstreamResponse = opaqueBlobRecovery.response;
3411
+ continue passthroughRecovery;
3412
+ }
3413
+ break;
3414
+ }
1663
3415
  const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
1664
3416
  const resolvedModel = headers.get("openai-model")?.trim();
1665
3417
  if (resolvedModel) logCtx.resolvedModel = resolvedModel;
@@ -1678,14 +3430,13 @@ async function handleResponsesInner(
1678
3430
  route.provider,
1679
3431
  route.modelId,
1680
3432
  logCtx,
1681
- req.headers.get("x-codex-parent-thread-id"),
1682
3433
  );
1683
3434
  const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
1684
3435
  // Capture quota from upstream response for multi-account tracking
1685
3436
  if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1686
3437
  // primary was the 5h window; it now carries weekly data for GPT plans.
1687
3438
  // Prefer primary when present, fall back to secondary for compatibility.
1688
- const quotaMeta = codexQuotaOutcomeMeta(upstreamResponse);
3439
+ const quotaMeta = { ...codexQuotaOutcomeMeta(upstreamResponse), ...(await codexDenialOutcomeMeta(upstreamResponse)) };
1689
3440
  const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
1690
3441
  applyAccountQuotaFromUpstreamHeaders(
1691
3442
  authCtx.accountId,
@@ -1701,7 +3452,7 @@ async function handleResponsesInner(
1701
3452
  || logCtx.terminalHttpStatus === 402
1702
3453
  ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1703
3454
  : undefined;
1704
- if (quotaFailureMessage !== undefined) {
3455
+ if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
1705
3456
  recordSubagentQuotaFailureForThreadSpawn(
1706
3457
  req.headers,
1707
3458
  subagentQuotaFailureModel,
@@ -1719,7 +3470,8 @@ async function handleResponsesInner(
1719
3470
  )) {
1720
3471
  recordCodexUpstreamOutcome(config, authCtx.accountId, upstreamResponse.status, {
1721
3472
  ...quotaMeta,
1722
- threadId: req.headers.get("x-codex-parent-thread-id"),
3473
+ threadId: authCtx.affinityKey,
3474
+ fixedAccount: authCtx.fixedAccount,
1723
3475
  modelId: route.modelId,
1724
3476
  probeLeaseId: codexProbeLeaseId(authCtx),
1725
3477
  probeQuotaScope: codexProbeQuotaScope(authCtx),
@@ -1732,17 +3484,42 @@ async function handleResponsesInner(
1732
3484
  // Codex renders that as the opaque "Unknown error" (#452). Combo attempts
1733
3485
  // keep their typed failure envelope. Non-empty bodies are relayed verbatim
1734
3486
  // (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
3487
+ // Manual-redirect policy (#914): a 3xx is relayed as-is (Location preserved
3488
+ // through sanitizePassthroughHeaders) so a redirect to a dead host can never
3489
+ // masquerade as a pre-connection failure after the credential was seen.
3490
+ // The numeric outcome above already classified it neutral — no streak.
3491
+ if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
3492
+ return new Response(upstreamResponse.body, {
3493
+ status: upstreamResponse.status,
3494
+ statusText: upstreamResponse.statusText,
3495
+ headers: sanitizePassthroughHeaders(upstreamResponse.headers),
3496
+ });
3497
+ }
1735
3498
  if (!upstreamResponse.ok) {
1736
3499
  if (options.comboAttempt) {
3500
+ // No pre-read guard here: `consumeComboFailure` -> `readBoundedResponseBody` reads
3501
+ // `response.body` itself and already threads the abort signal through its own read,
3502
+ // and the combo contract is that this body's getter is touched exactly once (pinned by
3503
+ // "captures passthrough failed usage from its original bounded body exactly once").
3504
+ // Attaching a guard would be a second `.body` access and break that contract for no
3505
+ // gain, since the bounded reader owns settlement on this path.
1737
3506
  const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
1738
3507
  options.onConsumedComboFailure?.(failure);
1739
3508
  return failure.response;
1740
3509
  }
1741
- const errorText = await upstreamResponse.text().catch(() => "");
1742
- return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
1743
- statusText: upstreamResponse.statusText,
1744
- headers,
1745
- });
3510
+ // The plain passthrough error path has no bounded reader of its own: `.text()` attaches
3511
+ // the reader only when it runs, so an abort landing between fetch resolution and that
3512
+ // call orphans Bun's internal read rejection (src/lib/abort.ts).
3513
+ const detachPassthroughErrorGuard = cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
3514
+ try {
3515
+ const errorText = await upstreamResponse.text().catch(() => "");
3516
+ return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
3517
+ statusText: upstreamResponse.statusText,
3518
+ headers,
3519
+ });
3520
+ } finally {
3521
+ detachPassthroughErrorGuard();
3522
+ }
1746
3523
  }
1747
3524
 
1748
3525
  // Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
@@ -1754,18 +3531,80 @@ async function handleResponsesInner(
1754
3531
  // `streamMode: "eager-relay"` opt-in. Darwin `auto` always stays tee. The
1755
3532
  // eager shape skips tee and uses one bounded reader with inline inspection
1756
3533
  // (src/server/relay-eager.ts; policy:
1757
- // devlog/_plan/260731_macos_rss_retention/100_darwin_eager_optin.md).
3534
+ // devlog/_fin/260731_macos_rss_retention/100_darwin_eager_optin.md).
1758
3535
  // The bundled known-bad runtime remains on tee by default on both platforms.
1759
3536
  if (isEventStream && upstreamResponse.body) {
3537
+ // For streamed passthrough, a successful terminal response means non-error upstream status
3538
+ // before relay starts. Waiting for SSE completion would retain request state across the whole
3539
+ // stream; a later body failure does not undo that this destination accepted and served the turn.
3540
+ commitReasoningReplayServingRoute();
3541
+ const terminalRepairPolicy = providerModelResponsesTerminalRepair(
3542
+ route.providerName,
3543
+ route.provider,
3544
+ route.modelId,
3545
+ );
3546
+ const passthroughSseBody = terminalRepairPolicy
3547
+ ? relayResponsesSseWithTerminalRepair(
3548
+ upstreamResponse.body,
3549
+ upstream,
3550
+ terminalRepairPolicy,
3551
+ translatorBudget,
3552
+ options.responsesTerminalRepairScheduler,
3553
+ )
3554
+ : upstreamResponse.body;
1760
3555
  const repairConfig = route.provider.responsesItemIdRepair;
1761
- const needsClientRewrite = imageGenCallAliases.size > 0 || hasResponsesItemIdRepair(repairConfig);
3556
+ const snapshotRepairEnabled = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair);
3557
+ const githubCopilotRepairEnabled = route.providerName === "github-copilot";
3558
+ const responseModelRewrite = parsed._responseModelId !== undefined
3559
+ && parsed._responseModelId !== parsed.modelId
3560
+ ? createResponsesModelPayloadRewrite(parsed._responseModelId)
3561
+ : undefined;
1762
3562
  // Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
1763
3563
  const payloadRewrites = [
1764
3564
  createImageGenCallRestoreRewrite(imageGenCallAliases),
3565
+ routedNamespaceToolAliases.size > 0
3566
+ ? createRoutedNamespaceCallRestoreRewrite(routedNamespaceToolAliases)
3567
+ : undefined,
1765
3568
  hasResponsesItemIdRepair(repairConfig)
1766
3569
  ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
1767
3570
  : undefined,
3571
+ responseModelRewrite,
3572
+ parsed.options.hideThinkingSummary !== true
3573
+ && routeUsesContentChannelReasoning(route.provider, route.modelId)
3574
+ ? createReasoningSummaryChannelPayloadRewrite()
3575
+ : undefined,
3576
+ ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
3577
+ // #893: sparse-snapshot gateways get field backfills AND lifecycle event
3578
+ // injection at the block level, after payload rewrites. Defaults come
3579
+ // from the finalized OUTBOUND body — the normalized internal tool shapes
3580
+ // are not the Responses wire shapes the snapshot must mirror.
3581
+ const blockRewrites = [
3582
+ payloadRewrites.length > 0
3583
+ ? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
3584
+ : undefined,
3585
+ routedCustomToolNames.size > 0
3586
+ ? createRoutedCustomToolRestoreBlockRewrite(routedCustomToolNames, translatorBudget)
3587
+ : undefined,
3588
+ routedToolSearchNames.size > 0
3589
+ ? createRoutedToolSearchRestoreBlockRewrite(routedToolSearchNames, translatorBudget)
3590
+ : undefined,
3591
+ githubCopilotRepairEnabled
3592
+ ? createGithubCopilotResponsesBlockRewrite(translatorBudget)
3593
+ : undefined,
3594
+ snapshotRepairEnabled
3595
+ ? createResponsesSnapshotBlockRewrite(outboundRequestBody, translatorBudget)
3596
+ : undefined,
3597
+ createResponsesFieldBackfillBlockRewrite(),
3598
+ // Last: every rewrite above can still rename or reshape a call item, so the guard must
3599
+ // compare the names the client will actually receive against the declared catalog.
3600
+ undeclaredToolGuardActive
3601
+ ? createUndeclaredToolCallGuardBlockRewrite(declaredWireToolNames)
3602
+ : undefined,
1768
3603
  ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
3604
+ const clientBlockRewrite = blockRewrites.length > 0
3605
+ ? composeSseBlockRewrites(...blockRewrites)
3606
+ : undefined;
3607
+ const needsClientRewrite = clientBlockRewrite !== undefined;
1769
3608
  // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
1770
3609
  // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
1771
3610
  // lost). The eager single reader applies the same rewrites inline.
@@ -1775,7 +3614,14 @@ async function handleResponsesInner(
1775
3614
  needsClientRewrite,
1776
3615
  config.streamMode ?? "auto",
1777
3616
  );
1778
- if (eagerPath?.useEagerRelay || win32EagerRewrite) {
3617
+ // A successful Codex WS upgrade is a push source. If it entered tee(),
3618
+ // the inspection branch could drain continuously while the slow client
3619
+ // branch retained bytes without a bound. Force the existing bounded,
3620
+ // single-reader relay before tee; HTTP fallback responses stay unmarked.
3621
+ const forceCodexWsEagerRelay = isCodexWsUpstreamResponse(upstreamResponse);
3622
+ const inlineEagerRewrite = needsClientRewrite
3623
+ && (forceCodexWsEagerRelay || win32EagerRewrite || eagerPath?.useEagerRelay === true);
3624
+ if (forceCodexWsEagerRelay || eagerPath?.useEagerRelay || win32EagerRewrite) {
1779
3625
  const turnAc = new AbortController();
1780
3626
  linkAbortSignal(upstream, turnAc.signal);
1781
3627
  registerTurn(turnAc, options.turnAdmissionLease);
@@ -1788,7 +3634,7 @@ async function handleResponsesInner(
1788
3634
  || logCtx.terminalHttpStatus === 402
1789
3635
  ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1790
3636
  : undefined;
1791
- if (quotaFailureMessage !== undefined) {
3637
+ if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
1792
3638
  recordSubagentQuotaFailureForThreadSpawn(
1793
3639
  req.headers,
1794
3640
  subagentQuotaFailureModel,
@@ -1804,16 +3650,20 @@ async function handleResponsesInner(
1804
3650
  const inspector = createSseInspector({
1805
3651
  onTerminal: reportNativeTerminal,
1806
3652
  logCtx,
1807
- onCompletedResponse: rememberPassthroughResponse,
3653
+ onCompletedResponse: rememberPassthroughResponseChecked,
3654
+ onParsedPayload: noteInspectedPayload,
1808
3655
  onFirstOutput: options.onFirstOutput,
3656
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
1809
3657
  });
1810
- const eagerBody = relaySseEagerBounded(upstreamResponse.body, turnAc, {
3658
+ const eagerBody = relaySseEagerBounded(passthroughSseBody, turnAc, {
1811
3659
  inspectChunk: chunk => inspector.feed(chunk),
1812
3660
  finishInspection: () => inspector.finish(),
1813
3661
  disposeInspection: () => inspector.dispose(),
1814
- sawTerminal: () => inspector.reported(),
1815
- ...(win32EagerRewrite
1816
- ? { rewritePayload: composeSsePayloadRewrites(...payloadRewrites) }
3662
+ // Stream lifetime follows the protocol terminal even when this request
3663
+ // has no outcome callback configured (reported() would stay false).
3664
+ sawTerminal: () => inspector.terminalSeen(),
3665
+ ...(clientBlockRewrite
3666
+ ? { rewriteBlocks: clientBlockRewrite }
1817
3667
  : {}),
1818
3668
  onSynthetic: kind => {
1819
3669
  if (!reportNativeTerminal) return;
@@ -1823,15 +3673,17 @@ async function handleResponsesInner(
1823
3673
  } else {
1824
3674
  logCtx.transportPhase = "mid_stream";
1825
3675
  logCtx.terminalSource = "synthetic";
3676
+ if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
1826
3677
  reportNativeTerminal("failed", 502);
1827
3678
  }
1828
3679
  },
1829
3680
  onClientCancel: () => options.onNativePassthroughCancel?.(),
1830
3681
  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).
3682
+ }, inlineEagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
3683
+ // When selected, this relay closes response.completed even if upstream
3684
+ // keeps the connection alive. Marked Codex WS traffic, Windows
3685
+ // forced-rewrite traffic, and Darwin explicit eager traffic apply
3686
+ // client rewrites inline rather than via the tee()+JS-pull chain.
1835
3687
  if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
1836
3688
  return markEagerRelaySseResponse(
1837
3689
  markNativePassthroughSseResponse(new Response(eagerBody, {
@@ -1840,7 +3692,7 @@ async function handleResponsesInner(
1840
3692
  })),
1841
3693
  );
1842
3694
  }
1843
- const [nativeBody, inspectBody] = upstreamResponse.body.tee();
3695
+ const [nativeBody, inspectBody] = passthroughSseBody.tee();
1844
3696
  const turnAc = new AbortController();
1845
3697
  const clientGone = new AbortController();
1846
3698
  linkAbortSignal(upstream, turnAc.signal);
@@ -1849,6 +3701,8 @@ async function handleResponsesInner(
1849
3701
  clientGoneSignal: clientGone.signal,
1850
3702
  drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
1851
3703
  upstream,
3704
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
3705
+ onParsedPayload: noteInspectedPayload,
1852
3706
  };
1853
3707
  if (recordTerminalOutcomes) {
1854
3708
  // A real terminal was parsed from the (teed) inspection stream — record it as the outcome
@@ -1863,7 +3717,7 @@ async function handleResponsesInner(
1863
3717
  || logCtx.terminalHttpStatus === 402
1864
3718
  ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1865
3719
  : undefined;
1866
- if (quotaFailureMessage !== undefined) {
3720
+ if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
1867
3721
  recordSubagentQuotaFailureForThreadSpawn(
1868
3722
  req.headers,
1869
3723
  subagentQuotaFailureModel,
@@ -1882,7 +3736,7 @@ async function handleResponsesInner(
1882
3736
  () => unregisterTurn(turnAc),
1883
3737
  logCtx,
1884
3738
  () => options.onNativePassthroughCancel?.(),
1885
- rememberPassthroughResponse,
3739
+ rememberPassthroughResponseChecked,
1886
3740
  options.onFirstOutput,
1887
3741
  inspectionConsumerOptions,
1888
3742
  );
@@ -1892,40 +3746,168 @@ async function handleResponsesInner(
1892
3746
  logCtx,
1893
3747
  turnAc.signal,
1894
3748
  () => unregisterTurn(turnAc),
1895
- rememberPassthroughResponse,
3749
+ rememberPassthroughResponseChecked,
1896
3750
  options.onFirstOutput,
1897
3751
  inspectionConsumerOptions,
1898
3752
  );
1899
3753
  }
1900
3754
  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)
3755
+ // Windows was handled by the eager terminal-aware branch above. Remaining
3756
+ // tee traffic can use the JS relay to close on a protocol terminal and to
3757
+ // convert a mid-stream reset into a clean response.failed event.
3758
+ const rewrittenBody = clientBlockRewrite !== undefined
3759
+ ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget)
1906
3760
  : nativeBody;
1907
- const clientBody = process.platform === "win32" && !needsClientRewrite
1908
- ? nativeBody
1909
- : relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason));
3761
+ const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason));
1910
3762
  return markNativePassthroughSseResponse(new Response(clientBody, {
1911
3763
  status: upstreamResponse.status,
1912
3764
  headers,
1913
3765
  }));
1914
3766
  }
1915
3767
  if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
1916
- const text = await upstreamResponse.text();
3768
+ // Bounded whole-body read: a non-streaming upstream JSON body is fully materialized
3769
+ // here (and again by the request-log finalizer and the WebSocket bridge's reframing),
3770
+ // so an unbounded .text() would let a hostile or stuck upstream grow proxy memory
3771
+ // without limit. This path is no longer rare — WebSocket turns for models whose
3772
+ // streaming terminal event is unreliable are deliberately answered with bounded JSON.
3773
+ // Oversize and stall deadlines both fail closed; a partial body is never parsed.
3774
+ const bounded = await readBoundedResponseBody(upstreamResponse, UPSTREAM_JSON_BODY_READ_OPTIONS);
3775
+ if (bounded.oversized) {
3776
+ return formatErrorResponse(502, "upstream_error", "upstream JSON response exceeded the safe body limit");
3777
+ }
3778
+ if (bounded.truncated) {
3779
+ return formatErrorResponse(502, "upstream_error", "upstream JSON response stalled before completing");
3780
+ }
3781
+ const text = bounded.text;
1917
3782
  inspectResponseLogJson(logCtx, text);
1918
- if (rememberPassthroughResponse) {
3783
+ const clientJson = (() => {
3784
+ const restoredNamespace = restoreRoutedNamespaceCallsInJson(
3785
+ restoreImageGenCallsInJson(text, imageGenCallAliases),
3786
+ routedNamespaceToolAliases,
3787
+ );
3788
+ const restored = restoreRoutedCustomCallsInJson(
3789
+ restoredNamespace,
3790
+ routedCustomToolNames,
3791
+ );
3792
+ const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
3793
+ restored,
3794
+ routedToolSearchNames,
3795
+ );
3796
+ const repaired = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)
3797
+ ? repairResponsesSnapshotJson(restoredToolSearch, outboundRequestBody)
3798
+ : restoredToolSearch;
3799
+ const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
3800
+ ? rewriteResponsesModelJson(backfillResponsesFieldsJson(repaired), parsed._responseModelId)
3801
+ : backfillResponsesFieldsJson(repaired);
3802
+ // The bounded-JSON answer bypasses the SSE payload rewrite, so content-
3803
+ // channel reasoning needs the same normalization here for the plain
3804
+ // JSON answer and every reframed-SSE variant built from clientJson.
3805
+ return parsed.options.hideThinkingSummary !== true
3806
+ && routeUsesContentChannelReasoning(route.provider, route.modelId)
3807
+ ? rewriteReasoningSummaryInJsonString(modelRewritten)
3808
+ : modelRewritten;
3809
+ })();
3810
+ // #1700: same fail-closed policy as the SSE relay above. Both the plain JSON answer and
3811
+ // the reframed-SSE branch below are built from this body, so one check covers them. This
3812
+ // runs BEFORE the continuation cache write below: a refused turn must not become state a
3813
+ // later `previous_response_id` replay can expand from.
3814
+ if (undeclaredToolGuardActive) {
3815
+ const undeclared = (() => {
3816
+ try {
3817
+ return undeclaredToolCallNameInResponse(JSON.parse(clientJson), declaredWireToolNames);
3818
+ } catch {
3819
+ return undefined;
3820
+ }
3821
+ })();
3822
+ if (undeclared !== undefined) {
3823
+ return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared));
3824
+ }
3825
+ }
3826
+ commitReasoningReplayServingRoute();
3827
+ if (rememberPassthroughResponseChecked) {
1919
3828
  try {
1920
- rememberPassthroughResponse(JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown });
3829
+ rememberPassthroughResponseChecked(
3830
+ JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown },
3831
+ );
1921
3832
  } catch { /* non-JSON despite content-type; recording is best-effort */ }
1922
3833
  }
1923
- return new Response(restoreImageGenCallsInJson(text, imageGenCallAliases), {
3834
+ // #875: the transport-neutral reliability policy forced a bounded JSON
3835
+ // upstream for a client that asked for SSE. Reframe the completed JSON
3836
+ // as the canonical terminal SSE sequence (created → output_item.done →
3837
+ // terminal → [DONE]) so Codex commits the turn instead of hanging on a
3838
+ // stream that never closes. Non-streaming clients keep the plain JSON.
3839
+ if (clientRequestedStream === true
3840
+ && options.inboundTransport !== "websocket"
3841
+ && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
3842
+ && route.provider.adapter === "openai-responses") {
3843
+ let completed: Record<string, unknown> | undefined;
3844
+ try {
3845
+ const parsedCompleted = JSON.parse(clientJson) as unknown;
3846
+ if (!parsedCompleted || typeof parsedCompleted !== "object" || Array.isArray(parsedCompleted)) {
3847
+ throw new TypeError("bounded Responses JSON is not an object");
3848
+ }
3849
+ let candidate = parsedCompleted as Record<string, unknown>;
3850
+ // The bounded-JSON answer bypasses the SSE relay, so it also bypasses
3851
+ // the SSE item-id rewrite. Apply the same client-facing normalization
3852
+ // here or this policy would silently disable id repair for the very
3853
+ // providers that need it (raw record already happened above).
3854
+ if (hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)) {
3855
+ candidate = repairResponsesJsonItemIds(candidate, route.provider.responsesItemIdRepair!, translatorBudget);
3856
+ }
3857
+ completed = candidate;
3858
+ } catch {
3859
+ // Non-JSON despite content-type: fall through to the plain relay.
3860
+ }
3861
+ if (completed) {
3862
+ let stream: ReadableStream<Uint8Array>;
3863
+ try {
3864
+ stream = responsesJsonToSseStream(completed);
3865
+ } catch (error) {
3866
+ if (error instanceof RangeError) {
3867
+ return formatErrorResponse(
3868
+ 502,
3869
+ "upstream_error",
3870
+ "upstream JSON response exceeded the synthesized SSE item limit",
3871
+ );
3872
+ }
3873
+ throw error;
3874
+ }
3875
+ const sseHeaders = sanitizePassthroughHeaders(headers);
3876
+ sseHeaders.set("content-type", "text/event-stream");
3877
+ sseHeaders.set("cache-control", "no-store");
3878
+ return new Response(stream, {
3879
+ status: upstreamResponse.status,
3880
+ statusText: upstreamResponse.statusText,
3881
+ headers: sseHeaders,
3882
+ });
3883
+ }
3884
+ }
3885
+ // WS turns reframe this JSON into events in the bridge, which is the
3886
+ // other relay-free path — normalize ids so both bounded-JSON paths agree.
3887
+ const outboundJson = options.inboundTransport === "websocket"
3888
+ && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
3889
+ && hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
3890
+ ? (() => {
3891
+ try {
3892
+ return JSON.stringify(repairResponsesJsonItemIds(
3893
+ JSON.parse(clientJson) as Record<string, unknown>,
3894
+ route.provider.responsesItemIdRepair!,
3895
+ translatorBudget,
3896
+ ));
3897
+ } catch {
3898
+ return clientJson;
3899
+ }
3900
+ })()
3901
+ : clientJson;
3902
+ return new Response(outboundJson, {
1924
3903
  status: upstreamResponse.status,
1925
3904
  statusText: upstreamResponse.statusText,
1926
3905
  headers,
1927
3906
  });
1928
3907
  }
3908
+ // An unclassified passthrough body is relayed directly and has no bounded completion observer;
3909
+ // use the same non-error-status success boundary as SSE instead of retaining per-stream state.
3910
+ commitReasoningReplayServingRoute();
1929
3911
  const body = relayWithAbort(upstreamResponse.body, upstream);
1930
3912
  const turnAc = new AbortController();
1931
3913
  const tracked = body ? trackStreamLifetime(body, turnAc, undefined, options.turnAdmissionLease) : null;
@@ -1933,6 +3915,12 @@ async function handleResponsesInner(
1933
3915
  status: upstreamResponse.status,
1934
3916
  headers,
1935
3917
  });
3918
+ } finally {
3919
+ if (hostAdmissionLease) {
3920
+ releaseUpstreamHostAdmission(hostAdmissionLease);
3921
+ releaseCodexAuthContextProbeLease(authCtx);
3922
+ }
3923
+ }
1936
3924
  }
1937
3925
 
1938
3926
  // Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
@@ -2000,13 +3988,19 @@ async function handleResponsesInner(
2000
3988
  && (tc.name === "image_generation" || imgPlan.toolNames.has(tc.name))) {
2001
3989
  parsed.options.toolChoice = { ...tc, name: IMAGE_GEN_TOOL_NAME };
2002
3990
  }
3991
+ const imageProviderFetch = providerFetch(
3992
+ route.provider,
3993
+ options.codexWsRuntimeIdentity,
3994
+ { providerName: route.providerName, modelId: route.modelId },
3995
+ );
2003
3996
  const imgResponse = await runWithImageBridge({
2004
3997
  parsed, adapter,
2005
3998
  incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
2006
3999
  ...(imgPlan ? { plan: imgPlan } : {}),
2007
4000
  ...(vidPlan ? { videoPlan: vidPlan } : {}),
2008
4001
  forwardHeaders: selectedForwardHeaders,
2009
- onAttemptSend: () => noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens),
4002
+ onAttemptSend: (recovery?: AttemptRecoveryKind) =>
4003
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery),
2010
4004
  abortSignal: options.abortSignal,
2011
4005
  maxRounds: imgPlan && vidPlan
2012
4006
  ? clampImageMaxRounds(Math.min(config.images?.maxRounds ?? 3, config.images?.videoMaxRounds ?? 2))
@@ -2015,8 +4009,12 @@ async function handleResponsesInner(
2015
4009
  : clampImageMaxRounds(config.images?.videoMaxRounds ?? 2),
2016
4010
  connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
2017
4011
  stallTimeoutSec: config.stallTimeoutSec,
2018
- fetchImpl: providerFetch(route.provider),
2019
- onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
4012
+ waitForRequestSlot: imageProviderFetch.waitForPacing,
4013
+ fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
4014
+ onRequestBuilt: request => {
4015
+ recordAdapterReasoning(logCtx, request);
4016
+ recordAdapterTier(logCtx, request);
4017
+ },
2020
4018
  ...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
2021
4019
  onUsage: usage => {
2022
4020
  // Cursor may assign _cursorConversationId inside the image loop's first runTurn;
@@ -2040,20 +4038,30 @@ async function handleResponsesInner(
2040
4038
  });
2041
4039
  if (!rotated) return null;
2042
4040
  route.provider = rotated;
2043
- return resolveAdapter(
4041
+ const rotatedAdapter = resolveAdapter(
2044
4042
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2045
4043
  config.cacheRetention,
2046
4044
  );
4045
+ bindRouteReasoningReplayScope({
4046
+ parsed,
4047
+ providerName: route.providerName,
4048
+ provider: route.provider,
4049
+ adapterName: rotatedAdapter.name,
4050
+ });
4051
+ return rotatedAdapter;
2047
4052
  },
4053
+ retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
2048
4054
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2049
4055
  ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
2050
- onCompletedResponse: (response, providerState) =>
4056
+ onCompletedResponse: (response, providerState) => {
4057
+ commitReasoningReplayServingRoute();
2051
4058
  rememberResponseState(
2052
4059
  parsed._rawBody,
2053
4060
  response,
2054
4061
  continuationStateForResponse(providerState),
2055
- adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2056
- ),
4062
+ responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
4063
+ );
4064
+ },
2057
4065
  });
2058
4066
  if (imgResponse.body) {
2059
4067
  const imgTurnAc = new AbortController();
@@ -2073,13 +4081,17 @@ async function handleResponsesInner(
2073
4081
  // through web-search instead of being swallowed. runTurn adapters never enter this branch.
2074
4082
  if (canRunWebSearch && wsPlan) {
2075
4083
  parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
2076
- noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
2077
4084
  const wsResponse = await runWithWebSearch({
2078
4085
  parsed, adapter,
2079
4086
  incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
2080
4087
  backend: wsPlan.backend,
2081
4088
  forwardProvider: wsPlan.forwardSidecar?.provider,
2082
4089
  anthropicSidecar: wsPlan.anthropicSidecar,
4090
+ xaiSidecar: wsPlan.xaiSidecar,
4091
+ geminiSidecar: wsPlan.geminiSidecar,
4092
+ xaiSearchOptions: wsPlan.xaiSearchOptions,
4093
+ // The exa key never rides the plan: read it from config at unpack time (L9).
4094
+ ...(wsPlan.exaConfigured ? { exaApiKey: config.webSearchSidecar?.exaApiKey } : {}),
2083
4095
  hostedTool: wsPlan.hostedTool,
2084
4096
  selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? selectedForwardHeaders,
2085
4097
  settings: wsPlan.settings,
@@ -2087,7 +4099,12 @@ async function handleResponsesInner(
2087
4099
  forceEmptyResponseId: true,
2088
4100
  abortSignal: options.abortSignal,
2089
4101
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2090
- onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
4102
+ onRequestBuilt: request => {
4103
+ recordAdapterReasoning(logCtx, request);
4104
+ recordAdapterTier(logCtx, request);
4105
+ },
4106
+ onAttemptSend: (recovery?: AttemptRecoveryKind) =>
4107
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery),
2091
4108
  onUsage: usage => {
2092
4109
  logCtx.usageFromBridge = true;
2093
4110
  if (usage) {
@@ -2099,6 +4116,7 @@ async function handleResponsesInner(
2099
4116
  connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
2100
4117
  routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
2101
4118
  stallTimeoutSec: wsPlan.stallTimeoutSec,
4119
+ streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
2102
4120
  on429: retryAfter => {
2103
4121
  const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
2104
4122
  retryAfter,
@@ -2108,11 +4126,20 @@ async function handleResponsesInner(
2108
4126
  });
2109
4127
  if (!rotated) return null;
2110
4128
  route.provider = rotated;
2111
- return resolveAdapter(
4129
+ const rotatedAdapter = resolveAdapter(
2112
4130
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2113
4131
  config.cacheRetention,
2114
4132
  );
4133
+ bindRouteReasoningReplayScope({
4134
+ parsed,
4135
+ providerName: route.providerName,
4136
+ provider: route.provider,
4137
+ adapterName: rotatedAdapter.name,
4138
+ });
4139
+ return rotatedAdapter;
2115
4140
  },
4141
+ retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
4142
+ onCompletedResponse: commitReasoningReplayServingRoute,
2116
4143
  });
2117
4144
  // Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
2118
4145
  // in-flight web-search turns instead of skipping them during graceful shutdown.
@@ -2126,36 +4153,107 @@ async function handleResponsesInner(
2126
4153
  return wsResponse;
2127
4154
  }
2128
4155
 
4156
+ // Empty-completion guard (codex-router PR #145 port): a 200 that completes with no output
4157
+ // text and no tool call is a failure the client cannot see — it silently records the turn as
4158
+ // done. The guard holds pre-content adapter events, suppresses the terminal of an empty
4159
+ // turn, retries the IDENTICAL request once, and surfaces a stated error when the retry is
4160
+ // empty or fails. This is a top-level config opt-in; OCX_EMPTY_COMPLETION_RETRY=0 is a
4161
+ // disable-only emergency override. Compaction turns and combo attempts keep their own
4162
+ // machinery (the combo preflight already handles empty streams). Native Chat-to-Chat
4163
+ // requests return from handleChatCompletions before entering Responses core, so they are
4164
+ // intentionally outside this guard and retain their existing one-send wire behavior.
4165
+ const emptyCompletionGuardEnabled =
4166
+ emptyCompletionRetryEnabled(config)
4167
+ && !options.comboAttempt
4168
+ && !routedCompaction;
4169
+
2129
4170
  if (adapter.runTurn) {
2130
4171
  const runTurnAbort = new AbortController();
2131
- linkAbortSignal(runTurnAbort, options.abortSignal);
4172
+ const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
2132
4173
  const queue = createAdapterEventQueue({
2133
4174
  onBacklogExceeded: () => runTurnAbort.abort(),
2134
4175
  });
2135
- const runTurn = async (): Promise<void> => {
4176
+ // Initial admission must settle before the streaming Response commits HTTP 200.
4177
+ // Let the outer Responses facade preserve the local retryable-429 contract.
4178
+ try {
4179
+ await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
4180
+ } catch (error) {
4181
+ cleanupRunTurnAbort();
4182
+ queue.close();
4183
+ throw error;
4184
+ }
4185
+ // One attempt of the runTurn transport, against an explicit queue. The
4186
+ // empty-completion guard re-invokes the IDENTICAL turn (same parsed request,
4187
+ // same forwarded headers, same abort signal) through a fresh queue, so the
4188
+ // attempt body must not capture the first queue. Each attempt consumes its
4189
+ // own provider pacing slot (#1584): retries are paced like first attempts.
4190
+ const runTurnAttempt = async (
4191
+ targetQueue: AdapterEventQueue,
4192
+ recovery?: AttemptRecoveryKind,
4193
+ pacingSlotAcquired = false,
4194
+ ): Promise<void> => {
2136
4195
  try {
2137
- noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
4196
+ if (!pacingSlotAcquired) {
4197
+ await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
4198
+ }
4199
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery);
4200
+ const runTurnProviderFetch = providerFetch(
4201
+ route.provider,
4202
+ options.codexWsRuntimeIdentity,
4203
+ {
4204
+ providerName: route.providerName,
4205
+ modelId: route.modelId,
4206
+ // runTurnAttempt acquired this logical turn's first physical-request slot above.
4207
+ // Cursor HTTP/1.1 consumes it for RunSSE; every BidiAppend and redial then waits on
4208
+ // the same provider queue through this stateful wrapper.
4209
+ pacingSlotAcquired: true,
4210
+ },
4211
+ );
2138
4212
  await adapter.runTurn?.(
2139
4213
  parsed,
2140
- { headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal, translatorBudget },
2141
- queue.push,
4214
+ {
4215
+ headers: selectedForwardHeaders,
4216
+ abortSignal: runTurnAbort.signal,
4217
+ translatorBudget,
4218
+ providerFetch: runTurnProviderFetch,
4219
+ },
4220
+ targetQueue.push,
2142
4221
  );
2143
4222
  } catch (err) {
2144
- queue.push({
2145
- type: "error",
2146
- message: err instanceof Error ? err.message : String(err),
2147
- });
4223
+ targetQueue.push(err instanceof RequestPacingQueueOverloadError
4224
+ ? {
4225
+ type: "error",
4226
+ status: 429,
4227
+ errorType: "rate_limit_error",
4228
+ retryable: true,
4229
+ message: err.message,
4230
+ }
4231
+ : {
4232
+ type: "error",
4233
+ message: err instanceof Error ? err.message : String(err),
4234
+ });
2148
4235
  } finally {
2149
4236
  // Cursor assigns a stable conversation id inside runTurn on the first headerless
2150
4237
  // turn; backfill so Logs can filter/total that opening request (#330 / #522).
2151
4238
  if (!logCtx.conversationId && parsed._cursorConversationId) {
2152
4239
  logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
2153
4240
  }
2154
- queue.close();
4241
+ targetQueue.close();
2155
4242
  }
2156
4243
  };
4244
+ const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
4245
+ // The empty-completion retry re-runs the turn against a fresh queue: the
4246
+ // first queue is closed once its attempt settles, and pushing into it after
4247
+ // close is a silent no-op.
4248
+ const runTurnRetrySource = (): AsyncIterable<AdapterEvent> => {
4249
+ const retryQueue = createAdapterEventQueue({
4250
+ onBacklogExceeded: () => runTurnAbort.abort(),
4251
+ });
4252
+ void runTurnAttempt(retryQueue, "empty-completion");
4253
+ return retryQueue.stream();
4254
+ };
2157
4255
 
2158
- const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
4256
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
2159
4257
  if (parsed.stream) {
2160
4258
  void runTurn();
2161
4259
  let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
@@ -2169,19 +4267,33 @@ async function handleResponsesInner(
2169
4267
  }
2170
4268
  eventSource = preflight.stream;
2171
4269
  }
4270
+ const guardedSource = emptyCompletionGuardEnabled
4271
+ ? guardEmptyCompletionEventStream({
4272
+ firstEvents: eventSource,
4273
+ // Identical-turn retry: same parsed request, same headers, same
4274
+ // signal — run the adapter transport again against a fresh queue.
4275
+ continuation: runTurnRetrySource,
4276
+ })
4277
+ : eventSource;
2172
4278
  const sseStream = bridgeToResponsesSSE(
2173
- eventSource, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
4279
+ guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
2174
4280
  () => {
2175
4281
  runTurnAbort.abort();
2176
4282
  queue.close();
2177
4283
  }, 2_000,
2178
4284
  {
2179
4285
  translatorBudget,
4286
+ replayCacheScope: parsed._reasoningReplayScope,
2180
4287
  ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
2181
4288
  stallTimeoutSec: config.stallTimeoutSec,
2182
4289
  hideThinkingSummary: parsed.options.hideThinkingSummary,
4290
+ declaredToolNames,
4291
+ toolParameterSchemas,
2183
4292
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2184
4293
  ...(routedCompaction ? { compaction: true } : {}),
4294
+ // grok-build's strict decoder dies on the typed response.heartbeat frame; its
4295
+ // eventsource layer tolerates comment keep-alives. Codex needs the opposite.
4296
+ ...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
2185
4297
  onUsage: usage => {
2186
4298
  // Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
2187
4299
  // zero-default detail objects, so provenance must come from here (cache_detail_missing).
@@ -2191,15 +4303,17 @@ async function handleResponsesInner(
2191
4303
  if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2192
4304
  }
2193
4305
  },
2194
- ...(routedCompaction ? {} : {
2195
- onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
4306
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
4307
+ commitReasoningReplayServingRoute();
4308
+ if (!routedCompaction) {
2196
4309
  rememberResponseState(
2197
4310
  parsed._rawBody,
2198
4311
  response,
2199
4312
  continuationStateForResponse(providerState),
2200
- adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2201
- ),
2202
- }),
4313
+ responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
4314
+ );
4315
+ }
4316
+ },
2203
4317
  },
2204
4318
  );
2205
4319
  const bridgeTurnAc = new AbortController();
@@ -2210,7 +4324,17 @@ async function handleResponsesInner(
2210
4324
  }
2211
4325
 
2212
4326
  await runTurn();
2213
- const events = await queue.collect();
4327
+ const firstAttemptEvents = await queue.collect();
4328
+ let events: AdapterEvent[];
4329
+ if (emptyCompletionGuardEnabled) {
4330
+ events = [];
4331
+ for await (const event of guardEmptyCompletionEventStream({
4332
+ firstEvents: (async function* () { yield* firstAttemptEvents; })(),
4333
+ continuation: runTurnRetrySource,
4334
+ })) events.push(event);
4335
+ } else {
4336
+ events = firstAttemptEvents;
4337
+ }
2214
4338
  if (options.comboAttempt) {
2215
4339
  const firstMeaningful = events.find(event => event.type !== "heartbeat");
2216
4340
  if (!firstMeaningful || firstMeaningful.type === "error") {
@@ -2221,10 +4345,13 @@ async function handleResponsesInner(
2221
4345
  }
2222
4346
  }
2223
4347
  let providerState: OcxProviderContinuationState | undefined;
2224
- const json = buildResponseJSON(events, parsed.modelId, {
4348
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
2225
4349
  translatorBudget,
4350
+ replayCacheScope: parsed._reasoningReplayScope,
2226
4351
  hideThinkingSummary: parsed.options.hideThinkingSummary,
2227
4352
  toolNsMap,
4353
+ declaredToolNames,
4354
+ toolParameterSchemas,
2228
4355
  freeformToolNames,
2229
4356
  toolSearchToolNames,
2230
4357
  ...(routedCompaction ? { compaction: true } : {}),
@@ -2242,43 +4369,105 @@ async function handleResponsesInner(
2242
4369
  parsed._rawBody,
2243
4370
  json,
2244
4371
  continuationStateForResponse(providerState),
2245
- adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
4372
+ responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
2246
4373
  );
2247
4374
  }
4375
+ // #1926 gap 2: the buffered path queued its signature persists inside
4376
+ // buildResponseJSON; bound the durability window before the JSON becomes
4377
+ // externally visible.
4378
+ await awaitThoughtSignatureDurability();
4379
+ if (adapterResponseReachedServingTerminal(events, json)) {
4380
+ commitReasoningReplayServingRoute();
4381
+ }
2248
4382
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
2249
4383
  }
2250
4384
 
2251
4385
  const upstream = new AbortController();
2252
4386
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
2253
4387
  const connectMs = config.connectTimeoutMs ?? 200_000;
4388
+ // Bridge stall budget (seconds of silence before upstream_stall_timeout); the retry backoff
4389
+ // heartbeat interval is derived from it so the watchdog is always fed during deliberate waits.
4390
+ const stallTimeoutMs = typeof config.stallTimeoutSec === "number" && Number.isFinite(config.stallTimeoutSec) && config.stallTimeoutSec > 0
4391
+ ? Math.floor(config.stallTimeoutSec * 1000)
4392
+ : 300_000;
2254
4393
  let activeAdapter = adapter;
2255
4394
 
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;
4395
+ // One immutable, body-safe outbound request per same-target sequence (URL, serialized body,
4396
+ // auth headers, generated compat headers). Same-target 429 replays reuse it verbatim; the
4397
+ // builder runs again only after a key/account/adapter rotation, an oauth refresh, or an
4398
+ // image-tier bias change (transportToken bump). `body` is always a serialized string, so
4399
+ // reuse is safe, and releaseBodyObservation is idempotent per build.
4400
+ let initialRequest: AdapterRequest | undefined;
4401
+ let inputTokenEstimate: number | undefined;
4402
+ try {
4403
+ initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
4404
+ refreshRoutedNamespaceToolAliases(initialRequest);
4405
+ recordAdapterReasoning(logCtx, initialRequest);
4406
+ recordAdapterTier(logCtx, initialRequest);
4407
+ inputTokenEstimate = typeof initialRequest.usageLog?.inputTokens === "number"
4408
+ ? initialRequest.usageLog.inputTokens
4409
+ : undefined;
4410
+ if (inputTokenEstimate !== undefined) logCtx.usageLogInputTokens = inputTokenEstimate;
4411
+ } catch (err) {
4412
+ // A throwing buildRequest never returned a request; if a post-build step threw, release
4413
+ // the serialized-body observation (idempotent) so the translator budget is not leaked.
4414
+ // The build runs after linkAbortSignal, so a failure must also tear the link down and
4415
+ // abort the upstream controller instead of escaping handleResponses unmapped.
4416
+ initialRequest?.releaseBodyObservation?.();
4417
+ cleanupUpstreamAbort();
4418
+ upstream.abort();
4419
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
4420
+ const msg = err instanceof Error ? err.message : String(err);
4421
+ return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
4422
+ }
4423
+ // The catch path above always returns, so the request is definitely assigned here.
4424
+ // Capture it in a const so the fetch callbacks read a narrowed, immutable value
4425
+ // (TypeScript drops narrowing for a `let` captured by a nested function).
4426
+ const builtInitialRequest = initialRequest;
4427
+ let sameTargetRequest: AdapterRequest | undefined = builtInitialRequest;
4428
+ let sameTargetParsed: OcxParsedRequest | undefined = parsed;
4429
+ let sameTargetToken = 0;
4430
+ let transportToken = 0;
4431
+ /**
4432
+ * Invalidate the same-target request cache. Every credential/adapter/parsed mutation MUST
4433
+ * go through here: the cache keys on `parsed` REFERENCE identity, so an in-place mutation
4434
+ * is invisible to it and a missed bump would replay a request built with a stale key.
4435
+ */
4436
+ const invalidateSameTargetRequest = (): void => { transportToken += 1; };
2262
4437
  let upstreamResponse: Response;
2263
4438
  try {
2264
4439
  if (activeAdapter.fetchResponse) {
2265
4440
  noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
2266
- upstreamResponse = await activeAdapter.fetchResponse(request, {
4441
+ await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal);
4442
+ upstreamResponse = await activeAdapter.fetchResponse(builtInitialRequest, {
2267
4443
  abortSignal: upstream.signal,
2268
4444
  timeoutMs: connectMs,
2269
4445
  stream: parsed.stream,
4446
+ executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4447
+ providerName: route.providerName,
4448
+ modelId: route.modelId,
4449
+ }),
2270
4450
  });
2271
4451
  } else {
2272
- upstreamResponse = await fetchWithResetRetry(
4452
+ // #1851 scope guard: transient-5xx retry on this generic adapter path is opt-in for
4453
+ // direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other
4454
+ // adapters keep reset-only retry so combo failover still hops on the first 5xx
4455
+ // instead of burning ~1.2s of same-target retries per hop.
4456
+ const fetchWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry;
4457
+ upstreamResponse = await fetchWithRetryPolicy(
2273
4458
  recovery => {
2274
4459
  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));
4460
+ return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
4461
+ method: builtInitialRequest.method,
4462
+ headers: builtInitialRequest.headers,
4463
+ body: builtInitialRequest.body,
4464
+ }, recovery), upstream.signal, connectMs, parsed.stream,
4465
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4466
+ providerName: route.providerName,
4467
+ modelId: route.modelId,
4468
+ }));
2280
4469
  },
2281
- { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
4470
+ { abortSignal: upstream.signal, label: safeHostLabel(builtInitialRequest.url) },
2282
4471
  );
2283
4472
  }
2284
4473
  } catch (err) {
@@ -2288,41 +4477,92 @@ async function handleResponsesInner(
2288
4477
  const msg = describeUpstreamConnectFailure(err, connectMs);
2289
4478
  return formatErrorResponse(502, "upstream_error", msg);
2290
4479
  } finally {
2291
- request.releaseBodyObservation?.();
4480
+ builtInitialRequest.releaseBodyObservation?.();
2292
4481
  }
2293
4482
 
4483
+ // Same-target 429 retry budget is per REQUEST: it lives OUTSIDE the recovery loop (so a 413/401
4484
+ // replay that comes back 429 cannot silently re-arm a fresh budget) and is SHARED with the
4485
+ // terminal-guard continuation below, so the main loop + one continuation can never exceed
4486
+ // `attempts` same-key replays in total (bounded per request).
4487
+ const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
4488
+ let rateLimitRetries = 0;
4489
+ // Shared with the terminal-guard continuation below: an image-tier reduction that let the
4490
+ // main request clear a 413 must not be forgotten on the very next continuation build.
4491
+ let imageTierBias = 0;
2294
4492
  if (!upstreamResponse.ok) {
2295
- // Recovery loop: multi-key 429 failover + at most ONE anthropic 413 tightened retry
4493
+ // Recovery loop: multi-key 429 failover + at most ONE opaque-state rebuild and ONE
4494
+ // anthropic 413 tightened retry
2296
4495
  // (devlog/260714_image_normalization_pipeline/030). One mutable activeAdapter serves
2297
4496
  // both paths so a 429→413 sequence never rebuilds against a stale pre-rotation
2298
4497
  // adapter, and imageTierBias — once armed — rides EVERY subsequent rebuild so a
2299
4498
  // 413→429 rotation cannot silently undo the tightening.
2300
- let imageTierBias = 0;
2301
4499
  let imageRetryAttempted = false;
4500
+ const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
2302
4501
  let oauth401ReplayAttempted = false;
4502
+ /**
4503
+ * Rebuild the request from the current parsed input (and any image-tier bias) and refetch
4504
+ * it once, tagging the attempt with the given recovery kind. Rebuilds are deterministic
4505
+ * for the same parsed request, so same-target replays stay byte-identical.
4506
+ */
2303
4507
  const rebuildAndRefetch = async (
2304
4508
  recovery: AttemptRecoveryKind,
2305
4509
  ): 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);
4510
+ let retryRequest: AdapterRequest;
4511
+ if (sameTargetRequest !== undefined && sameTargetParsed === parsed && sameTargetToken === transportToken) {
4512
+ // Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
4513
+ retryRequest = sameTargetRequest;
4514
+ } else {
4515
+ try {
4516
+ retryRequest = await activeAdapter.buildRequest(parsed, {
4517
+ headers: selectedForwardHeaders,
4518
+ translatorBudget,
4519
+ ...(imageTierBias > 0 ? { imageTierBias } : {}),
4520
+ });
4521
+ recordAdapterReasoning(logCtx, retryRequest);
4522
+ recordAdapterTier(logCtx, retryRequest);
4523
+ } catch (err) {
4524
+ // A rotated/rebuilt adapter build failure is a request-shaping error, not an
4525
+ // upstream connect failure: tear the abort link down and map it as 400 (no 413
4526
+ // translator-budget mapping here — that stays with parseRequest/buildToolBridgeMaps).
4527
+ cleanupUpstreamAbort();
4528
+ upstream.abort();
4529
+ if (options.abortSignal?.aborted) return { failed: clientCancelledResponse() };
4530
+ const msg = err instanceof Error ? err.message : String(err);
4531
+ return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(msg)) };
4532
+ }
4533
+ sameTargetRequest = retryRequest;
4534
+ sameTargetParsed = parsed;
4535
+ sameTargetToken = transportToken;
4536
+ }
4537
+ refreshRoutedNamespaceToolAliases(retryRequest);
2312
4538
  const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
2313
4539
  ? retryRequest.usageLog.inputTokens
2314
4540
  : undefined;
2315
4541
  if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
2316
4542
  logCtx.providerAdapter = activeAdapter.name;
2317
- sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
4543
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
2318
4544
  noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
2319
4545
  try {
2320
4546
  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));
4547
+ if (activeAdapter.fetchResponse) {
4548
+ await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal);
4549
+ return await activeAdapter.fetchResponse(retryRequest, {
4550
+ abortSignal: upstream.signal,
4551
+ timeoutMs: connectMs,
4552
+ stream: parsed.stream,
4553
+ executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4554
+ providerName: route.providerName,
4555
+ modelId: route.modelId,
4556
+ }),
4557
+ });
4558
+ }
4559
+ return await fetchWithHeaderTimeout(retryRequest.url, {
4560
+ method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
4561
+ }, upstream.signal, connectMs, parsed.stream,
4562
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4563
+ providerName: route.providerName,
4564
+ modelId: route.modelId,
4565
+ }));
2326
4566
  } finally {
2327
4567
  retryRequest.releaseBodyObservation?.();
2328
4568
  }
@@ -2336,6 +4576,7 @@ async function handleResponsesInner(
2336
4576
  return { failed: formatErrorResponse(502, "upstream_error", msg) };
2337
4577
  }
2338
4578
  };
4579
+ // Keep recovery kinds in sync with the native Responses `passthroughRecovery:` loop above.
2339
4580
  recovery: for (;;) {
2340
4581
  if (
2341
4582
  upstreamResponse.status === 401
@@ -2350,9 +4591,13 @@ async function handleResponsesInner(
2350
4591
  refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
2351
4592
  } catch (err) {
2352
4593
  cleanupUpstreamAbort();
2353
- return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
4594
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
2354
4595
  }
2355
4596
  sentOAuthSnapshot = refreshed;
4597
+ replayOAuthCredentialSnapshot = {
4598
+ accountId: refreshed.accountId,
4599
+ generation: refreshed.generation,
4600
+ };
2356
4601
  if (route.providerName === "kiro") {
2357
4602
  parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
2358
4603
  }
@@ -2363,16 +4608,63 @@ async function handleResponsesInner(
2363
4608
  route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
2364
4609
  );
2365
4610
  route.provider = refreshedProvider;
4611
+ invalidateSameTargetRequest();
2366
4612
  activeAdapter = resolveAdapter(
2367
4613
  resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
2368
4614
  config.cacheRetention,
2369
4615
  );
4616
+ bindRouteReasoningReplayScope({
4617
+ parsed,
4618
+ providerName: route.providerName,
4619
+ provider: refreshedProvider,
4620
+ adapterName: activeAdapter.name,
4621
+ oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
4622
+ });
2370
4623
  const result = await rebuildAndRefetch("oauth-401");
2371
4624
  if ("failed" in result) return result.failed;
2372
4625
  upstreamResponse = result;
2373
4626
  continue recovery;
2374
4627
  }
2375
4628
 
4629
+ // Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
4630
+ // 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
4631
+ // so wait (Retry-After or the fixed interval) and replay the IDENTICAL request on the
4632
+ // same key first. Pre-stream only: a 429 arrives before any bytes are relayed, so the
4633
+ // replay is lossless. Runs before key failover so "primary-first" setups keep the same
4634
+ // key on rate-limit blips; only after the attempts are exhausted does failover run.
4635
+ while (
4636
+ upstreamResponse.status === 429
4637
+ && rateLimitPolicy !== null
4638
+ && rateLimitRetries < rateLimitPolicy.attempts
4639
+ ) {
4640
+ rateLimitRetries += 1;
4641
+ // Release unread body + deliberate wait via the shared same-target helper.
4642
+ const retryAfterHeader = upstreamResponse.headers.get("retry-after");
4643
+ try {
4644
+ for await (const _ of prepareSameTarget429Wait({
4645
+ body: upstreamResponse.body,
4646
+ signal: options.abortSignal,
4647
+ delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
4648
+ })) {
4649
+ // pre-stream: no stall watchdog to feed
4650
+ }
4651
+ } catch {
4652
+ cleanupUpstreamAbort();
4653
+ upstream.abort();
4654
+ return clientCancelledResponse();
4655
+ }
4656
+ // Client cancellation wins over any stale timer edge: re-check before dispatching the
4657
+ // replay so an adapter never starts work for a request the client already abandoned.
4658
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
4659
+ cleanupUpstreamAbort();
4660
+ upstream.abort();
4661
+ return clientCancelledResponse();
4662
+ }
4663
+ const result = await rebuildAndRefetch("rate-limit-429");
4664
+ if ("failed" in result) return result.failed;
4665
+ upstreamResponse = result;
4666
+ }
4667
+
2376
4668
  // Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
2377
4669
  // SAME request once per remaining key. OAuth/forward providers and single-key pools
2378
4670
  // return null immediately, so this stays a no-op for them (src/providers/key-failover.ts).
@@ -2388,10 +4680,17 @@ async function handleResponsesInner(
2388
4680
  // until runtime cleanup (one per rotated key under a rate-limit storm).
2389
4681
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
2390
4682
  route.provider = rotated;
4683
+ invalidateSameTargetRequest();
2391
4684
  activeAdapter = resolveAdapter(
2392
4685
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2393
4686
  config.cacheRetention,
2394
4687
  );
4688
+ bindRouteReasoningReplayScope({
4689
+ parsed,
4690
+ providerName: route.providerName,
4691
+ provider: route.provider,
4692
+ adapterName: activeAdapter.name,
4693
+ });
2395
4694
  const result = await rebuildAndRefetch("key-429");
2396
4695
  if ("failed" in result) return result.failed;
2397
4696
  upstreamResponse = result;
@@ -2418,13 +4717,14 @@ async function handleResponsesInner(
2418
4717
  anthropicPoolAccountId = nextAccountId;
2419
4718
  anthropicPoolFailovers += 1;
2420
4719
  route.provider = { ...route.provider, apiKey: accessToken };
4720
+ invalidateSameTargetRequest();
2421
4721
  promoteAnthropicActiveAccount(nextAccountId);
2422
4722
  logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
2423
4723
  activeAdapter = resolveAdapter(
2424
4724
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2425
4725
  config.cacheRetention,
2426
4726
  );
2427
- sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
4727
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
2428
4728
  const result = await rebuildAndRefetch("anthropic-oauth-429");
2429
4729
  if ("failed" in result) return result.failed;
2430
4730
  upstreamResponse = result;
@@ -2432,6 +4732,27 @@ async function handleResponsesInner(
2432
4732
  break;
2433
4733
  }
2434
4734
  }
4735
+ // Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
4736
+ // or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
4737
+ // the missing authoritative signal. Rebuild once through the same sanitation path used by a
4738
+ // known route switch; invalidating is mandatory because `parsed` mutates in place and the
4739
+ // same-target cache would otherwise replay the rejected bytes verbatim.
4740
+ const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
4741
+ response: upstreamResponse,
4742
+ outboundBody: sameTargetRequest?.body,
4743
+ adapterName: activeAdapter.name,
4744
+ parsed,
4745
+ guard: opaqueBlobRecoveryGuard,
4746
+ signal: upstream.signal,
4747
+ }, recovery => {
4748
+ invalidateSameTargetRequest();
4749
+ return rebuildAndRefetch(recovery);
4750
+ });
4751
+ if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
4752
+ if (opaqueBlobRecovery.kind === "recovered") {
4753
+ upstreamResponse = opaqueBlobRecovery.response;
4754
+ continue recovery;
4755
+ }
2435
4756
  // Anthropic 413 request_too_large: rebuild once with every image one tier lower
2436
4757
  // (spiral guard: single attempt). The biased response re-enters the 429 check above.
2437
4758
  if (shouldAttemptImageTierRetry({
@@ -2442,6 +4763,7 @@ async function handleResponsesInner(
2442
4763
  })) {
2443
4764
  imageRetryAttempted = true;
2444
4765
  imageTierBias = 1;
4766
+ invalidateSameTargetRequest();
2445
4767
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
2446
4768
  const result = await rebuildAndRefetch("image-413");
2447
4769
  if ("failed" in result) return result.failed;
@@ -2452,29 +4774,56 @@ async function handleResponsesInner(
2452
4774
  }
2453
4775
  if (!upstreamResponse.ok) {
2454
4776
  if (options.comboAttempt) {
4777
+ // No pre-read guard: `consumeComboFailure` -> `readBoundedResponseBody` reads
4778
+ // `response.body` itself with the abort signal threaded through, and the combo
4779
+ // contract is that this body's getter is touched exactly once. A guard here would be
4780
+ // a second `.body` access for no gain, since the bounded reader owns settlement.
2455
4781
  const failure = await consumeComboFailure(upstreamResponse, options.abortSignal)
2456
4782
  .finally(cleanupUpstreamAbort);
2457
4783
  options.onConsumedComboFailure?.(failure);
2458
4784
  return failure.response;
2459
4785
  }
4786
+ // The plain error path has no bounded reader of its own: `.text()` attaches the reader
4787
+ // only when it runs, so an abort landing between fetch resolution and that call orphans
4788
+ // Bun's internal read rejection — the uncatchable teardown `cancelBodyOnAbort` absorbs
4789
+ // (src/lib/abort.ts). Found while investigating #1419; not a fix for the native trap.
4790
+ const detachErrorBodyGuard = cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
2460
4791
  const errorText = await upstreamResponse.text().catch(() => "unknown error");
4792
+ detachErrorBodyGuard();
2461
4793
  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
- );
4794
+ if (!isFixedCodexAccount(authCtx)) {
4795
+ recordSubagentQuotaFailureForThreadSpawn(
4796
+ req.headers,
4797
+ subagentQuotaFailureModel,
4798
+ upstreamResponse.status === 429 || upstreamResponse.status === 402
4799
+ ? upstreamResponse.status
4800
+ : `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
4801
+ config,
4802
+ subagentFallbackAccountId,
4803
+ );
4804
+ }
2471
4805
  // Upstreams occasionally echo request details in error bodies — scrub token-shaped
2472
4806
  // material before it reaches the client-facing error surface.
2473
- const message = `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`;
4807
+ const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
4808
+ const message = enrichOpenCodeZenRateLimitMessage(
4809
+ `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
4810
+ {
4811
+ status: upstreamResponse.status,
4812
+ providerName: route.providerName,
4813
+ baseUrl: route.provider.baseUrl,
4814
+ adapter: route.provider.adapter,
4815
+ authMode: route.provider.authMode,
4816
+ hasApiKey: Boolean(route.provider.apiKey?.trim()),
4817
+ upstreamRetryAfter,
4818
+ // This recovery path is the HTTP Responses wire; custom runTurn transports
4819
+ // never reach enrichOpenCodeZenRateLimitMessage here.
4820
+ supportsHttpSameKeyRetry: true,
4821
+ },
4822
+ );
2474
4823
  const retryAfter = resolveClientRetryAfter({
2475
4824
  status: upstreamResponse.status,
2476
4825
  message,
2477
- upstreamRetryAfter: upstreamResponse.headers.get("retry-after"),
4826
+ upstreamRetryAfter,
2478
4827
  });
2479
4828
  return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
2480
4829
  ...(retryAfter !== undefined ? { retryAfter } : {}),
@@ -2484,64 +4833,169 @@ async function handleResponsesInner(
2484
4833
 
2485
4834
  cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
2486
4835
 
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;
4836
+ // One bounded internal continuation re-ask for clean end_turn turns that announced an edit
4837
+ // without emitting a tool call. Anthropic gets this by default; openai-chat providers opt in
4838
+ // per-provider via `terminalContinuationGuard` (the heuristic was tuned on Anthropic turns,
4839
+ // so it stays off for the shared openai-chat adapter unless a provider enables it).
4840
+ const terminalGuardEnabled = (activeAdapter.name === "anthropic"
4841
+ || (activeAdapter.name === "openai-chat" && route.provider.terminalContinuationGuard === true))
4842
+ && !options.comboAttempt && !routedCompaction;
4843
+ /**
4844
+ * One bounded internal re-ask for Anthropic end_turn-without-tool-call turns. Replays the
4845
+ * continuation on a 429 with the same-key retry budget (hoisted per request), then falls
4846
+ * back to key/account failover; a failure becomes an in-stream adapter error so the client
4847
+ * never sees a second hidden HTTP response or an unbounded retry loop.
4848
+ */
4849
+ const fetchTerminalGuardContinuation = async function* (
4850
+ nextParsed: OcxParsedRequest,
4851
+ initialRecoveryKind?: AttemptRecoveryKind,
4852
+ ): AsyncGenerator<AdapterEvent> {
2494
4853
  let response: Response | undefined;
4854
+ // One-shot recovery label for the next top-of-loop continuation send after a failover rotation.
4855
+ let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined = initialRecoveryKind;
4856
+ /**
4857
+ * Build and fetch one terminal-guard continuation. `recoveryKind` tags same-target and
4858
+ * failover sends (`empty-completion`, `rate-limit-429`, `key-429`,
4859
+ * `anthropic-oauth-429`, `image-413`); the
4860
+ * adapter rebuild is deterministic for the same parsed request (tests assert byte-identical
4861
+ * replays).
4862
+ */
4863
+ const fetchContinuation = async (recoveryKind?: AttemptRecoveryKind): Promise<Response> => {
4864
+ let continuationRequest: AdapterRequest | undefined;
4865
+ if (sameTargetRequest !== undefined && sameTargetParsed === nextParsed && sameTargetToken === transportToken) {
4866
+ // Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
4867
+ continuationRequest = sameTargetRequest;
4868
+ } else {
4869
+ try {
4870
+ continuationRequest = await activeAdapter.buildRequest(nextParsed, {
4871
+ headers: selectedForwardHeaders,
4872
+ translatorBudget,
4873
+ ...(imageTierBias > 0 ? { imageTierBias } : {}),
4874
+ });
4875
+ recordAdapterReasoning(logCtx, continuationRequest);
4876
+ recordAdapterTier(logCtx, continuationRequest);
4877
+ } catch (err) {
4878
+ // The main body is already streaming, so there is no HTTP error surface: release
4879
+ // any partial body observation and surface the failure as an in-stream error via
4880
+ // the outer catch (no upstream.abort() — that would kill the live body stream).
4881
+ continuationRequest?.releaseBodyObservation?.();
4882
+ throw err;
4883
+ }
4884
+ sameTargetRequest = continuationRequest;
4885
+ sameTargetParsed = nextParsed;
4886
+ sameTargetToken = transportToken;
4887
+ }
4888
+ // Both branches assign the request (the build catch rethrows), so capture it in a
4889
+ // const for the fetch callback and finally below — a `let` read inside a nested
4890
+ // function keeps its undefined half, which would break the byte-identical replay.
4891
+ const builtContinuationRequest = continuationRequest;
4892
+ const continuationEstimate = typeof builtContinuationRequest.usageLog?.inputTokens === "number"
4893
+ ? builtContinuationRequest.usageLog.inputTokens
4894
+ : undefined;
4895
+ if (continuationEstimate !== undefined) logCtx.usageLogInputTokens = continuationEstimate;
4896
+ // Optional recovery label for same-target / failover continuation sends.
4897
+ const replayKind: AttemptRecoveryKind | undefined = recoveryKind;
4898
+ try {
4899
+ if (activeAdapter.fetchResponse) {
4900
+ noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replayKind);
4901
+ await waitForProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal);
4902
+ return await activeAdapter.fetchResponse(builtContinuationRequest, {
4903
+ abortSignal: upstream.signal,
4904
+ timeoutMs: connectMs,
4905
+ stream: nextParsed.stream,
4906
+ executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4907
+ providerName: route.providerName,
4908
+ modelId: nextParsed.modelId,
4909
+ }),
4910
+ });
4911
+ }
4912
+ // Same #1851 scope guard as the initial send: transient-5xx retry only for direct
4913
+ // Google AI Studio; every other adapter keeps reset-only semantics here.
4914
+ const fetchContinuationWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry;
4915
+ return await fetchContinuationWithRetryPolicy(
4916
+ recovery => {
4917
+ noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind);
4918
+ return fetchWithHeaderTimeout(
4919
+ builtContinuationRequest.url,
4920
+ applyUpstreamRecoveryInit({
4921
+ method: builtContinuationRequest.method,
4922
+ headers: builtContinuationRequest.headers,
4923
+ body: builtContinuationRequest.body,
4924
+ }, recovery),
4925
+ upstream.signal,
4926
+ connectMs,
4927
+ nextParsed.stream,
4928
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4929
+ providerName: route.providerName,
4930
+ modelId: nextParsed.modelId,
4931
+ }),
4932
+ );
4933
+ },
4934
+ { abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url) },
4935
+ );
4936
+ } finally {
4937
+ builtContinuationRequest.releaseBodyObservation?.();
4938
+ }
4939
+ };
2495
4940
  while (true) {
2496
4941
  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;
4942
+ const recoveryKind = nextContinuationRecoveryKind;
4943
+ nextContinuationRecoveryKind = undefined;
4944
+ response = await fetchContinuation(recoveryKind);
4945
+ } catch (error) {
4946
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
4947
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
4948
+ } else {
4949
+ yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
4950
+ }
4951
+ return;
4952
+ }
4953
+
4954
+ // Same-target 429 wait-and-retry (opt-in `retryOn429`) before key/account failover:
4955
+ // a primary-key rate-limit blip replays on the SAME key, matching the main recovery
4956
+ // loop; only after the attempts are exhausted does the continuation fail over.
4957
+ while (
4958
+ response.status === 429
4959
+ && rateLimitPolicy !== null
4960
+ && rateLimitRetries < rateLimitPolicy.attempts
4961
+ ) {
4962
+ rateLimitRetries += 1;
4963
+ // Release unread body + heartbeat-fed wait via the shared same-target helper.
4964
+ const retryAfterHeader = response.headers.get("retry-after");
2507
4965
  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
- });
4966
+ yield* prepareSameTarget429Wait({
4967
+ body: response.body,
4968
+ // Listen on the upstream signal: once the SSE body is being streamed, a client
4969
+ // cancel aborts `upstream` through the bridge, and upstream is also linked from
4970
+ // options.abortSignal — so this covers both cancellation paths.
4971
+ signal: upstream.signal,
4972
+ delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
4973
+ heartbeatIntervalMs: Math.min(10_000, Math.max(250, stallTimeoutMs / 2)),
4974
+ });
4975
+ } catch {
4976
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
4977
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
2515
4978
  } 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
- );
4979
+ yield { type: "error", message: "Provider continuation failed: retry wait interrupted" };
2534
4980
  }
2535
- } finally {
2536
- continuationRequest.releaseBodyObservation?.();
4981
+ return;
2537
4982
  }
2538
- } catch (error) {
2539
- if (options.abortSignal?.aborted) {
4983
+ // Client cancellation wins over any stale timer edge: re-check before dispatching the
4984
+ // replay so the continuation never starts work for a request the client abandoned.
4985
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
2540
4986
  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)}` };
4987
+ return;
4988
+ }
4989
+ try {
4990
+ response = await fetchContinuation("rate-limit-429");
4991
+ } catch (error) {
4992
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
4993
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
4994
+ } else {
4995
+ yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
4996
+ }
4997
+ return;
2543
4998
  }
2544
- return;
2545
4999
  }
2546
5000
 
2547
5001
  if (response.status === 429 && hasKeyPoolFailover(route.provider)) {
@@ -2554,10 +5008,26 @@ async function handleResponsesInner(
2554
5008
  if (rotated) {
2555
5009
  try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
2556
5010
  route.provider = rotated;
5011
+ invalidateSameTargetRequest();
2557
5012
  activeAdapter = resolveAdapter(
2558
5013
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2559
5014
  config.cacheRetention,
2560
5015
  );
5016
+ bindRouteReasoningReplayScope({
5017
+ parsed: nextParsed,
5018
+ providerName: route.providerName,
5019
+ provider: route.provider,
5020
+ adapterName: activeAdapter.name,
5021
+ });
5022
+ // Response persistence closes over the outer parsed request; keep its owner binding in
5023
+ // sync with the terminal-guard clone that builds the rotated continuation request.
5024
+ bindRouteReasoningReplayScope({
5025
+ parsed,
5026
+ providerName: route.providerName,
5027
+ provider: route.provider,
5028
+ adapterName: activeAdapter.name,
5029
+ });
5030
+ nextContinuationRecoveryKind = "key-429";
2561
5031
  continue;
2562
5032
  }
2563
5033
  }
@@ -2580,13 +5050,15 @@ async function handleResponsesInner(
2580
5050
  anthropicPoolAccountId = nextAccountId;
2581
5051
  anthropicPoolFailovers += 1;
2582
5052
  route.provider = { ...route.provider, apiKey: accessToken };
5053
+ invalidateSameTargetRequest();
2583
5054
  promoteAnthropicActiveAccount(nextAccountId);
2584
5055
  logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
2585
5056
  activeAdapter = resolveAdapter(
2586
5057
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
2587
5058
  config.cacheRetention,
2588
5059
  );
2589
- sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
5060
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5061
+ nextContinuationRecoveryKind = "anthropic-oauth-429";
2590
5062
  continue;
2591
5063
  } catch {
2592
5064
  // fall through to emit continuation error below
@@ -2600,14 +5072,21 @@ async function handleResponsesInner(
2600
5072
  alreadyAttempted: imageTierBias > 0,
2601
5073
  })) {
2602
5074
  imageTierBias = 1;
5075
+ invalidateSameTargetRequest();
2603
5076
  try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
5077
+ nextContinuationRecoveryKind = "image-413";
2604
5078
  continue;
2605
5079
  }
2606
5080
  break;
2607
5081
  }
2608
5082
 
2609
5083
  if (!response.ok) {
5084
+ // Same pre-read guard as the initial response's error branch: a non-2xx continuation body
5085
+ // is still a Bun fetch body, and an abort landing before `.text()` attaches its reader
5086
+ // orphans the internal rejection.
5087
+ const detachContinuationErrorGuard = cancelBodyOnAbort(response.body, upstream.signal);
2610
5088
  const errorText = await response.text().catch(() => "unknown error");
5089
+ detachContinuationErrorGuard();
2611
5090
  yield {
2612
5091
  type: "error",
2613
5092
  status: response.status,
@@ -2623,9 +5102,9 @@ async function handleResponsesInner(
2623
5102
  const detachContinuationBodyGuard = cancelBodyOnAbort(response.body, upstream.signal);
2624
5103
  try {
2625
5104
  if (nextParsed.stream) {
2626
- yield* activeAdapter.parseStream(response, translatorBudget);
5105
+ yield* activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata);
2627
5106
  } else if (activeAdapter.parseResponse) {
2628
- yield* await activeAdapter.parseResponse(response, translatorBudget);
5107
+ yield* await activeAdapter.parseResponse(response, translatorBudget, logCtx.activeTierMetadata);
2629
5108
  } else {
2630
5109
  yield { type: "error", message: "Provider continuation does not support response parsing" };
2631
5110
  }
@@ -2641,8 +5120,25 @@ async function handleResponsesInner(
2641
5120
  }
2642
5121
  };
2643
5122
 
5123
+ const fetchGuardedEmptyCompletionRetry = (): AsyncIterable<AdapterEvent> => {
5124
+ const retryEvents = fetchTerminalGuardContinuation(parsed, "empty-completion");
5125
+ return terminalGuardEnabled
5126
+ ? guardTerminalEventStream({
5127
+ parsed,
5128
+ firstEvents: retryEvents,
5129
+ adapterName: activeAdapter.name,
5130
+ maxAutoContinuations: 1,
5131
+ continuation: fetchTerminalGuardContinuation,
5132
+ })
5133
+ : retryEvents;
5134
+ };
5135
+
2644
5136
  if (parsed.stream) {
2645
- const initialEventStream = activeAdapter.parseStream(upstreamResponse, translatorBudget);
5137
+ const initialEventStream = activeAdapter.parseStream(
5138
+ upstreamResponse,
5139
+ translatorBudget,
5140
+ logCtx.activeTierMetadata,
5141
+ );
2646
5142
  const eventStream = terminalGuardEnabled
2647
5143
  ? guardTerminalEventStream({
2648
5144
  parsed,
@@ -2652,17 +5148,32 @@ async function handleResponsesInner(
2652
5148
  continuation: fetchTerminalGuardContinuation,
2653
5149
  })
2654
5150
  : initialEventStream;
2655
- const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
5151
+ // The empty-completion guard sits OUTSIDE the terminal guard: a completed
5152
+ // turn with no text and no tool call is retried with the IDENTICAL request
5153
+ // (fetchTerminalGuardContinuation(parsed) replays the cached byte-identical
5154
+ // request — same body, same headers, same signal).
5155
+ const guardedEventStream = emptyCompletionGuardEnabled
5156
+ ? guardEmptyCompletionEventStream({
5157
+ firstEvents: eventStream,
5158
+ continuation: fetchGuardedEmptyCompletionRetry,
5159
+ })
5160
+ : eventStream;
5161
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
2656
5162
  const sseStream = bridgeToResponsesSSE(
2657
- eventStream, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
5163
+ guardedEventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
2658
5164
  () => upstream.abort(), 2_000,
2659
5165
  {
2660
5166
  translatorBudget,
5167
+ replayCacheScope: parsed._reasoningReplayScope,
2661
5168
  ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
2662
5169
  stallTimeoutSec: config.stallTimeoutSec,
2663
5170
  hideThinkingSummary: parsed.options.hideThinkingSummary,
5171
+ declaredToolNames,
5172
+ toolParameterSchemas,
2664
5173
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2665
5174
  ...(routedCompaction ? { compaction: true } : {}),
5175
+ // Same grok-surface split as the runTurn branch above.
5176
+ ...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
2666
5177
  onUsage: usage => {
2667
5178
  // Raw adapter usage, pre wire-normalization (see the runTurn branch above).
2668
5179
  logCtx.usageFromBridge = true;
@@ -2671,18 +5182,20 @@ async function handleResponsesInner(
2671
5182
  if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2672
5183
  }
2673
5184
  },
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) =>
5185
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
5186
+ commitReasoningReplayServingRoute();
5187
+ // Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
5188
+ // PRE-compaction history, and a later previous_response_id expansion would rehydrate the
5189
+ // giant stale chain Codex just replaced.
5190
+ if (!routedCompaction) {
2679
5191
  rememberResponseState(
2680
5192
  parsed._rawBody,
2681
5193
  response,
2682
5194
  continuationStateForResponse(providerState),
2683
- activeAdapter.name === "kiro" ? { force: true } : undefined,
2684
- ),
2685
- }),
5195
+ responseStateOptions(activeAdapter.name === "kiro"),
5196
+ );
5197
+ }
5198
+ },
2686
5199
  },
2687
5200
  );
2688
5201
  const bridgeTurnAc = new AbortController();
@@ -2695,28 +5208,45 @@ async function handleResponsesInner(
2695
5208
  if (activeAdapter.parseResponse) {
2696
5209
  let events: AdapterEvent[];
2697
5210
  try {
2698
- const initialEvents = await activeAdapter.parseResponse(upstreamResponse, translatorBudget);
5211
+ const initialEvents = await activeAdapter.parseResponse(
5212
+ upstreamResponse,
5213
+ translatorBudget,
5214
+ logCtx.activeTierMetadata,
5215
+ );
5216
+ let guardedEvents: AdapterEvent[];
2699
5217
  if (terminalGuardEnabled) {
2700
- events = [];
5218
+ guardedEvents = [];
2701
5219
  for await (const event of guardTerminalEventStream({
2702
5220
  parsed,
2703
5221
  firstEvents: (async function* () { yield* initialEvents; })(),
2704
5222
  adapterName: activeAdapter.name,
2705
5223
  maxAutoContinuations: 1,
2706
5224
  continuation: fetchTerminalGuardContinuation,
5225
+ })) guardedEvents.push(event);
5226
+ } else {
5227
+ guardedEvents = initialEvents;
5228
+ }
5229
+ if (emptyCompletionGuardEnabled) {
5230
+ events = [];
5231
+ for await (const event of guardEmptyCompletionEventStream({
5232
+ firstEvents: (async function* () { yield* guardedEvents; })(),
5233
+ continuation: fetchGuardedEmptyCompletionRetry,
2707
5234
  })) events.push(event);
2708
5235
  } else {
2709
- events = initialEvents;
5236
+ events = guardedEvents;
2710
5237
  }
2711
5238
  } finally {
2712
5239
  cleanupUpstreamAbort();
2713
5240
  }
2714
- const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
5241
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
2715
5242
  let providerState: OcxProviderContinuationState | undefined;
2716
- const json = buildResponseJSON(events, parsed.modelId, {
5243
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
2717
5244
  translatorBudget,
5245
+ replayCacheScope: parsed._reasoningReplayScope,
2718
5246
  hideThinkingSummary: parsed.options.hideThinkingSummary,
2719
5247
  toolNsMap,
5248
+ declaredToolNames,
5249
+ toolParameterSchemas,
2720
5250
  freeformToolNames,
2721
5251
  toolSearchToolNames,
2722
5252
  ...(routedCompaction ? { compaction: true } : {}),
@@ -2735,13 +5265,24 @@ async function handleResponsesInner(
2735
5265
  parsed._rawBody,
2736
5266
  json,
2737
5267
  continuationStateForResponse(providerState),
2738
- activeAdapter.name === "kiro" ? { force: true } : undefined,
5268
+ responseStateOptions(activeAdapter.name === "kiro"),
2739
5269
  );
2740
5270
  }
5271
+ // #1926 gap 2: same buffered-path durability bound as the primary branch.
5272
+ await awaitThoughtSignatureDurability();
5273
+ if (adapterResponseReachedServingTerminal(events, json)) {
5274
+ commitReasoningReplayServingRoute();
5275
+ }
2741
5276
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
2742
5277
  }
2743
5278
 
2744
5279
  return formatErrorResponse(400, "invalid_request_error", "Non-streaming not supported by this adapter");
5280
+ } finally {
5281
+ if (pendingHostAdmissionLease) {
5282
+ releaseUpstreamHostAdmission(pendingHostAdmissionLease);
5283
+ releaseCodexAuthContextProbeLease(authCtx);
5284
+ }
5285
+ }
2745
5286
  }
2746
5287
 
2747
5288