@bitkyc08/opencodex 2.63.0 → 2.64.0-preview.20260923

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 (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-D5eN1TID.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-Bh_ErQDh.js} +1 -1
  10. package/gui/dist/assets/index-BEq4OCOz.js +86 -0
  11. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  12. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-BqrsSrIR.js +0 -50
  259. package/gui/dist/assets/index-C6SJrh0N.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -41,6 +41,7 @@ import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
41
41
  import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
42
42
  import { slugsEquivalent } from "../../providers/slug-codec";
43
43
  import {
44
+ callerCodexWorkspaceAccountId,
44
45
  codexProbeLeaseId,
45
46
  codexTransientProbeGrant,
46
47
  codexProbeQuotaScope,
@@ -173,8 +174,9 @@ export function normalizeCodexUnsupportedModelDetail(value: string): string {
173
174
  * that comparison fail for the one model that is still account-gated, which silently disabled
174
175
  * both the alternate-account retry and the same-account ladder built for exactly that case.
175
176
  *
176
- * The envelope is unchanged and stays exact: a top-level `detail` string, whitespace-collapsed
177
- * and case-folded, matching the whole sentence with nothing before or after it. No prose is
177
+ * Accept the HTTP `detail` envelope and the `error.message` envelope emitted by the
178
+ * WebSocket refused-create projection. Both must match the whole sentence, whitespace-collapsed
179
+ * and case-folded, with nothing before or after it. Competing envelopes are ambiguous. No prose is
178
180
  * inferred and no other 400 shape is admitted, because a 400 is also what a malformed request
179
181
  * earns and that must never read as an entitlement fact.
180
182
  */
@@ -186,7 +188,18 @@ export function codexUnsupportedModelFromDetail(
186
188
  try {
187
189
  const payload = JSON.parse(bodyText) as unknown;
188
190
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
189
- const detail = (payload as { detail?: unknown }).detail;
191
+ const record = payload as Record<string, unknown>;
192
+ const hasDetail = Object.hasOwn(record, "detail");
193
+ const hasError = Object.hasOwn(record, "error");
194
+ if (hasDetail === hasError) return undefined;
195
+ let detail: unknown = record.detail;
196
+ if (hasError) {
197
+ const error = record.error;
198
+ if (!error || typeof error !== "object" || Array.isArray(error)) return undefined;
199
+ const fields = error as Record<string, unknown>;
200
+ if ([fields.type, fields.code].some(value => value != null && typeof value !== "string")) return undefined;
201
+ detail = fields.message;
202
+ }
190
203
  if (typeof detail !== "string") return undefined;
191
204
  const matched = /^the '([^']{1,256})' model is not supported when using codex with a chatgpt account\.$/u
192
205
  .exec(normalizeCodexUnsupportedModelDetail(detail));
@@ -280,15 +293,11 @@ export async function shouldRetryCodexPoolAccountQuota(
280
293
  // body carries no quota evidence either, but the marker is the contract, not the prose.
281
294
  if (isNonReplayableResponse(response)) return false;
282
295
  if (response.status === 402 || response.status === 429) {
283
- // Status alone used to authorize the move, which is right for a limit the ACCOUNT owns and
284
- // wrong for one it merely belongs to. An organization- or project-scoped exhaustion refuses
285
- // every credential inside that organization, so the second account meets the same counter
286
- // and the only thing the rotation buys is a second cold prompt prefix (#4546). Positive
287
- // evidence is required to withhold it: the helper fails closed, so an unreadable or
288
- // ambiguous body keeps the broad #584 behaviour unchanged, and `rate_limit_exceeded`,
289
- // `slow_down` and plan-level exhaustion still rotate exactly as before.
290
- const { codexScopedExhaustionCode } = await import("../../codex/quota-rejection");
291
- return await codexScopedExhaustionCode(response, { signal }) === undefined;
296
+ // The response does not identify the organization or project whose quota was exhausted.
297
+ // Resolve the alternate before deciding whether its known workspace identity proves that an
298
+ // organization-scoped retry would be futile. Until then, preserve the broad #584 behaviour.
299
+ void signal;
300
+ return true;
292
301
  }
293
302
  if (response.status < 500 || response.status >= 600) return false;
294
303
  try {
@@ -305,6 +314,20 @@ export async function shouldRetryCodexPoolAccountQuota(
305
314
  }
306
315
 
307
316
 
317
+ export async function shouldRetryCodexScopedQuotaOnAlternate(
318
+ response: Response,
319
+ firstWorkspaceAccountId: string,
320
+ alternateWorkspaceAccountId: string | undefined,
321
+ signal?: AbortSignal,
322
+ ): Promise<boolean> {
323
+ if (!firstWorkspaceAccountId || firstWorkspaceAccountId !== alternateWorkspaceAccountId) return true;
324
+ const { codexScopedExhaustionCode } = await import("../../codex/quota-rejection");
325
+ const code = await codexScopedExhaustionCode(response, { signal });
326
+ // Workspace identity binds organization-level limits, but the response supplies no project id.
327
+ return code === undefined || code === "project_spend_limit_exceeded";
328
+ }
329
+
330
+
308
331
  /**
309
332
  * A pre-stream upstream 5xx another Codex account may still be able to serve.
310
333
  *
@@ -448,6 +471,7 @@ export function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest
448
471
  if (logCtx) {
449
472
  logCtx.preserveResolvedModelFromRoute = true;
450
473
  delete logCtx.resolvedModel;
474
+ logCtx.wireModel = wireModel;
451
475
  }
452
476
  parsed.modelId = wireModel;
453
477
  if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
@@ -532,6 +556,22 @@ export async function retryCodexPoolOnAlternateAccount(
532
556
  writerGeneration: firstAuthCtx.writerGeneration,
533
557
  });
534
558
  };
559
+ // A body-confirmed quota response may arrive under HTTP 5xx. A path that returns the
560
+ // first response without a move must still record the NORMALIZED outcome: the ordinary
561
+ // terminal recorder sees only that wire status and would misclassify it as transient,
562
+ // leaving the exhausted account immediately selectable next turn.
563
+ const recordWrappedQuotaOutcome = (): void => {
564
+ if (outcomeStatus === firstResponse.status || (outcomeStatus !== 429 && outcomeStatus !== 402)) return;
565
+ recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
566
+ ...codexQuotaOutcomeMeta(firstResponse),
567
+ threadId: firstAuthCtx.affinityKey,
568
+ modelId: route.modelId,
569
+ probeLeaseId: codexProbeLeaseId(firstAuthCtx),
570
+ probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
571
+ transientProbe: codexTransientProbeGrant(firstAuthCtx),
572
+ writerGeneration: firstAuthCtx.writerGeneration,
573
+ });
574
+ };
535
575
  if (outcomeStatus === 400 && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)) {
536
576
  invalidateCodexModelEntitlementsForAccount(firstAuthCtx.accountId);
537
577
  let refreshed;
@@ -628,26 +668,45 @@ export async function retryCodexPoolOnAlternateAccount(
628
668
  && retryAuthCtx?.kind !== "main-pool"
629
669
  && retryAuthCtx?.kind !== "main"
630
670
  ) {
631
- // A body-confirmed quota response may arrive under HTTP 5xx. Without an alternate,
632
- // the ordinary terminal recorder sees only that wire status and would misclassify it
633
- // as transient, leaving the exhausted account immediately selectable next turn.
634
- if (outcomeStatus !== firstResponse.status && (outcomeStatus === 429 || outcomeStatus === 402)) {
635
- recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
636
- ...codexQuotaOutcomeMeta(firstResponse),
637
- threadId: firstAuthCtx.affinityKey,
638
- modelId: route.modelId,
639
- probeLeaseId: codexProbeLeaseId(firstAuthCtx),
640
- probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
641
- transientProbe: codexTransientProbeGrant(firstAuthCtx),
642
- writerGeneration: firstAuthCtx.writerGeneration,
643
- });
644
- }
671
+ recordWrappedQuotaOutcome();
645
672
  // No usable alternate was resolved, so the reserved move never becomes a send.
646
673
  accountMovePermit?.release();
647
674
  recordUnmovedTransientOutcome();
648
675
  return { kind: "no-alternate" };
649
676
  }
650
677
 
678
+ if (
679
+ (outcomeStatus === 429 || outcomeStatus === 402)
680
+ && !await shouldRetryCodexScopedQuotaOnAlternate(
681
+ firstResponse,
682
+ firstAuthCtx.chatgptAccountId,
683
+ retryAuthCtx.kind === "pool" || retryAuthCtx.kind === "main-pool"
684
+ ? retryAuthCtx.chatgptAccountId
685
+ // A request-owned `main` alternate has no stored account id; its workspace
686
+ // identity is what the caller's own credential materializes upstream.
687
+ : callerCodexWorkspaceAccountId(callerAuthHeaders),
688
+ options.abortSignal,
689
+ )
690
+ ) {
691
+ // Suppressing the move is not suppressing the evidence: a same-workspace refusal
692
+ // still records its normalized quota outcome on the account that produced it.
693
+ recordWrappedQuotaOutcome();
694
+ accountMovePermit?.release();
695
+ releaseCodexAuthContextProbeLease(retryAuthCtx);
696
+ return { kind: "no-alternate" };
697
+ }
698
+
699
+ // The scope classification above reads the rejection body asynchronously, so the
700
+ // request may have been cancelled while it ran. Re-check before the send below
701
+ // mutates routing state or spends the alternate on a caller that is gone.
702
+ if (options.abortSignal?.aborted) {
703
+ recordWrappedQuotaOutcome();
704
+ recordUnmovedTransientOutcome();
705
+ accountMovePermit?.release();
706
+ releaseCodexAuthContextProbeLease(retryAuthCtx);
707
+ return { kind: "no-alternate" };
708
+ }
709
+
651
710
  const quotaMeta = { ...codexQuotaOutcomeMeta(firstResponse), ...(await codexDenialOutcomeMeta(firstResponse)) };
652
711
  if (outcomeStatus === 429 || outcomeStatus === 402) {
653
712
  const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
@@ -45,27 +45,28 @@ export async function consumeComboFailure(
45
45
  // a second body read, so this mirrors its normalization: raw 402/429, or a 5xx whose intact,
46
46
  // display-safe body carries a recognized quota message.
47
47
  let quotaConfirmedByBody = false;
48
+ const serverError = response.status >= 500 && response.status < 600;
48
49
  try {
49
- const body = await readBoundedResponseBody(response, {
50
- signal,
51
- // Match shouldRetryCodexPoolAccountQuota before treating a 5xx body as quota evidence.
52
- fatalUtf8: response.status >= 500 && response.status < 600,
53
- });
54
- usage = usageFromComboFailureText(body.text);
55
- if (
56
- response.status >= 500 && response.status < 600
57
- && body.displaySafe && !body.truncated
58
- ) {
50
+ const body = await readBoundedResponseBody(response, { signal, reportUtf8Validity: serverError });
51
+ // A 5xx body counts as quota or classification evidence only when it decoded as valid
52
+ // UTF-8, matching shouldRetryCodexPoolAccountQuota. A malformed byte keeps the status-only
53
+ // fallback, with one exception: a cyber-policy refusal must still stop the combo, so the
54
+ // replacement-decoded text may carry that verdict and nothing else.
55
+ const utf8Trusted = !serverError || body.utf8Valid === true;
56
+ if (utf8Trusted) usage = usageFromComboFailureText(body.text);
57
+ if (serverError && utf8Trusted && body.displaySafe && !body.truncated) {
59
58
  const quotaMessage = codexQuotaFailureMessage(body.text);
60
59
  quotaConfirmedByBody = quotaMessage !== undefined
61
60
  && isRateLimitOrQuotaFailureMessage(quotaMessage);
62
61
  }
63
- if (body.displaySafe) {
62
+ if (body.displaySafe && !body.truncated) {
64
63
  const normalized = normalizeUpstreamErrorText(body.text, fallback);
65
- classificationText = normalized.safeText;
66
- upstreamCode = normalized.code;
67
- upstreamMessage = normalized.message;
68
- upstreamType = normalized.type;
64
+ if (utf8Trusted || isCyberPolicyCode(normalized.code) || isCyberPolicyMessage(normalized.safeText)) {
65
+ classificationText = normalized.safeText;
66
+ upstreamCode = normalized.code;
67
+ upstreamMessage = normalized.message;
68
+ upstreamType = normalized.type;
69
+ }
69
70
  }
70
71
  } catch (error) {
71
72
  if (signal?.aborted) throw error;
@@ -16,6 +16,7 @@ import { prepareEffortNormalization } from "../effort-policy";
16
16
  import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
17
17
  import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
18
18
  import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
19
+ import { hasValidatedActiveReasoningEffort } from "../../responses/parser";
19
20
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
20
21
  import { applyOpenAiVirtualModel } from "../../providers/openai-virtual-models";
21
22
  import {
@@ -164,7 +165,8 @@ export async function applyFinalRouteRequestNormalization(args: {
164
165
  if (inboundWire === "responses" && parsed._rawBody) {
165
166
  const summary = (parsed._rawBody as { reasoning?: { summary?: unknown } }).reasoning?.summary;
166
167
  parsed.options.hideThinkingSummary = summary === "none"
167
- || (!summary && route.provider.showThinkingSummary !== true);
168
+ || (!summary && !hasValidatedActiveReasoningEffort(parsed.options)
169
+ && route.provider.showThinkingSummary !== true);
168
170
  }
169
171
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
170
172
  logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
@@ -195,6 +197,8 @@ export async function applyFinalRouteRequestNormalization(args: {
195
197
 
196
198
  if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
197
199
  logCtx.resolvedModel = route.modelId;
200
+ logCtx.wireModel = route.modelId;
201
+ logCtx.responseModelEcho = parsed._responseModelId;
198
202
  logCtx.preserveResolvedModelFromRoute = true;
199
203
  }
200
204
 
@@ -1,11 +1,14 @@
1
1
  import { ENCRYPTED_FUNCTION_OUTPUT_REJECTION, upstreamErrorMessageFromPayload } from "../../lib/errors";
2
2
  import { readBoundedResponseBody } from "../../lib/bounded-body";
3
3
  import { isReasoningEffortRejection } from "../../providers/reasoning-metadata";
4
+ import { isAnthropicFastRefusal } from "../../providers/anthropic-fast";
5
+ import type { AdapterRequest } from "../../adapters/base";
4
6
  import { isNonReplayableResponse } from "../../lib/upstream-retry";
5
7
  import type { OcxParsedRequest } from "../../types";
6
8
  import type { RequestLogContext } from "../request-log";
7
9
  import type { AttemptRecoveryKind } from "../../usage/log";
8
10
  import { rememberReasoningReplayOpaqueBlobRejection } from "../../responses/reasoning-replay-cache";
11
+ import { resolvedAdapterWire } from "../../responses/continuation-ownership";
9
12
 
10
13
  export const OPAQUE_RESPONSES_INPUT_TYPES = new Set([
11
14
  "reasoning",
@@ -22,6 +25,21 @@ export const FUNCTION_OUTPUT_TYPES = new Set(["function_call_output", "custom_to
22
25
  export const AGENT_MESSAGE_TYPE = "agent_message";
23
26
 
24
27
 
28
+ /**
29
+ * Whether the adapter serving this send speaks the Responses wire, as the adapter registry
30
+ * declares it.
31
+ *
32
+ * Recovery used to compare the adapter name with `"openai-responses"`. Azure OpenAI wraps the same
33
+ * passthrough under its own name, so an `invalid_encrypted_content` after moving a conversation
34
+ * onto Azure never recovered (#5583). The registry already records that relationship as
35
+ * `contractParent: "openai-responses"`, so reading the resolved wire covers Azure and any later
36
+ * wrapper of the same contract by construction, while every translated wire stays excluded.
37
+ */
38
+ export function adapterSpeaksResponsesWire(adapterName: string): boolean {
39
+ return resolvedAdapterWire(adapterName) === "openai-responses";
40
+ }
41
+
42
+
25
43
  export function encryptedFunctionOutputParts(output: unknown): boolean {
26
44
  return Array.isArray(output) && output.some(part => (
27
45
  part !== null
@@ -282,7 +300,7 @@ export function shouldAttemptOpaqueBlobRecovery(args: {
282
300
  && isEncryptedFunctionOutputRejection(args.errorBody)
283
301
  );
284
302
  return acceptedStatus
285
- && args.adapterName === "openai-responses"
303
+ && adapterSpeaksResponsesWire(args.adapterName)
286
304
  && !args.alreadyAttempted
287
305
  && outboundResponsesBodyCarriesOpaqueBlob(args.outboundBody)
288
306
  && isSelfIdentifiedOpaqueBlobRejection(args.errorBody);
@@ -312,6 +330,33 @@ export async function reasoningEffortRejectionText(
312
330
  }
313
331
 
314
332
 
333
+ /**
334
+ * Whether an Anthropic response refused the fast lane of a request that actually sent it.
335
+ *
336
+ * "Actually sent" is read from the adapter's own tier record for that exact request, never from
337
+ * the route: a request built without `speed` can be refused for other reasons and must not be
338
+ * downgraded. The body is read from a clone, so a refusal that is not recovered (or whose resend
339
+ * is not admitted) still reaches the caller intact.
340
+ */
341
+ export async function anthropicFastRefused(
342
+ response: Response,
343
+ sentRequest: AdapterRequest | undefined,
344
+ adapterName: string,
345
+ alreadyAttempted: boolean,
346
+ signal: AbortSignal,
347
+ ): Promise<boolean> {
348
+ if (alreadyAttempted || adapterName !== "anthropic") return false;
349
+ if (response.status !== 400 && response.status !== 429) return false;
350
+ const outcome = sentRequest?.tierLog?.outcome;
351
+ if (outcome?.wireKind !== "anthropic-speed" || typeof outcome.wireValue !== "string") return false;
352
+ try {
353
+ const body = await readBoundedResponseBody(response.clone(), { signal });
354
+ return isAnthropicFastRefusal(response.status, response.headers, body.truncated ? undefined : body.text);
355
+ } catch {
356
+ return isAnthropicFastRefusal(response.status, response.headers, undefined);
357
+ }
358
+ }
359
+
315
360
  export async function opaqueBlobRejectionBodyForRecovery(
316
361
  response: Response,
317
362
  outboundBody: string | undefined,
@@ -323,7 +368,7 @@ export async function opaqueBlobRejectionBodyForRecovery(
323
368
  isNonReplayableResponse(response)
324
369
  || response.status < 400
325
370
  || (response.status >= 500 && response.status !== 502)
326
- || adapterName !== "openai-responses"
371
+ || !adapterSpeaksResponsesWire(adapterName)
327
372
  || alreadyAttempted
328
373
  || !outboundResponsesBodyCarriesOpaqueBlob(outboundBody)
329
374
  ) return undefined;
@@ -365,6 +410,9 @@ export async function consoleGoUploadRejectionBody(
365
410
 
366
411
  export function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
367
412
  parsed._stripReasoningEncryptedContent = true;
413
+ // The destination rejected state another serving identity minted. A reasoning item's id names
414
+ // an item in that identity's store, so it goes with the blob.
415
+ parsed._dropForeignReasoningItemIds = true;
368
416
  const rawBody = parsed._rawBody;
369
417
  if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) return;
370
418
  const input = (rawBody as { input?: unknown }).input;
@@ -114,6 +114,8 @@ export interface HandleResponsesOptions {
114
114
  callerDirectAuth?: CallerDirectAuth | null;
115
115
  /** Internal recursion guard; callers outside this module must not set it. */
116
116
  comboAttempt?: boolean;
117
+ /** Internal handoff: this combo was selected by shadow-call interception. */
118
+ shadowCallIntercepted?: boolean;
117
119
  compactionRoutingOverride?: CompactionRoutingOverride | null;
118
120
  /** Internal combo handoff for one parent-validated continuation snapshot. */
119
121
  comboReplaySnapshot?: {
@@ -21,6 +21,7 @@ import {
21
21
  durableReplayDestinationIdentity,
22
22
  bindReasoningReplayScope,
23
23
  reasoningReplayServingIdentityChanged,
24
+ reasoningReplayItemStoreChanged,
24
25
  reasoningReplayOpaqueBlobRejectionMemoized,
25
26
  } from "../../responses/reasoning-replay-cache";
26
27
  import type { OAuthAccessSnapshot } from "../../oauth";
@@ -137,9 +138,15 @@ export function bindRouteReasoningReplayScope(args: {
137
138
  // after the first mismatch, but it cannot make history minted by the prior route decodable.
138
139
  if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
139
140
  parsed._stripReasoningEncryptedContent = true;
141
+ // Only a different destination or credential makes the replayed item ids unresolvable; a
142
+ // model change on the same store keeps them.
143
+ if (reasoningReplayItemStoreChanged(parsed._reasoningReplayScope)) {
144
+ parsed._dropForeignReasoningItemIds = true;
145
+ }
140
146
  }
141
147
  if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
142
148
  parsed._stripReasoningEncryptedContent = true;
149
+ parsed._dropForeignReasoningItemIds = true;
143
150
  }
144
151
  bindProviderContinuationForRoute(parsed, continuationOwner);
145
152
  }
@@ -189,7 +189,7 @@ export { readDisplaySafeErrorText } from "./core-errors";
189
189
  export { usesCodexForwardPoolAuth } from "./core-codex-account";
190
190
  export { preAuthUpstreamHostCircuitKey } from "./core-codex-account";
191
191
  export { upstreamHostCircuitOpenResponse } from "./core-codex-account";
192
- export { shouldRetryCodexPoolAccountQuota } from "./core-codex-account";
192
+ export { shouldRetryCodexPoolAccountQuota, shouldRetryCodexScopedQuotaOnAlternate } from "./core-codex-account";
193
193
  export { shouldRetryCodexPoolAccountTransient } from "./core-codex-account";
194
194
  export { codexAccountGatedCanonicalWireModel } from "./core-codex-account";
195
195
  export { codexForwardTerminalOutcomeRecorder } from "./core-codex-account";
@@ -123,11 +123,18 @@ function looksLikeUnknownOpaqueSlot(payload: string): boolean {
123
123
  */
124
124
  const FERNET_TOKEN_CANDIDATE = /g[A-Za-z0-9_-]{97,}={0,2}/g;
125
125
  const FERNET_TOKEN_BOUNDARY_CHAR = /[A-Za-z0-9_=-]/;
126
+ // A normal mixed agent task contains one encrypted body. Keep pathological slots
127
+ // from amplifying into an attacker-controlled number of request parts.
128
+ const MAX_EMBEDDED_FERNET_RUNS_PER_SLOT = 64;
126
129
 
127
130
  interface FernetTokenRun {
128
131
  index: number;
129
132
  token: string;
130
133
  }
134
+ interface FernetRunScan {
135
+ runs: FernetTokenRun[];
136
+ overflow: boolean;
137
+ }
131
138
 
132
139
  /**
133
140
  * Validate only the key-independent Fernet wire structure. Authenticity cannot be
@@ -158,11 +165,11 @@ function isStructurallyValidFernetToken(token: string): boolean {
158
165
  }
159
166
 
160
167
  export function structurallyValidFernetTokens(payload: string): string[] {
161
- return fernetTokenRuns(payload).map(run => run.token);
168
+ return fernetTokenRuns(payload).runs.map(run => run.token);
162
169
  }
163
170
 
164
171
  /** Maximal, boundary-delimited and structurally valid Fernet runs embedded in a slot. */
165
- function fernetTokenRuns(payload: string): FernetTokenRun[] {
172
+ function fernetTokenRuns(payload: string): FernetRunScan {
166
173
  const runs: FernetTokenRun[] = [];
167
174
  for (const match of payload.matchAll(FERNET_TOKEN_CANDIDATE)) {
168
175
  const index = match.index ?? 0;
@@ -172,9 +179,10 @@ function fernetTokenRuns(payload: string): FernetTokenRun[] {
172
179
  if (before && FERNET_TOKEN_BOUNDARY_CHAR.test(before)) continue;
173
180
  if (after && FERNET_TOKEN_BOUNDARY_CHAR.test(after)) continue;
174
181
  if (!isStructurallyValidFernetToken(token)) continue;
182
+ if (runs.length === MAX_EMBEDDED_FERNET_RUNS_PER_SLOT) return { runs, overflow: true };
175
183
  runs.push({ index, token });
176
184
  }
177
- return runs;
185
+ return { runs, overflow: false };
178
186
  }
179
187
 
180
188
  function textWithoutFernetRuns(payload: string, runs: readonly FernetTokenRun[]): string {
@@ -291,7 +299,9 @@ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
291
299
  }
292
300
 
293
301
  if (fragmentParts.has(part)) continue;
294
- const runs = fernetTokenRuns(record.encrypted_content);
302
+ const scan = fernetTokenRuns(record.encrypted_content);
303
+ if (scan.overflow) return true;
304
+ const { runs } = scan;
295
305
  if (runs.length > 0) hasFernetTask = true;
296
306
  readableParts.push(textWithoutFernetRuns(record.encrypted_content, runs));
297
307
  }
@@ -308,9 +318,11 @@ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
308
318
 
309
319
 
310
320
  export function encryptedSlotParts(payload: string): Array<Record<string, string>> {
321
+ const scan = fernetTokenRuns(payload);
322
+ if (scan.overflow) return [{ type: "input_text", text: OMITTED_ENCRYPTED_CONTENT_TEXT }];
311
323
  const parts: Array<Record<string, string>> = [];
312
324
  let last = 0;
313
- for (const run of fernetTokenRuns(payload)) {
325
+ for (const run of scan.runs) {
314
326
  const before = payload.slice(last, run.index);
315
327
  if (before.trim().length > 0) parts.push({ type: "input_text", text: before });
316
328
  parts.push({ type: "encrypted_content", encrypted_content: run.token });
@@ -420,7 +432,9 @@ export function stripAgentMessageCiphertextInPlace(input: unknown): number {
420
432
 
421
433
  /** Free text: drop embedded token runs, and replace a slot that is nothing but a token. */
422
434
  function textWithoutCiphertext(text: string): string {
423
- const runs = fernetTokenRuns(text);
435
+ const scan = fernetTokenRuns(text);
436
+ if (scan.overflow) return OMITTED_ENCRYPTED_CONTENT_TEXT;
437
+ const { runs } = scan;
424
438
  if (runs.length > 0) return textWithRunsOmitted(text, runs);
425
439
  return looksLikeFernetToken(text.trim()) ? OMITTED_ENCRYPTED_CONTENT_TEXT : text;
426
440
  }
@@ -473,11 +487,11 @@ function contentWithoutCiphertext(content: unknown[]): unknown[] {
473
487
  changed = true;
474
488
  // Keep whatever plaintext a recognizable slot carries around its token; a slot this
475
489
  // cannot parse is replaced whole rather than forwarded on the chance that it is benign.
476
- const runs = fernetTokenRuns(record.encrypted_content);
490
+ const scan = fernetTokenRuns(record.encrypted_content);
477
491
  return {
478
492
  type: "input_text",
479
- text: runs.length > 0
480
- ? textWithRunsOmitted(record.encrypted_content, runs)
493
+ text: scan.overflow ? OMITTED_ENCRYPTED_CONTENT_TEXT : scan.runs.length > 0
494
+ ? textWithRunsOmitted(record.encrypted_content, scan.runs)
481
495
  : OMITTED_ENCRYPTED_CONTENT_TEXT,
482
496
  };
483
497
  }
@@ -207,7 +207,10 @@ export async function deliverPassthroughResponse(
207
207
 
208
208
  const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions);
209
209
  const resolvedModel = headers.get("openai-model")?.trim();
210
- if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
210
+ if (resolvedModel) {
211
+ logCtx.servedModel = resolvedModel;
212
+ if (!logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
213
+ }
211
214
  if (isUsageDebugEnabled()) {
212
215
  const upstreamContentType = upstreamResponse.headers.get("content-type");
213
216
  if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
@@ -380,36 +383,62 @@ export async function deliverPassthroughResponse(
380
383
  });
381
384
  // Capture the binding that actually served the first leg, after its permitted reselection.
382
385
  const webSearchBridgeBinding = requestBindings.get(nativeExchange.request);
383
- // The bridge wraps the RAW upstream body, so terminal repair below still owns the single
384
- // client-facing terminal — the bridge drops the terminal of every intercepted leg.
385
- const upstreamSseBody = webSearchBridgePlan
386
+ // Repair must observe the raw first leg before the bridge suppresses an intercepted search
387
+ // lifecycle. Otherwise a provider that leaves that complete call open never arms repair's
388
+ // grace timer, so the bridge cannot execute the search or begin its continuation.
389
+ let passthroughSseBody = terminalRepairPolicy
390
+ ? relayResponsesSseWithTerminalRepair(
391
+ upstreamResponse.body,
392
+ upstream,
393
+ terminalRepairPolicy,
394
+ translatorBudget,
395
+ options.responsesTerminalRepairScheduler,
396
+ )
397
+ : upstreamResponse.body;
398
+ passthroughSseBody = webSearchBridgePlan
386
399
  ? createPassthroughWebSearchBridgeStream({
387
400
  plan: webSearchBridgePlan,
388
- firstLeg: upstreamResponse.body,
401
+ firstLeg: passthroughSseBody,
389
402
  requestBody: nativeExchange.request.body,
390
403
  // Continuation legs replay the same built request with the executed search appended.
391
404
  // The first leg already passed the recovery ladder, the outbound size ceiling, and the
392
405
  // host circuit; a KEY-auth destination has no OAuth refresh to replay on a later leg.
393
- send: (continuationBody: string) => fetchWithHeaderTimeout(
394
- nativeExchange.request.url,
395
- { method: nativeExchange.request.method, headers: nativeExchange.request.headers, body: continuationBody },
396
- upstream.signal,
397
- connectMs,
398
- true,
399
- providerFetch(route.provider, options.codexWsRuntimeIdentity, {
400
- // Pacing can outlive a manual selection change. A continuation must retain the
401
- // first leg's key and appended search result, never rebuild from the original turn.
402
- beforeDispatch: () => {
403
- if (webSearchBridgeBinding?.kind !== "api-key"
404
- || !providerApiKeySelectionIsCurrent(config, route.providerName, webSearchBridgeBinding.provider)) {
405
- throw new Error("API key selection changed during a web-search continuation");
406
- }
407
- },
408
- providerName: route.providerName,
409
- modelId: route.modelId,
410
- }),
411
- false,
412
- ),
406
+ send: async (continuationBody: string) => {
407
+ const continuation = await fetchWithHeaderTimeout(
408
+ nativeExchange.request.url,
409
+ { method: nativeExchange.request.method, headers: nativeExchange.request.headers, body: continuationBody },
410
+ upstream.signal,
411
+ connectMs,
412
+ true,
413
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
414
+ // Pacing can outlive a manual selection change. A continuation must retain the
415
+ // first leg's key and appended search result, never rebuild from the original turn.
416
+ beforeDispatch: () => {
417
+ if (webSearchBridgeBinding?.kind !== "api-key"
418
+ || !providerApiKeySelectionIsCurrent(config, route.providerName, webSearchBridgeBinding.provider)) {
419
+ throw new Error("API key selection changed during a web-search continuation");
420
+ }
421
+ },
422
+ providerName: route.providerName,
423
+ modelId: route.modelId,
424
+ }),
425
+ false,
426
+ );
427
+ // The same provider can leave a complete continuation open without a terminal, which
428
+ // stalls the bridge's decide loop exactly like the first leg — so every leg gets the
429
+ // same repair, not only the intercepted first one.
430
+ if (!terminalRepairPolicy || !continuation.ok || !continuation.body) return continuation;
431
+ return new Response(
432
+ relayResponsesSseWithTerminalRepair(
433
+ continuation.body,
434
+ upstream,
435
+ terminalRepairPolicy,
436
+ translatorBudget,
437
+ options.responsesTerminalRepairScheduler,
438
+ ),
439
+ continuation,
440
+ );
441
+ },
413
442
  execute: createPassthroughWebSearchBridgeExecutor(webSearchBridgePlan, {
414
443
  providerApiKey: route.provider.apiKey ?? "",
415
444
  auth: webSearchBridgeAuth,
@@ -432,16 +461,7 @@ export async function deliverPassthroughResponse(
432
461
  onFinalize: () => releaseCodexAuthContextProbeLease(openAiSidecar?.authContext),
433
462
  signal: upstream.signal,
434
463
  })
435
- : upstreamResponse.body;
436
- const passthroughSseBody = terminalRepairPolicy
437
- ? relayResponsesSseWithTerminalRepair(
438
- upstreamSseBody,
439
- upstream,
440
- terminalRepairPolicy,
441
- translatorBudget,
442
- options.responsesTerminalRepairScheduler,
443
- )
444
- : upstreamSseBody;
464
+ : passthroughSseBody;
445
465
  const repairConfig = route.provider.responsesItemIdRepair;
446
466
  // Grok Build renders deltas live but reconstructs its durable assistant
447
467
  // turn from the completed response snapshot. Native Responses streams