@bitkyc08/opencodex 2.52.0 → 2.53.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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-BlO4Yl6q.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -1,3 +1,4 @@
1
+ import { capturePoolQuotaWriter } from "../../codex/account-store";
1
2
  import type { Server } from "bun";
2
3
  import { recordContextSessionOwner } from "../../codex/context-owner";
3
4
  import { contextRelayActivated } from "../../codex/context-compat";
@@ -104,7 +105,10 @@ import {
104
105
  } from "../../lib/errors";
105
106
  import { injectionDebugLog } from "../../lib/injection-debug-log";
106
107
  import { resolveClientRetryAfter } from "../../lib/retry-after";
107
- import { enrichOpenCodeZenUpstreamMessage } from "../../providers/opencode-zen-rate-limit";
108
+ import {
109
+ enrichOpenCodeZenUpstreamMessage,
110
+ isTransientConsoleGoUploadRejection,
111
+ } from "../../providers/opencode-zen-rate-limit";
108
112
  import { CODE_MODE_EXEC_TOOL_NAME, modelInList, namespacedToolName } from "../../types";
109
113
  import type {
110
114
  AdapterEvent,
@@ -216,6 +220,7 @@ import {
216
220
  isNonReplayableResponse,
217
221
  isTransientUpstreamStatus,
218
222
  prepareSameTarget429Wait,
223
+ sleepWithAbort,
219
224
  } from "../../lib/upstream-retry";
220
225
  import {
221
226
  ForwardAdmissionCredentialError,
@@ -291,6 +296,7 @@ import {
291
296
  } from "../lifecycle";
292
297
  import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
293
298
  import { readBoundedResponseBody } from "../../lib/bounded-body";
299
+ import { isReasoningEffortRejection, planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
294
300
  import {
295
301
  ENCRYPTED_FUNCTION_OUTPUT_REJECTION,
296
302
  isRateLimitOrQuotaFailureMessage,
@@ -346,6 +352,7 @@ import {
346
352
  markEagerRelaySseResponse,
347
353
  markNativePassthroughSseResponse,
348
354
  relaySseWithFailedTail,
355
+ codexSafetyBufferingFilterOptions,
349
356
  relayWithAbort,
350
357
  sanitizePassthroughHeaders,
351
358
  } from "../relay";
@@ -364,17 +371,12 @@ import {
364
371
  import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
365
372
  import { cancelBodyOnAbort } from "../../lib/abort";
366
373
  import { isCodexWsUpstreamResponse, type BunRuntimeGateInput } from "./ws-upstream";
374
+ import { readCodexWsStage } from "./codex-ws-wire";
367
375
  import {
368
376
  createResponsesItemIdPayloadRewrite,
369
377
  hasResponsesItemIdRepair,
370
378
  repairResponsesJsonItemIds,
371
379
  } from "../responses-item-id-repair";
372
- import {
373
- createReasoningSummaryChannelPayloadRewrite,
374
- rewriteReasoningSummaryInJson,
375
- rewriteReasoningSummaryInJsonString,
376
- routeUsesContentChannelReasoning,
377
- } from "../responses-reasoning-summary-rewrite";
378
380
  import {
379
381
  createImageGenCallRestoreRewrite,
380
382
  imageGenToolCallAliases,
@@ -431,6 +433,19 @@ import {
431
433
  restoreRoutedNamespaceCallsInJson,
432
434
  type RoutedNamespaceToolAliases,
433
435
  } from "../../responses/namespace-tool-compat";
436
+ import {
437
+ createPlaintextV2AgentMessageCallRestoreRewrite,
438
+ PLAINTEXT_V2_AGENT_MESSAGE_RESTORE_OVERFLOW_MESSAGE,
439
+ restorePlaintextV2AgentMessageCalls,
440
+ restorePlaintextV2AgentMessageCallsInJsonResult,
441
+ shouldPreparePlaintextV2AgentMessages,
442
+ } from "../../responses/plaintext-v2-agent-messages";
443
+ import {
444
+ createMuseToolNameRestoreRewrite,
445
+ restoreMuseToolNames,
446
+ restoreMuseToolNamesInJson,
447
+ type MuseToolNameAliases,
448
+ } from "../../responses/muse-tool-name-alias";
434
449
  import {
435
450
  collectDeclaredBareWireToolNames,
436
451
  collectDeclaredNamelessClientCallTypes,
@@ -830,6 +845,28 @@ export function shouldAttemptOpaqueBlobRecovery(args: {
830
845
  && isSelfIdentifiedOpaqueBlobRejection(args.errorBody);
831
846
  }
832
847
 
848
+ /**
849
+ * Peek the upstream error body for the reasoning-effort downgrade. Only 400/403 are considered
850
+ * and the body must be complete and display-safe, the same contract the other rejection peeks
851
+ * use. The match is deliberately narrow: the upstream has to name reasoning effort, so an
852
+ * unrelated 400 never triggers a replay.
853
+ */
854
+ async function reasoningEffortRejectionText(
855
+ response: Response,
856
+ alreadyAttempted: boolean,
857
+ signal: AbortSignal,
858
+ ): Promise<string | undefined> {
859
+ if (alreadyAttempted) return undefined;
860
+ if (response.status !== 400 && response.status !== 403) return undefined;
861
+ try {
862
+ const body = await readBoundedResponseBody(response.clone(), { signal });
863
+ if (!body.displaySafe || body.truncated) return undefined;
864
+ return isReasoningEffortRejection(body.text) ? body.text : undefined;
865
+ } catch {
866
+ return undefined;
867
+ }
868
+ }
869
+
833
870
  async function opaqueBlobRejectionBodyForRecovery(
834
871
  response: Response,
835
872
  outboundBody: string | undefined,
@@ -853,6 +890,31 @@ async function opaqueBlobRejectionBodyForRecovery(
853
890
  }
854
891
  }
855
892
 
893
+ /**
894
+ * Backoff for the single exact-request replay after a canonical Console upload rejection.
895
+ */
896
+ const CONSOLE_GO_UPLOAD_RETRY_DELAY_MS = 800;
897
+
898
+ /**
899
+ * Peek the upstream error body for the Console Go transient-400 recovery. Only a complete,
900
+ * display-safe body may drive a retry decision (same contract as
901
+ * opaqueBlobRejectionBodyForRecovery), and reading a clone leaves the original response intact
902
+ * for the caller's own error surface when no retry is taken.
903
+ */
904
+ async function consoleGoUploadRejectionBody(
905
+ response: Response,
906
+ alreadyAttempted: boolean,
907
+ signal: AbortSignal,
908
+ ): Promise<string | undefined> {
909
+ if (isNonReplayableResponse(response) || response.status !== 400 || alreadyAttempted) return undefined;
910
+ try {
911
+ const body = await readBoundedResponseBody(response.clone(), { signal });
912
+ return body.displaySafe && !body.truncated ? body.text : undefined;
913
+ } catch {
914
+ return undefined;
915
+ }
916
+ }
917
+
856
918
  /**
857
919
  * Materialize an upstream error body only when the bounded reader observed a complete,
858
920
  * display-safe payload. Partial timeout and over-limit prefixes are attacker-controlled,
@@ -1044,7 +1106,7 @@ function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderCo
1044
1106
  const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
1045
1107
  return headers => {
1046
1108
  if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return;
1047
- applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId });
1109
+ applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined });
1048
1110
  };
1049
1111
  }
1050
1112
 
@@ -1449,7 +1511,7 @@ async function retryCodexPoolOnAlternateAccount(
1449
1511
  firstResponse.headers,
1450
1512
  firstAuthCtx.writerGeneration,
1451
1513
  firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
1452
- { modelId: route.modelId },
1514
+ { modelId: route.modelId, poolWriter: firstAuthCtx.kind === "pool" ? firstAuthCtx.poolQuotaWriter : undefined },
1453
1515
  );
1454
1516
  }
1455
1517
  const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
@@ -2423,6 +2485,7 @@ async function refreshPoolForwardAuth(args: {
2423
2485
  accessToken: refreshed.accessToken,
2424
2486
  chatgptAccountId: refreshed.chatgptAccountId,
2425
2487
  generation: refreshed.generation,
2488
+ poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
2426
2489
  };
2427
2490
  const provider = applyCodexAuthContextToProvider(
2428
2491
  stripCodexRuntimeProviderFields(route.provider),
@@ -2572,6 +2635,19 @@ async function applyFinalRouteRequestNormalization(args: {
2572
2635
  route.provider = resolveOpenCodeGoTransport(route.provider,
2573
2636
  args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req));
2574
2637
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
2638
+ parsed._plaintextV2AgentMessages = shouldPreparePlaintextV2AgentMessages({
2639
+ enabled: config.plaintextV2AgentMessages === true,
2640
+ inboundWire,
2641
+ canonicalChatGpt: isCanonicalOpenAiForwardProvider(route.provider),
2642
+ requestBody: parsed._rawBody,
2643
+ });
2644
+ // Recompute from the original wire preference on every route, including fallback.
2645
+ // A provider default never converts raw reasoning into a summary.
2646
+ if (inboundWire === "responses" && parsed._rawBody) {
2647
+ const summary = (parsed._rawBody as { reasoning?: { summary?: unknown } }).reasoning?.summary;
2648
+ parsed.options.hideThinkingSummary = summary === "none"
2649
+ || (!summary && route.provider.showThinkingSummary !== true);
2650
+ }
2575
2651
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
2576
2652
  logCtx.model = route.modelId;
2577
2653
  logCtx.provider = route.providerName;
@@ -2927,6 +3003,7 @@ export async function handleComboResponses(
2927
3003
  pick.target,
2928
3004
  comboDefaultEffort(config, comboId),
2929
3005
  supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
3006
+ combo.reasoningEffortMode,
2930
3007
  );
2931
3008
  const childHeaders = buildComboChildHeaders(req.headers);
2932
3009
  const childRequest = new Request(req.url, {
@@ -4804,14 +4881,23 @@ async function handleResponsesInner(
4804
4881
  }
4805
4882
 
4806
4883
  let routedNamespaceToolAliases: RoutedNamespaceToolAliases = new Map();
4807
- const refreshRoutedNamespaceToolAliases = (builtRequest: AdapterRequest): void => {
4884
+ let plaintextV2AgentMessageToolNames: ReadonlySet<string> = new Set();
4885
+ let plaintextV2AgentMessageAliasedToolNames: ReadonlySet<string> = new Set();
4886
+ let routedMuseToolNameAliases: MuseToolNameAliases = new Map();
4887
+ const refreshRequestToolAliases = (builtRequest: AdapterRequest): void => {
4808
4888
  routedNamespaceToolAliases = builtRequest.convertedRoutedNamespaceToolAliases ?? new Map();
4889
+ plaintextV2AgentMessageToolNames = builtRequest.plaintextV2AgentMessageToolNames ?? new Set();
4890
+ plaintextV2AgentMessageAliasedToolNames = builtRequest.plaintextV2AgentMessageAliasedToolNames ?? new Set();
4891
+ routedMuseToolNameAliases = builtRequest.convertedMuseToolNameAliases ?? new Map();
4809
4892
  };
4810
4893
 
4811
4894
  if ("passthrough" in adapter && adapter.passthrough && !routedCompaction) {
4812
4895
  let hostAdmissionLease = pendingHostAdmissionLease;
4813
4896
  pendingHostAdmissionLease = null;
4814
4897
  try {
4898
+ const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider)
4899
+ ? codexSafetyBufferingFilterOptions(config)
4900
+ : undefined;
4815
4901
  const imageGenCallAliases = route.provider.authMode === "forward"
4816
4902
  ? new Map<string, { namespace: string; name: string }>()
4817
4903
  : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
@@ -4902,7 +4988,7 @@ async function handleResponsesInner(
4902
4988
  // would incorrectly disable restoration for the exact ambiguous-name case the alias fixes.
4903
4989
  routedToolSearchNames.add(name);
4904
4990
  }
4905
- refreshRoutedNamespaceToolAliases(request);
4991
+ refreshRequestToolAliases(request);
4906
4992
  // #1700: the bridged paths refuse a call to a tool the request never declared
4907
4993
  // (`declaredToolNames`, src/bridge.ts). The passthrough had no equivalent, so a routed
4908
4994
  // provider's top-level `apply_patch` — which under Codex code mode exists only as a nested
@@ -5008,7 +5094,7 @@ async function handleResponsesInner(
5008
5094
  // `buildToolBridgeMaps` also aliases a namespaced tool under its bare name when the
5009
5095
  // caller's `tool_choice` selected it unambiguously, which the bridge needs to route the
5010
5096
  // call back. For `exec` alone that alias would also switch on nested-helper
5011
- // normalization and re-authorize `exec_command`/`shell_command`/`apply_patch`, so it is
5097
+ // normalization and re-authorize `exec_command`/`shell_command`/`apply_patch`/`view_image`, so it is
5012
5098
  // admitted here only when the caller's own catalog declared a bare `exec`. Selecting an
5013
5099
  // MCP `exec` is not a declaration of the code-mode shell tool.
5014
5100
  if (
@@ -5078,7 +5164,9 @@ async function handleResponsesInner(
5078
5164
  // provider) every name looks undeclared, and flipping this would stop recording continuation
5079
5165
  // state for exactly the passthrough traffic the guard deliberately stands down for.
5080
5166
  if (undeclaredToolGuardActive && !inspectionSawUndeclaredTool && undeclaredToolCallName(
5081
- restoreAuthorizedBareNamespaceToolCalls(payload),
5167
+ restoreAuthorizedBareNamespaceToolCalls(
5168
+ restoreMuseToolNames(payload, routedMuseToolNameAliases).value,
5169
+ ),
5082
5170
  declaredWireToolNames,
5083
5171
  declaredNamelessClientCallTypes,
5084
5172
  providerExecutedCallTypes,
@@ -5088,7 +5176,7 @@ async function handleResponsesInner(
5088
5176
  }
5089
5177
  // The snapshot callback opts the inspector into output reconstruction. Compaction
5090
5178
  // has no continuation cache, so use the parsed terminal here without adding retention.
5091
- if (!rememberPassthroughResponse && payload && typeof payload === "object"
5179
+ if (plaintextV2AgentMessageToolNames.size === 0 && !rememberPassthroughResponse && payload && typeof payload === "object"
5092
5180
  && "type" in payload && payload.type === "response.completed"
5093
5181
  && "response" in payload && payload.response && typeof payload.response === "object"
5094
5182
  && !Array.isArray(payload.response)) {
@@ -5100,19 +5188,26 @@ async function handleResponsesInner(
5100
5188
  ) => {
5101
5189
  if (inspectionSawUndeclaredTool) return;
5102
5190
  const restored = restoreRoutedCustomCalls(
5103
- restoreAuthorizedBareNamespaceToolCalls(restoreRoutedNamespaceCalls(response, routedNamespaceToolAliases).value),
5191
+ restoreAuthorizedBareNamespaceToolCalls(
5192
+ restoreRoutedNamespaceCalls(
5193
+ restoreMuseToolNames(response, routedMuseToolNameAliases).value,
5194
+ routedNamespaceToolAliases,
5195
+ ).value,
5196
+ ),
5104
5197
  routedCustomToolNames,
5105
5198
  routedCustomToolRepairNames,
5106
5199
  declaredWireToolNames,
5107
5200
  ).value;
5108
- const restoredResponse = (functionRepairSchemas.size > 0
5201
+ const normalizedResponse = (functionRepairSchemas.size > 0
5109
5202
  ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored)))
5110
5203
  : restored) as { id?: unknown; output?: unknown; status?: unknown };
5204
+ const plaintextRestore = restorePlaintextV2AgentMessageCalls(
5205
+ normalizedResponse, plaintextV2AgentMessageToolNames, plaintextV2AgentMessageAliasedToolNames,
5206
+ );
5207
+ if (plaintextRestore.overflowed) return;
5208
+ const restoredResponse = plaintextRestore.value as typeof normalizedResponse;
5111
5209
  // Replay overlap compares the items the client echoes, including visible reasoning shape.
5112
- const replayResponse = parsed.options.hideThinkingSummary !== true
5113
- && routeUsesContentChannelReasoning(route.provider, route.modelId)
5114
- ? rewriteReasoningSummaryInJson(restoredResponse) as typeof restoredResponse
5115
- : restoredResponse;
5210
+ const replayResponse = restoredResponse;
5116
5211
  if (
5117
5212
  undeclaredToolGuardActive
5118
5213
  && undeclaredToolCallNameInResponse(
@@ -5189,6 +5284,22 @@ async function handleResponsesInner(
5189
5284
  }
5190
5285
  hostAdmissionLease = null;
5191
5286
  };
5287
+ /**
5288
+ * #4191: a Codex WS exchange pins its content-free stage record on the
5289
+ * Response it resolves (markCodexWsStage). Adopting the record here, at
5290
+ * the single funnel every physical upstream response passes through,
5291
+ * binds it to the attempt that actually served it — including the 502/504
5292
+ * pre-response JSON settles that never reach the SSE relay.
5293
+ */
5294
+ const adoptCodexWsStage = (response: Response): void => {
5295
+ const stage = readCodexWsStage(response);
5296
+ if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
5297
+ };
5298
+ const adoptObservedResponse = <T extends Response>(response: T): T => {
5299
+ settleObservedHostResponse();
5300
+ adoptCodexWsStage(response);
5301
+ return response;
5302
+ };
5192
5303
  let passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
5193
5304
  ? request.usageLog.inputTokens
5194
5305
  : undefined;
@@ -5320,10 +5431,7 @@ async function handleResponsesInner(
5320
5431
  route.provider.authMode === "forward")
5321
5432
  // Every real attempt response — including an intermediate 5xx the
5322
5433
  // retry wrapper replaces — proves the host was reached (#914 review).
5323
- .then(res => {
5324
- settleObservedHostResponse();
5325
- return res;
5326
- });
5434
+ .then(adoptObservedResponse);
5327
5435
  },
5328
5436
  { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
5329
5437
  );
@@ -5334,8 +5442,13 @@ async function handleResponsesInner(
5334
5442
  }
5335
5443
 
5336
5444
  const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
5445
+ // At most one reasoning-effort downgrade per request.
5446
+ const reasoningEffortDowngradeGuard: { attempted: boolean } = { attempted: false };
5337
5447
  let oauth401ReplayAttempted = false;
5338
5448
  let codex401ReplayKind: "main" | "stored" | null = null;
5449
+ // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
5450
+ // moments later; at most one byte-identical replay is allowed per request.
5451
+ const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
5339
5452
  const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
5340
5453
  let rateLimitRetries = 0;
5341
5454
  const rebuildAndRefetch = async (
@@ -5350,11 +5463,13 @@ async function handleResponsesInner(
5350
5463
  return { failed: formatErrorResponse(502, "upstream_error", "Recovery changed the provider wire unexpectedly") };
5351
5464
  }
5352
5465
  try {
5353
- request = await retryAdapter.buildRequest(parsed, {
5354
- headers: selectedForwardHeaders,
5355
- translatorBudget,
5356
- });
5357
- refreshRoutedNamespaceToolAliases(request);
5466
+ if (recovery !== "console-go-upload-retry") {
5467
+ request = await retryAdapter.buildRequest(parsed, {
5468
+ headers: selectedForwardHeaders,
5469
+ translatorBudget,
5470
+ });
5471
+ }
5472
+ refreshRequestToolAliases(request);
5358
5473
  recordAdapterReasoning(logCtx, request);
5359
5474
  recordAdapterTier(logCtx, request);
5360
5475
  } catch (err) {
@@ -5396,10 +5511,7 @@ async function handleResponsesInner(
5396
5511
  ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
5397
5512
  }),
5398
5513
  route.provider.authMode === "forward")
5399
- .then(response => {
5400
- settleObservedHostResponse();
5401
- return response;
5402
- });
5514
+ .then(adoptObservedResponse);
5403
5515
  },
5404
5516
  { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
5405
5517
  );
@@ -5471,7 +5583,7 @@ async function handleResponsesInner(
5471
5583
  headers: selectedForwardHeaders,
5472
5584
  translatorBudget,
5473
5585
  });
5474
- refreshRoutedNamespaceToolAliases(request);
5586
+ refreshRequestToolAliases(request);
5475
5587
  recordAdapterReasoning(logCtx, request);
5476
5588
  recordAdapterTier(logCtx, request);
5477
5589
  refreshUndeclaredToolGuard(request);
@@ -5504,10 +5616,7 @@ async function handleResponsesInner(
5504
5616
  codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
5505
5617
  ),
5506
5618
  route.provider.authMode === "forward",
5507
- ).then(response => {
5508
- settleObservedHostResponse();
5509
- return response;
5510
- });
5619
+ ).then(adoptObservedResponse);
5511
5620
  } catch (err) {
5512
5621
  return transportFailureResponse(err);
5513
5622
  } finally {
@@ -5592,7 +5701,7 @@ async function handleResponsesInner(
5592
5701
  headers: selectedForwardHeaders,
5593
5702
  translatorBudget,
5594
5703
  });
5595
- refreshRoutedNamespaceToolAliases(request);
5704
+ refreshRequestToolAliases(request);
5596
5705
  recordAdapterReasoning(logCtx, request);
5597
5706
  recordAdapterTier(logCtx, request);
5598
5707
  } catch (err) {
@@ -5622,10 +5731,7 @@ async function handleResponsesInner(
5622
5731
  ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
5623
5732
  }),
5624
5733
  route.provider.authMode === "forward")
5625
- .then(res => {
5626
- settleObservedHostResponse();
5627
- return res;
5628
- });
5734
+ .then(adoptObservedResponse);
5629
5735
  },
5630
5736
  { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
5631
5737
  );
@@ -5722,10 +5828,7 @@ async function handleResponsesInner(
5722
5828
  ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
5723
5829
  }),
5724
5830
  route.provider.authMode === "forward")
5725
- .then(res => {
5726
- settleObservedHostResponse();
5727
- return res;
5728
- });
5831
+ .then(adoptObservedResponse);
5729
5832
  },
5730
5833
  { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
5731
5834
  );
@@ -5804,6 +5907,7 @@ async function handleResponsesInner(
5804
5907
  passthroughEstimate,
5805
5908
  stream: parsed.stream,
5806
5909
  onResponse: (response, retryAuthCtx, retryRequest) => {
5910
+ adoptCodexWsStage(response);
5807
5911
  captureAffinityResponse(
5808
5912
  response,
5809
5913
  retryAuthCtx,
@@ -5819,7 +5923,7 @@ async function handleResponsesInner(
5819
5923
  if (retry.kind === "retried") {
5820
5924
  authCtx = retry.authCtx;
5821
5925
  request = retry.request;
5822
- refreshRoutedNamespaceToolAliases(request);
5926
+ refreshRequestToolAliases(request);
5823
5927
  refreshUndeclaredToolGuard(request);
5824
5928
  upstreamResponse = retry.upstreamResponse;
5825
5929
  selectedForwardHeaders = retry.selectedForwardHeaders;
@@ -5887,9 +5991,68 @@ async function handleResponsesInner(
5887
5991
  logCtx.terminalIncompleteReason = preflightLog.terminalIncompleteReason;
5888
5992
  }
5889
5993
  }
5994
+ // Console Go (opencode-zen / opencode-go) intermittently rejects a body it accepts seconds
5995
+ // later with 400 invalid_request_error / "Invalid upload request." Replay the byte-identical
5996
+ // request once after the exact gateway rejection. Single-shot guard.
5997
+ // This recovery reuses the captured request; other recovery kinds still rebuild.
5998
+ if (!consoleGoUploadRetryGuard.attempted) {
5999
+ const uploadRejectionBody = await consoleGoUploadRejectionBody(
6000
+ upstreamResponse,
6001
+ consoleGoUploadRetryGuard.attempted,
6002
+ upstream.signal,
6003
+ );
6004
+ if (uploadRejectionBody !== undefined
6005
+ && isTransientConsoleGoUploadRejection({
6006
+ status: upstreamResponse.status,
6007
+ errorBody: uploadRejectionBody,
6008
+ outboundUrl: request.url,
6009
+ })) {
6010
+ consoleGoUploadRetryGuard.attempted = true;
6011
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
6012
+ if (!upstream.signal.aborted) {
6013
+ try {
6014
+ await sleepWithAbort(CONSOLE_GO_UPLOAD_RETRY_DELAY_MS, upstream.signal);
6015
+ } catch { return clientCancelledResponse(); }
6016
+ }
6017
+ if (upstream.signal.aborted) return clientCancelledResponse();
6018
+ const result = await rebuildAndRefetch("console-go-upload-retry");
6019
+ if ("failed" in result) return result.failed;
6020
+ upstreamResponse = result;
6021
+ continue passthroughRecovery;
6022
+ }
6023
+ }
6024
+ // Reasoning-effort downgrade: a rung the catalog still advertises can be refused upstream --
6025
+ // the metadata records the model's ladder, not this account's entitlement (a Muse Code
6026
+ // subscription gates max on muse-spark-1.3-contributor, for example). Learn the refusal so
6027
+ // later turns clamp before dispatch, then replay once at the next lower published rung
6028
+ // instead of failing the turn; requestedEffort/effectiveEffort keep both values in usage.
6029
+ if (!reasoningEffortDowngradeGuard.attempted) {
6030
+ const rejectionText = await reasoningEffortRejectionText(
6031
+ upstreamResponse,
6032
+ reasoningEffortDowngradeGuard.attempted,
6033
+ upstream.signal,
6034
+ );
6035
+ const downgrade = rejectionText === undefined
6036
+ ? undefined
6037
+ : planReasoningEffortDowngrade({
6038
+ provider: route.provider,
6039
+ modelId: parsed.modelId,
6040
+ requested: parsed.options.reasoning,
6041
+ rejectionText,
6042
+ });
6043
+ if (downgrade) {
6044
+ reasoningEffortDowngradeGuard.attempted = true;
6045
+ parsed.options.reasoning = downgrade.effort;
6046
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
6047
+ const result = await rebuildAndRefetch("reasoning-effort-downgrade");
6048
+ if ("failed" in result) return result.failed;
6049
+ upstreamResponse = result;
6050
+ continue passthroughRecovery;
6051
+ }
6052
+ }
5890
6053
  break;
5891
6054
  }
5892
- const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
6055
+ const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions);
5893
6056
  const resolvedModel = headers.get("openai-model")?.trim();
5894
6057
  if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
5895
6058
  if (isUsageDebugEnabled()) {
@@ -5900,7 +6063,7 @@ async function handleResponsesInner(
5900
6063
  // treating a successful body as SSE when the caller requested streaming.
5901
6064
  const passthroughCt = headers.get("content-type")?.toLowerCase();
5902
6065
  const isEventStream = passthroughCt?.includes("text/event-stream")
5903
- || (upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
6066
+ || (plaintextV2AgentMessageToolNames.size === 0 && upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
5904
6067
  const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
5905
6068
  config,
5906
6069
  authCtx,
@@ -5926,7 +6089,7 @@ async function handleResponsesInner(
5926
6089
  if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
5927
6090
  applyAccountQuotaFromUpstreamHeaders(authCtx.accountId, upstreamResponse.headers,
5928
6091
  authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined,
5929
- { modelId: route.modelId });
6092
+ { modelId: route.modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined });
5930
6093
  }
5931
6094
  if (terminalBodyWillRecord) {
5932
6095
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
@@ -5978,7 +6141,7 @@ async function handleResponsesInner(
5978
6141
  return new Response(upstreamResponse.body, {
5979
6142
  status: upstreamResponse.status,
5980
6143
  statusText: upstreamResponse.statusText,
5981
- headers: sanitizePassthroughHeaders(upstreamResponse.headers),
6144
+ headers: sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions),
5982
6145
  });
5983
6146
  }
5984
6147
  if (!upstreamResponse.ok) {
@@ -6040,6 +6203,8 @@ async function handleResponsesInner(
6040
6203
  isPassthrough: true,
6041
6204
  stream: parsed.stream === true,
6042
6205
  });
6206
+ // Capture the binding that actually served the first leg, after its permitted reselection.
6207
+ const webSearchBridgeBinding = requestBindings.get(request);
6043
6208
  // The bridge wraps the RAW upstream body, so terminal repair below still owns the single
6044
6209
  // client-facing terminal — the bridge drops the terminal of every intercepted leg.
6045
6210
  const upstreamSseBody = webSearchBridgePlan
@@ -6057,7 +6222,14 @@ async function handleResponsesInner(
6057
6222
  connectMs,
6058
6223
  true,
6059
6224
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
6060
- dispatchOverride: oauthDispatch(request),
6225
+ // Pacing can outlive a manual selection change. A continuation must retain the
6226
+ // first leg's key and appended search result, never rebuild from the original turn.
6227
+ beforeDispatch: () => {
6228
+ if (webSearchBridgeBinding?.kind !== "api-key"
6229
+ || !providerApiKeySelectionIsCurrent(config, route.providerName, webSearchBridgeBinding.provider)) {
6230
+ throw new Error("API key selection changed during a web-search continuation");
6231
+ }
6232
+ },
6061
6233
  providerName: route.providerName,
6062
6234
  modelId: route.modelId,
6063
6235
  }),
@@ -6100,6 +6272,9 @@ async function handleResponsesInner(
6100
6272
  createImageGenCallRestoreRewrite(imageGenCallAliases),
6101
6273
  // #3217: a call whose namespace repeats its own name is unroutable in codex-rs.
6102
6274
  createSelfNamedToolCallNamespaceScrubRewrite(selfNamedNamespaceScrubAuthorization),
6275
+ routedMuseToolNameAliases.size > 0
6276
+ ? createMuseToolNameRestoreRewrite(routedMuseToolNameAliases)
6277
+ : undefined,
6103
6278
  routedNamespaceToolAliases.size > 0
6104
6279
  ? createRoutedNamespaceCallRestoreRewrite(routedNamespaceToolAliases)
6105
6280
  : undefined,
@@ -6110,15 +6285,23 @@ async function handleResponsesInner(
6110
6285
  ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
6111
6286
  : undefined,
6112
6287
  responseModelRewrite,
6113
- parsed.options.hideThinkingSummary !== true
6114
- && routeUsesContentChannelReasoning(route.provider, route.modelId)
6115
- ? createReasoningSummaryChannelPayloadRewrite()
6116
- : undefined,
6117
6288
  ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
6118
6289
  // #893: sparse-snapshot gateways get field backfills AND lifecycle event
6119
6290
  // injection at the block level, after payload rewrites. Defaults come
6120
6291
  // from the finalized OUTBOUND body — the normalized internal tool shapes
6121
6292
  // are not the Responses wire shapes the snapshot must mirror.
6293
+ // Only validated client blocks may publish plaintext continuation state.
6294
+ // Raw inspection precedes rewriting on eager relays, so it cannot own this write.
6295
+ const plaintextInspector = plaintextV2AgentMessageToolNames.size > 0
6296
+ ? createSseInspector({ onCompletedResponse: rememberPassthroughResponseChecked })
6297
+ : undefined;
6298
+ const plaintextEncoder = plaintextInspector ? new TextEncoder() : undefined;
6299
+ const rememberPlaintextBlock = plaintextInspector
6300
+ ? Object.assign((block: string): readonly string[] => {
6301
+ plaintextInspector.feed(plaintextEncoder!.encode(`${block}\n\n`));
6302
+ return [block];
6303
+ }, { dispose: () => plaintextInspector.dispose() })
6304
+ : undefined;
6122
6305
  const blockRewrites = [
6123
6306
  payloadRewrites.length > 0
6124
6307
  ? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
@@ -6146,6 +6329,11 @@ async function handleResponsesInner(
6146
6329
  snapshotRepairEnabled
6147
6330
  ? createResponsesSnapshotBlockRewrite(outboundRequestBody, translatorBudget)
6148
6331
  : undefined,
6332
+ plaintextV2AgentMessageToolNames.size > 0
6333
+ ? payloadRewriteAsBlockRewrite(createPlaintextV2AgentMessageCallRestoreRewrite(
6334
+ plaintextV2AgentMessageToolNames, plaintextV2AgentMessageAliasedToolNames,
6335
+ ))
6336
+ : undefined,
6149
6337
  createResponsesFieldBackfillBlockRewrite(),
6150
6338
  functionRepairSchemas.size > 0
6151
6339
  ? createResponsesFunctionToolRepairBlockRewrite(functionRepairSchemas, translatorBudget)
@@ -6160,6 +6348,7 @@ async function handleResponsesInner(
6160
6348
  declaredBareWireToolNames,
6161
6349
  )
6162
6350
  : undefined,
6351
+ rememberPlaintextBlock,
6163
6352
  ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
6164
6353
  const clientBlockRewrite = blockRewrites.length > 0
6165
6354
  ? composeSseBlockRewrites(...blockRewrites)
@@ -6207,7 +6396,7 @@ async function handleResponsesInner(
6207
6396
  const inspector = createSseInspector({
6208
6397
  onTerminal: reportNativeTerminal,
6209
6398
  logCtx,
6210
- onCompletedResponse: rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined,
6399
+ onCompletedResponse: rememberPassthroughResponse && plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
6211
6400
  onParsedPayload: noteInspectedPayload,
6212
6401
  onFirstOutput: options.onFirstOutput,
6213
6402
  pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
@@ -6244,6 +6433,7 @@ async function handleResponsesInner(
6244
6433
  onDone: () => unregisterTurn(turnAc),
6245
6434
  }, {
6246
6435
  clientGoneSignal: options.abortSignal,
6436
+ terminalBoundary: codexSafetyBufferingOptions,
6247
6437
  ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
6248
6438
  ...(logCtx.upstreamError === undefined ? {} : { upstreamError: logCtx.upstreamError }),
6249
6439
  });
@@ -6306,7 +6496,7 @@ async function handleResponsesInner(
6306
6496
  responseCompletionCancelled = true;
6307
6497
  options.onNativePassthroughCancel?.();
6308
6498
  },
6309
- rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined,
6499
+ rememberPassthroughResponse && plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
6310
6500
  options.onFirstOutput,
6311
6501
  inspectionConsumerOptions,
6312
6502
  );
@@ -6316,7 +6506,7 @@ async function handleResponsesInner(
6316
6506
  logCtx,
6317
6507
  turnAc.signal,
6318
6508
  () => unregisterTurn(turnAc),
6319
- rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined,
6509
+ rememberPassthroughResponse && plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
6320
6510
  options.onFirstOutput,
6321
6511
  inspectionConsumerOptions,
6322
6512
  );
@@ -6335,7 +6525,7 @@ async function handleResponsesInner(
6335
6525
  responseCompletionCancelled = true;
6336
6526
  clientGone.abort(reason);
6337
6527
  },
6338
- { upstreamError: logCtx.upstreamError },
6528
+ { upstreamError: logCtx.upstreamError, terminalBoundary: codexSafetyBufferingOptions },
6339
6529
  );
6340
6530
  return markNativePassthroughSseResponse(new Response(clientBody, {
6341
6531
  status: upstreamResponse.status,
@@ -6358,10 +6548,14 @@ async function handleResponsesInner(
6358
6548
  }
6359
6549
  const text = bounded.text;
6360
6550
  inspectResponseLogJson(logCtx, text);
6551
+ let plaintextV2RestoreFailed = false;
6361
6552
  let clientJson = (() => {
6362
6553
  const restoredNamespace = restoreRoutedNamespaceCallsInJson(
6363
6554
  scrubSelfNamedToolCallNamespaceInJson(
6364
- restoreImageGenCallsInJson(text, imageGenCallAliases),
6555
+ restoreMuseToolNamesInJson(
6556
+ restoreImageGenCallsInJson(text, imageGenCallAliases),
6557
+ routedMuseToolNameAliases,
6558
+ ),
6365
6559
  selfNamedNamespaceScrubAuthorization,
6366
6560
  ),
6367
6561
  routedNamespaceToolAliases,
@@ -6380,18 +6574,20 @@ async function handleResponsesInner(
6380
6574
  restored,
6381
6575
  routedToolSearchNames,
6382
6576
  );
6383
- const repaired = normalizeFunctionCompletionJson(restoredToolSearch);
6577
+ const normalizedJson = normalizeFunctionCompletionJson(restoredToolSearch);
6578
+ const plaintextRestore = restorePlaintextV2AgentMessageCallsInJsonResult(
6579
+ normalizedJson, plaintextV2AgentMessageToolNames, plaintextV2AgentMessageAliasedToolNames,
6580
+ );
6581
+ plaintextV2RestoreFailed = plaintextRestore.overflowed;
6582
+ const repaired = plaintextRestore.value;
6384
6583
  const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
6385
6584
  ? rewriteResponsesModelJson(repaired, parsed._responseModelId)
6386
6585
  : repaired;
6387
- // The bounded-JSON answer bypasses the SSE payload rewrite, so content-
6388
- // channel reasoning needs the same normalization here for the plain
6389
- // JSON answer and every reframed-SSE variant built from clientJson.
6390
- return parsed.options.hideThinkingSummary !== true
6391
- && routeUsesContentChannelReasoning(route.provider, route.modelId)
6392
- ? rewriteReasoningSummaryInJsonString(modelRewritten)
6393
- : modelRewritten;
6586
+ return modelRewritten;
6394
6587
  })();
6588
+ if (plaintextV2RestoreFailed) {
6589
+ return formatErrorResponse(502, "upstream_error", PLAINTEXT_V2_AGENT_MESSAGE_RESTORE_OVERFLOW_MESSAGE);
6590
+ }
6395
6591
  // #1700: same fail-closed policy as the SSE relay above. Both the plain JSON answer and
6396
6592
  // the reframed-SSE branch below are built from this body, so one check covers them. This
6397
6593
  // runs BEFORE the continuation cache write below: a refused turn must not become state a
@@ -6466,7 +6662,7 @@ async function handleResponsesInner(
6466
6662
  }
6467
6663
  throw error;
6468
6664
  }
6469
- const sseHeaders = sanitizePassthroughHeaders(headers);
6665
+ const sseHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions);
6470
6666
  sseHeaders.set("content-type", "text/event-stream");
6471
6667
  sseHeaders.set("cache-control", "no-store");
6472
6668
  return new Response(stream, {
@@ -6499,6 +6695,10 @@ async function handleResponsesInner(
6499
6695
  headers,
6500
6696
  });
6501
6697
  }
6698
+ if (plaintextV2AgentMessageToolNames.size > 0) {
6699
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
6700
+ return formatErrorResponse(502, "upstream_error", "plaintext V2 agent-message response used an unsupported content type");
6701
+ }
6502
6702
  // An unclassified passthrough body is relayed directly and has no bounded completion observer;
6503
6703
  // use the same non-error-status success boundary as SSE instead of retaining per-stream state.
6504
6704
  commitReasoningReplayServingRoute(request.headers);
@@ -7267,7 +7467,7 @@ async function handleResponsesInner(
7267
7467
  Math.max(1, budget - transientSendsUsed);
7268
7468
  try {
7269
7469
  initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
7270
- refreshRoutedNamespaceToolAliases(initialRequest);
7470
+ refreshRequestToolAliases(initialRequest);
7271
7471
  recordAdapterReasoning(logCtx, initialRequest);
7272
7472
  recordAdapterTier(logCtx, initialRequest);
7273
7473
  inputTokenEstimate = typeof initialRequest.usageLog?.inputTokens === "number"
@@ -7376,7 +7576,14 @@ async function handleResponsesInner(
7376
7576
  // 413→429 rotation cannot silently undo the tightening.
7377
7577
  let imageRetryAttempted = false;
7378
7578
  const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
7579
+ // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
7580
+ // moments later; at most one byte-identical replay is allowed per request.
7581
+ const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
7379
7582
  let oauth401ReplayAttempted = false;
7583
+ // At most one reasoning-effort downgrade per request. This sits outside the recovery loop
7584
+ // below for the same reason the two guards above do: a guard declared inside it is reset by
7585
+ // every `continue recovery`, which would let one turn walk the whole ladder down.
7586
+ const reasoningEffortDowngradeGuard: { attempted: boolean } = { attempted: false };
7380
7587
  /**
7381
7588
  * Rebuild the request from the current parsed input (and any image-tier bias) and refetch
7382
7589
  * it once, tagging the attempt with the given recovery kind. Rebuilds are deterministic
@@ -7412,7 +7619,7 @@ async function handleResponsesInner(
7412
7619
  sameTargetParsed = parsed;
7413
7620
  sameTargetToken = transportToken;
7414
7621
  }
7415
- refreshRoutedNamespaceToolAliases(retryRequest);
7622
+ refreshRequestToolAliases(retryRequest);
7416
7623
  const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
7417
7624
  ? retryRequest.usageLog.inputTokens
7418
7625
  : undefined;
@@ -7760,6 +7967,63 @@ async function handleResponsesInner(
7760
7967
  upstreamResponse = result;
7761
7968
  continue recovery;
7762
7969
  }
7970
+ // Console Go (opencode-zen / opencode-go) intermittently rejects a body it accepts seconds
7971
+ // later with 400 invalid_request_error / "Invalid upload request." Replay the
7972
+ // byte-identical request once after the exact gateway rejection.
7973
+ if (!consoleGoUploadRetryGuard.attempted) {
7974
+ const uploadRejectionBody = await consoleGoUploadRejectionBody(
7975
+ upstreamResponse,
7976
+ consoleGoUploadRetryGuard.attempted,
7977
+ upstream.signal,
7978
+ );
7979
+ if (uploadRejectionBody !== undefined
7980
+ && isTransientConsoleGoUploadRejection({
7981
+ status: upstreamResponse.status,
7982
+ errorBody: uploadRejectionBody,
7983
+ outboundUrl: sameTargetRequest?.url,
7984
+ })) {
7985
+ consoleGoUploadRetryGuard.attempted = true;
7986
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
7987
+ if (!upstream.signal.aborted) {
7988
+ try {
7989
+ await sleepWithAbort(CONSOLE_GO_UPLOAD_RETRY_DELAY_MS, upstream.signal);
7990
+ } catch { cleanupUpstreamAbort(); return clientCancelledResponse(); }
7991
+ }
7992
+ if (upstream.signal.aborted) { cleanupUpstreamAbort(); return clientCancelledResponse(); }
7993
+ const result = await rebuildAndRefetch("console-go-upload-retry");
7994
+ if ("failed" in result) return result.failed;
7995
+ upstreamResponse = result;
7996
+ continue recovery;
7997
+ }
7998
+ }
7999
+ // Reasoning-effort downgrade, mirroring the passthroughRecovery loop above: learn the
8000
+ // refused rung, then replay once at the next published one.
8001
+ if (!reasoningEffortDowngradeGuard.attempted) {
8002
+ const rejectionText = await reasoningEffortRejectionText(
8003
+ upstreamResponse,
8004
+ reasoningEffortDowngradeGuard.attempted,
8005
+ upstream.signal,
8006
+ );
8007
+ const downgrade = rejectionText === undefined
8008
+ ? undefined
8009
+ : planReasoningEffortDowngrade({
8010
+ provider: route.provider,
8011
+ modelId: parsed.modelId,
8012
+ requested: parsed.options.reasoning,
8013
+ rejectionText,
8014
+ });
8015
+ if (downgrade) {
8016
+ reasoningEffortDowngradeGuard.attempted = true;
8017
+ parsed.options.reasoning = downgrade.effort;
8018
+ // The same-target cache keys on parsed identity, so a mutated effort needs a token bump.
8019
+ invalidateSameTargetRequest();
8020
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
8021
+ const result = await rebuildAndRefetch("reasoning-effort-downgrade");
8022
+ if ("failed" in result) return result.failed;
8023
+ upstreamResponse = result;
8024
+ continue recovery;
8025
+ }
8026
+ }
7763
8027
  break;
7764
8028
  }
7765
8029
  if (!upstreamResponse.ok) {