@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -13,6 +13,9 @@ import {
13
13
  } from "./outbound-body-guard";
14
14
  import { nativeContextLimits } from "../../codex/catalog";
15
15
  import { describeUpstreamConnectFailure } from "./upstream-error";
16
+ import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
17
+ import { applyAccountQuotaFromUpstreamHeaders as applyCapturedCodexQuota } from "../../codex/quota";
18
+ import { isCodexWsQuotaObservedResponse } from "./ws-upstream";
16
19
  import {
17
20
  multiAgentGuidanceEnabled,
18
21
  resolveEnvValue,
@@ -67,6 +70,7 @@ import { evidenceFromBody } from "../../routing/request-evidence";
67
70
  import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
68
71
  import {
69
72
  advanceComboAfterFailure,
73
+ comboCooldownRetryAfterSeconds,
70
74
  comboDefaultEffort,
71
75
  comboFailureCooldownScope,
72
76
  comboFailureDecision,
@@ -75,11 +79,11 @@ import {
75
79
  concreteComboRequestBody,
76
80
  getCombo,
77
81
  isComboTargetInCooldown,
78
- comboCooldownRetryAfterSeconds,
79
82
  NoAvailableComboTargetsError,
80
83
  noteComboSuccess,
81
84
  parseRetryAfterMs,
82
85
  pickComboTarget,
86
+ pickComboTargetWithWait,
83
87
  targetKey,
84
88
  } from "../../combos";
85
89
  import { isInjectionDebugEnabled } from "../../lib/debug-settings";
@@ -122,6 +126,7 @@ import {
122
126
  getAnthropicPoolAccessToken,
123
127
  getAnthropicPoolRetryAfterSeconds,
124
128
  isAnthropicAccountPoolEnabled,
129
+ hasAnthropicFailoverQuorum,
125
130
  promoteAnthropicActiveAccount,
126
131
  resolveAnthropicAccountForSession,
127
132
  rotateAnthropicAccountOn429,
@@ -143,6 +148,8 @@ import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenA
143
148
  import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
144
149
  import {
145
150
  applyCodexAuthContextToProvider,
151
+ createCodexReserveDispatchGuard,
152
+ unwrapUpstreamRetryEvidenceError,
146
153
  codexPoolAffinityKey,
147
154
  CodexAccountCooldownError,
148
155
  CodexAuthContextError,
@@ -158,6 +165,7 @@ import {
158
165
  releaseCodexAuthContextProbeLease,
159
166
  stripCodexRuntimeProviderFields,
160
167
  type CodexAuthContext,
168
+ type CodexAuthPolicyConfig,
161
169
  } from "../../codex/auth-context";
162
170
  import {
163
171
  entitledCodexAccountIdsForModel,
@@ -201,6 +209,7 @@ import type { DataPlaneAdmission } from "../auth-cors";
201
209
  import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
202
210
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
203
211
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
212
+ import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported } from "../../codex/loopback-target";
204
213
  import { providerContextCap } from "../../providers/context-cap";
205
214
  import {
206
215
  fastPolicyForModel,
@@ -237,10 +246,12 @@ import {
237
246
  rateLimitRetryDelayMs,
238
247
  rateLimitRetryPolicyFor,
239
248
  rotateProviderTransportOn429,
249
+ rotateProviderTransportOn401,
240
250
  transientRetryPolicyFor,
241
251
  } from "../../providers/key-failover";
242
252
  import { shouldAttemptImageTierRetry } from "../image-retry";
243
253
  import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
254
+ import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
244
255
  import type { WsData } from "../ws-bridge";
245
256
  import {
246
257
  codexAccountSelectionForTurn,
@@ -287,6 +298,7 @@ import {
287
298
  conversationIdFromResponsesRequest,
288
299
  normalizeLogConversationId,
289
300
  reasoningReplayConversationIdFromResponsesRequest,
301
+ sessionLaneIdFromRequest,
290
302
  sessionIdHeaderFromRequest,
291
303
  } from "../request-log-conversation";
292
304
  import type { AttemptRecoveryKind } from "../../usage/log";
@@ -332,6 +344,7 @@ import {
332
344
  } from "../responses-image-gen-repair";
333
345
  import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
334
346
  import { parseRequestEffortRowId } from "../effort-row";
347
+ import { parseSyntheticRowId } from "../fast-row";
335
348
  import {
336
349
  collectSelfNamedNamespaceScrubAuthorization,
337
350
  createSelfNamedToolCallNamespaceScrubRewrite,
@@ -861,6 +874,13 @@ export function usesCodexForwardPoolAuth(
861
874
  && provider.authMode === "forward" && provider.adapter === "openai-responses";
862
875
  }
863
876
 
877
+ function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig): CodexWsQuotaObserver | undefined {
878
+ if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
879
+ const { accountId, writerGeneration } = authCtx;
880
+ const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
881
+ return headers => applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter);
882
+ }
883
+
864
884
  export function preAuthUpstreamHostCircuitKey(
865
885
  route: Pick<RouteResult, "provider" | "providerName" | "codexAccountMode" | "codexAccountId">,
866
886
  config: OcxConfig,
@@ -968,6 +988,9 @@ interface CodexPoolAccountRetryArgs {
968
988
  parsed: OcxParsedRequest;
969
989
  logCtx: RequestLogContext;
970
990
  options: {
991
+ admission?: DataPlaneAdmission;
992
+ codexAuthPolicy?: CodexAuthPolicyConfig;
993
+ visionDescribeTerminal?: boolean;
971
994
  abortSignal?: AbortSignal;
972
995
  onCodexAuthContextResolved?: (ctx: CodexAuthContext) => void;
973
996
  deferCodexResetDerivedCooldown?: boolean;
@@ -1164,6 +1187,8 @@ async function retryCodexPoolOnAlternateAccount(
1164
1187
  "pool",
1165
1188
  {
1166
1189
  excludeAccountId: firstAuthCtx.accountId,
1190
+ admission: options.admission,
1191
+ codexAuthPolicy: options.codexAuthPolicy,
1167
1192
  modelId: route.modelId,
1168
1193
  requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
1169
1194
  beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
@@ -1213,6 +1238,7 @@ async function retryCodexPoolOnAlternateAccount(
1213
1238
  firstAuthCtx.accountId,
1214
1239
  firstResponse.headers,
1215
1240
  firstAuthCtx.writerGeneration,
1241
+ firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
1216
1242
  );
1217
1243
  }
1218
1244
  const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
@@ -1234,7 +1260,7 @@ async function retryCodexPoolOnAlternateAccount(
1234
1260
  // Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
1235
1261
  // ordinary requests must block the first account before the alternate send.
1236
1262
  if (!deferFirstOutcome) recordFirstOutcome();
1237
- const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx);
1263
+ const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
1238
1264
  const retryProvider = applyCodexAuthContextToProvider(
1239
1265
  stripCodexRuntimeProviderFields(route.provider),
1240
1266
  retryAuthCtx,
@@ -1303,6 +1329,9 @@ async function retryCodexPoolOnAlternateAccount(
1303
1329
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1304
1330
  providerName: route.providerName,
1305
1331
  modelId: route.modelId,
1332
+ onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider),
1333
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1334
+ ? createCodexReserveDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1306
1335
  }),
1307
1336
  // Credential-bearing forward send: never follow a redirect into a
1308
1337
  // dead-host rejection after the credential was seen (#914).
@@ -1472,6 +1501,8 @@ export interface ConsumedComboFailure {
1472
1501
  upstreamCode?: string;
1473
1502
  /** Valid numeric/date value used only for cooldown calculation. */
1474
1503
  retryAfter?: string;
1504
+ /** Upstream Codex quota-window reset timestamps used for combo cooldowns. */
1505
+ resetAt?: string[];
1475
1506
  /** Reserved for 040 usage attribution without adding another body read. */
1476
1507
  usage?: OcxUsage;
1477
1508
  }
@@ -1479,6 +1510,8 @@ export interface ConsumedComboFailure {
1479
1510
 
1480
1511
 
1481
1512
  export interface HandleResponsesOptions {
1513
+ /** Original live policy owner; separate from caller-specific routing/sidecar snapshots. */
1514
+ codexAuthPolicy?: CodexAuthPolicyConfig;
1482
1515
  turnAdmissionLease?: AdmissionLease;
1483
1516
  /**
1484
1517
  * How the caller proved data-plane admission (#1686).
@@ -1588,9 +1621,26 @@ export async function consumeComboFailure(
1588
1621
  let upstreamCode: string | undefined;
1589
1622
  let upstreamMessage: string | undefined;
1590
1623
  let upstreamType: string | undefined;
1624
+ // Whether the body itself confirms a quota/rate-limit refusal, computed on the SAME read as
1625
+ // the classification below. `shouldRetryCodexPoolAccountQuota` cannot be called here without
1626
+ // a second body read, so this mirrors its normalization: raw 402/429, or a 5xx whose intact,
1627
+ // display-safe body carries a recognized quota message.
1628
+ let quotaConfirmedByBody = false;
1591
1629
  try {
1592
- const body = await readBoundedResponseBody(response, { signal });
1630
+ const body = await readBoundedResponseBody(response, {
1631
+ signal,
1632
+ // Match shouldRetryCodexPoolAccountQuota before treating a 5xx body as quota evidence.
1633
+ fatalUtf8: response.status >= 500 && response.status < 600,
1634
+ });
1593
1635
  usage = usageFromComboFailureText(body.text);
1636
+ if (
1637
+ response.status >= 500 && response.status < 600
1638
+ && body.displaySafe && !body.truncated
1639
+ ) {
1640
+ const quotaMessage = codexQuotaFailureMessage(body.text);
1641
+ quotaConfirmedByBody = quotaMessage !== undefined
1642
+ && isRateLimitOrQuotaFailureMessage(quotaMessage);
1643
+ }
1594
1644
  if (body.displaySafe) {
1595
1645
  const normalized = normalizeUpstreamErrorText(body.text, fallback);
1596
1646
  classificationText = normalized.safeText;
@@ -1611,18 +1661,24 @@ export async function consumeComboFailure(
1611
1661
  ? fallback
1612
1662
  : `${fallback}: ${classificationText}`;
1613
1663
  const upstreamRetryAfter = response.headers.get("retry-after");
1664
+ // Past HTTP dates are an immediate retry directive, just like the numeric value zero.
1665
+ // Normalize before the client helper discards them and substitutes a default delay.
1666
+ const effectiveRetryAfter = parseRetryAfterMs(upstreamRetryAfter, now) === undefined
1667
+ && parseRetryAfterMs(upstreamRetryAfter, now, { preserveImmediate: true }) !== undefined
1668
+ ? "0"
1669
+ : upstreamRetryAfter;
1614
1670
  // Client response may get the synthetic "2" fallback; cooldown metadata must not —
1615
1671
  // otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
1616
1672
  const clientRetryAfter = resolveClientRetryAfter({
1617
1673
  status: response.status,
1618
1674
  message,
1619
- upstreamRetryAfter,
1675
+ upstreamRetryAfter: effectiveRetryAfter,
1620
1676
  now,
1621
1677
  });
1622
1678
  const cooldownRetryAfter = resolveClientRetryAfter({
1623
1679
  status: response.status,
1624
1680
  message,
1625
- upstreamRetryAfter,
1681
+ upstreamRetryAfter: effectiveRetryAfter,
1626
1682
  now,
1627
1683
  includeDefault: false,
1628
1684
  });
@@ -1639,6 +1695,14 @@ export async function consumeComboFailure(
1639
1695
  classificationText,
1640
1696
  ...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
1641
1697
  ...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
1698
+ // The EFFECTIVE classification decides, not the raw status. An upstream that wraps a quota
1699
+ // refusal in a 5xx still carries `x-codex-*-reset-at`, and gating on 402/429 alone threw
1700
+ // those away, so the combo target came back up immediately instead of waiting for the
1701
+ // window it was told about. `cyberFailure` stays excluded: a policy block is not a quota.
1702
+ ...(!cyberFailure
1703
+ && (response.status === 429 || response.status === 402 || quotaConfirmedByBody)
1704
+ ? { resetAt: codexQuotaOutcomeMeta(response).resetAt }
1705
+ : {}),
1642
1706
  ...(usage ? { usage } : {}),
1643
1707
  };
1644
1708
  }
@@ -1754,6 +1818,31 @@ function unreadableEncryptedAgentTaskResponse(): Response {
1754
1818
  );
1755
1819
  }
1756
1820
 
1821
+ /**
1822
+ * Keep this trust boundary deliberately narrow: only a key-auth Responses route may consume
1823
+ * opaque child-task ciphertext, and the model's final wire override must still be Responses.
1824
+ * Callers keep combo attempts on their existing native-only recovery/fail-closed behavior.
1825
+ */
1826
+ function canPassThroughEncryptedV2AgentTask(
1827
+ route: RouteResult,
1828
+ inboundWire: InboundWire,
1829
+ ): boolean {
1830
+ if (route.combo !== undefined) return false;
1831
+ const provider = route.provider;
1832
+ if (
1833
+ inboundWire !== "responses"
1834
+ || provider.allowEncryptedV2AgentTasks !== true
1835
+ || (provider.authMode ?? "key") !== "key"
1836
+ ) return false;
1837
+
1838
+ return resolveWireProtocolOverride(
1839
+ route.providerName,
1840
+ route.modelId,
1841
+ provider,
1842
+ inboundWire,
1843
+ ).adapter === "openai-responses";
1844
+ }
1845
+
1757
1846
  type ResponsesAuthResolution =
1758
1847
  | { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean }
1759
1848
  | { ok: false; response: Response };
@@ -1801,6 +1890,8 @@ async function resolveResponsesCodexAuth(
1801
1890
  let authCtx: CodexAuthContext;
1802
1891
  if (route.codexAccountMode) {
1803
1892
  authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
1893
+ admission: options.admission,
1894
+ codexAuthPolicy: options.codexAuthPolicy,
1804
1895
  accountId: route.codexAccountId,
1805
1896
  modelId: route.modelId,
1806
1897
  substituteMainCredentialForDirect: substituteMainCredential,
@@ -1829,6 +1920,23 @@ async function resolveResponsesCodexAuth(
1829
1920
  authCtx = { kind: "main", accountId: null };
1830
1921
  options.onCodexAuthContextResolved?.(undefined);
1831
1922
  }
1923
+ // This resolver also builds a synthetic main context for unrelated keyed routes. Only
1924
+ // the actual Codex-forward transport consumes main quota; provider names are not proof
1925
+ // (custom-named canonical-forward providers must retain the same protection).
1926
+ const mainPolicyConfig = isCanonicalOpenAiForwardProvider(route.provider)
1927
+ ? options.codexAuthPolicy ?? config : undefined;
1928
+ const headers = await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
1929
+ admission: options.admission,
1930
+ config: mainPolicyConfig,
1931
+ modelId: route.modelId,
1932
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
1933
+ substituteMainCredential,
1934
+ signal: options.abortSignal,
1935
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
1936
+ });
1937
+ // Awaiting even a cached materialization yields. Preserve the policy error if the live
1938
+ // quota/config changed during that yield, before usability could mislabel it as reauth.
1939
+ headersForCodexAuthContext(headers, authCtx, mainPolicyConfig, route.modelId, options.admission);
1832
1940
  if (!isCodexAuthContextUsable(authCtx, config)) {
1833
1941
  releaseCodexAuthContextProbeLease(authCtx);
1834
1942
  return {
@@ -1839,11 +1947,7 @@ async function resolveResponsesCodexAuth(
1839
1947
  return {
1840
1948
  ok: true,
1841
1949
  authCtx,
1842
- headers: await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
1843
- substituteMainCredential,
1844
- signal: options.abortSignal,
1845
- nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
1846
- }),
1950
+ headers,
1847
1951
  substituteMainCredential,
1848
1952
  };
1849
1953
  } catch (err) {
@@ -1886,6 +1990,7 @@ function isTerminalPoolRefreshFailure(error: unknown): boolean {
1886
1990
  */
1887
1991
  async function refreshPoolForwardAuth(args: {
1888
1992
  req: Request;
1993
+ config: OcxConfig;
1889
1994
  route: RouteResult;
1890
1995
  authCtx: CodexAuthContext & { kind: "pool" };
1891
1996
  substituteMainCredential: boolean;
@@ -1894,7 +1999,7 @@ async function refreshPoolForwardAuth(args: {
1894
1999
  | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
1895
2000
  | { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
1896
2001
  > {
1897
- const { req, route, authCtx, substituteMainCredential, options } = args;
2002
+ const { req, config, route, authCtx, substituteMainCredential, options } = args;
1898
2003
  try {
1899
2004
  const refreshed = await forceRefreshCodexPoolToken(authCtx.accountId, {
1900
2005
  rejectedGeneration: authCtx.generation,
@@ -1932,6 +2037,9 @@ async function refreshPoolForwardAuth(args: {
1932
2037
  route.codexAccountMode,
1933
2038
  );
1934
2039
  const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
2040
+ admission: options.admission,
2041
+ config: options.codexAuthPolicy ?? config,
2042
+ modelId: route.modelId,
1935
2043
  substituteMainCredential,
1936
2044
  signal: options.abortSignal,
1937
2045
  nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
@@ -1958,6 +2066,7 @@ async function refreshPoolForwardAuth(args: {
1958
2066
 
1959
2067
  async function refreshNativeMainForwardAuth(args: {
1960
2068
  req: Request;
2069
+ config: OcxConfig;
1961
2070
  route: RouteResult;
1962
2071
  authCtx: CodexAuthContext;
1963
2072
  substituteMainCredential: boolean;
@@ -1966,7 +2075,7 @@ async function refreshNativeMainForwardAuth(args: {
1966
2075
  | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
1967
2076
  | { ok: false; response: Response }
1968
2077
  > {
1969
- const { req, route, authCtx, substituteMainCredential, options } = args;
2078
+ const { req, config, route, authCtx, substituteMainCredential, options } = args;
1970
2079
  if (authCtx.kind !== "main-pool") {
1971
2080
  return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
1972
2081
  }
@@ -1989,6 +2098,9 @@ async function refreshNativeMainForwardAuth(args: {
1989
2098
  route.codexAccountMode,
1990
2099
  );
1991
2100
  const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
2101
+ admission: options.admission,
2102
+ config: options.codexAuthPolicy ?? config,
2103
+ modelId: route.modelId,
1992
2104
  substituteMainCredential,
1993
2105
  signal: options.abortSignal,
1994
2106
  nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
@@ -1998,7 +2110,9 @@ async function refreshNativeMainForwardAuth(args: {
1998
2110
  if (options.abortSignal?.aborted || req.signal.aborted) {
1999
2111
  return { ok: false, response: clientCancelledResponse() };
2000
2112
  }
2001
- return { ok: false, response: nativeMainRefreshFailureResponse(error) };
2113
+ return { ok: false, response: mapCodexAuthContextErrorToResponse(error, {
2114
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
2115
+ }) ?? nativeMainRefreshFailureResponse(error) };
2002
2116
  }
2003
2117
  }
2004
2118
 
@@ -2059,6 +2173,7 @@ async function applyFinalRouteRequestNormalization(args: {
2059
2173
 
2060
2174
  // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
2061
2175
  // this request will actually use (#404).
2176
+ route.provider = resolveOpenCodeGoTransport(route.provider, sessionLaneIdFromRequest(req.headers));
2062
2177
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
2063
2178
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
2064
2179
  logCtx.model = route.modelId;
@@ -2290,6 +2405,15 @@ export async function handleComboResponses(
2290
2405
  comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
2291
2406
  const initialNow = Date.now();
2292
2407
  let pick: ReturnType<typeof pickComboTarget> = null;
2408
+ const pickWithWait = (pickOptions: {
2409
+ exclude?: Iterable<string>;
2410
+ eligible?: (target: NonNullable<typeof combo>["targets"][number]) => boolean;
2411
+ now?: number;
2412
+ }) => pickComboTargetWithWait(config, comboId, {
2413
+ ...pickOptions,
2414
+ waitForCooldownMs: combo.waitForCooldownMs,
2415
+ abortSignal: options.abortSignal,
2416
+ });
2293
2417
 
2294
2418
  if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
2295
2419
  const recovery = agentTaskRecoveryConfig(config);
@@ -2307,9 +2431,7 @@ export async function handleComboResponses(
2307
2431
  );
2308
2432
  return unreadableEncryptedAgentTaskResponse();
2309
2433
  }
2310
- pick = pickComboTarget(config, comboId, {
2311
- eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
2312
- });
2434
+ pick = await pickWithWait({ now: initialNow });
2313
2435
  if (!pick) {
2314
2436
  discardEncryptedAgentTaskRecovery(
2315
2437
  req,
@@ -2317,7 +2439,9 @@ export async function handleComboResponses(
2317
2439
  config,
2318
2440
  { parentThreadId: inboundClientThreadId },
2319
2441
  );
2320
- return comboUnavailable(comboId);
2442
+ return options.abortSignal?.aborted
2443
+ ? clientCancelledResponse()
2444
+ : comboUnavailable(comboId);
2321
2445
  }
2322
2446
  let recovered = false;
2323
2447
  try {
@@ -2347,14 +2471,16 @@ export async function handleComboResponses(
2347
2471
  comboPayloadReadable = true;
2348
2472
  comboReplaySnapshot.recoveredPlaintext = true;
2349
2473
  } else {
2350
- pick = pickComboTarget(config, comboId, {
2351
- eligible: target => payloadEligible(target)
2352
- && !isComboTargetInCooldown(comboId, target, initialNow),
2474
+ pick = await pickWithWait({
2475
+ eligible: payloadEligible,
2476
+ now: initialNow,
2353
2477
  });
2354
2478
  }
2355
2479
 
2356
2480
  if (!pick) {
2357
- return comboUnavailable(comboId);
2481
+ return options.abortSignal?.aborted
2482
+ ? clientCancelledResponse()
2483
+ : comboUnavailable(comboId);
2358
2484
  }
2359
2485
  // One immutable combo selection trace, before any child dispatch; child
2360
2486
  // adoption below must never replace it with a concrete child route trace.
@@ -2558,9 +2684,12 @@ export async function handleComboResponses(
2558
2684
  console.warn(
2559
2685
  `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
2560
2686
  );
2687
+ const failureNow = Date.now();
2561
2688
  const nextPick = advanceComboAfterFailure(config, pick, {
2562
2689
  retryAfter: failure.retryAfter,
2563
- now: Date.now(),
2690
+ resetAt: failure.resetAt,
2691
+ cooldownMs: combo.cooldownMs,
2692
+ now: failureNow,
2564
2693
  cooldownScope: comboFailureCooldownScope(failure.response.status, failure.classificationText, {
2565
2694
  code: failure.upstreamCode,
2566
2695
  }),
@@ -2569,8 +2698,19 @@ export async function handleComboResponses(
2569
2698
  code: failure.upstreamCode,
2570
2699
  message: failure.classificationText,
2571
2700
  });
2572
- if (!nextPick) adoptFailedChildLog(childLog);
2573
- pick = nextPick;
2701
+ if (nextPick) {
2702
+ pick = nextPick;
2703
+ } else {
2704
+ pick = await pickWithWait({
2705
+ exclude: pick.attempted,
2706
+ eligible: payloadEligible,
2707
+ now: failureNow,
2708
+ });
2709
+ }
2710
+ if (!pick) {
2711
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
2712
+ adoptFailedChildLog(childLog);
2713
+ }
2574
2714
  }
2575
2715
  if (
2576
2716
  lastFailure?.status === 413
@@ -2688,7 +2828,13 @@ export async function handleResponses(
2688
2828
  const ownsBudget = options.translatorBudget === undefined;
2689
2829
  const translatorBudget = options.translatorBudget ?? createTranslatorBudget();
2690
2830
  try {
2691
- const response = await handleResponsesInner(req, config, logCtx, { ...options, translatorBudget });
2831
+ const response = await handleResponsesInner(req, config, logCtx, {
2832
+ ...options,
2833
+ // Capture before combo replay rebuilds the Request headers; children carry options.
2834
+ visionDescribeTerminal: options.visionDescribeTerminal === true
2835
+ || req.headers.get("x-opencodex-vision-describe") === "1",
2836
+ translatorBudget,
2837
+ });
2692
2838
  return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
2693
2839
  } catch (error) {
2694
2840
  if (ownsBudget) translatorBudget.dispose();
@@ -2725,10 +2871,22 @@ async function handleResponsesInner(
2725
2871
  }
2726
2872
  // An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
2727
2873
  // as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
2728
- const comboEffortRow = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
2874
+ const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
2729
2875
  && typeof (body as { model?: unknown }).model === "string"
2730
- ? parseRequestEffortRowId((body as { model: string }).model, config)
2731
- : null;
2876
+ // One parse for both grammars, from the selector as the client sent it. Parsing them
2877
+ // separately made the outcome depend on which ran first.
2878
+ ? parseSyntheticRowId((body as { model: string }).model, config)
2879
+ : { fastRow: null, effortRow: null };
2880
+ const comboEffortRow = comboRows.effortRow;
2881
+ if (comboRows.fastRow) {
2882
+ // Same reason as the effort row above: the combo dispatcher reads `model` next, so the
2883
+ // selector has to be normalized before it, or a combo child is built from a synthetic id.
2884
+ const raw = body as Record<string, unknown>;
2885
+ raw.model = comboRows.fastRow.baseId;
2886
+ // A caller INTENT, not a decision. decideTier still rules on eligibility downstream, so
2887
+ // fastMode:false and an ineligible route both still suppress it.
2888
+ raw.service_tier = "priority";
2889
+ }
2732
2890
  if (comboEffortRow) {
2733
2891
  const raw = body as Record<string, unknown>;
2734
2892
  raw.model = comboEffortRow.baseId;
@@ -2794,7 +2952,15 @@ async function handleResponsesInner(
2794
2952
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
2795
2953
  try {
2796
2954
  parsed = parseRequest(body);
2797
- const effortRow = parseRequestEffortRowId(parsed.modelId, config);
2955
+ // Captured before any parser mutates it, so both grammars see the client's id.
2956
+ const { fastRow, effortRow } = parseSyntheticRowId(parsed.modelId, config);
2957
+ if (fastRow) {
2958
+ parsed.modelId = fastRow.baseId;
2959
+ parsed.options.serviceTier = "priority";
2960
+ const raw = parsed._rawBody as Record<string, unknown>;
2961
+ raw.model = fastRow.baseId;
2962
+ raw.service_tier = "priority";
2963
+ }
2798
2964
  if (effortRow) {
2799
2965
  parsed.modelId = effortRow.baseId;
2800
2966
  parsed.options.reasoning = effortRow.effort;
@@ -3032,6 +3198,7 @@ async function handleResponsesInner(
3032
3198
  subagentFallbackAccountPreview,
3033
3199
  subagentFallbackModelEligibleAccountIdsForModel,
3034
3200
  fallbackChain,
3201
+ candidateRoute => canPassThroughEncryptedV2AgentTask(candidateRoute, inboundWire),
3035
3202
  );
3036
3203
  if (fallback) {
3037
3204
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
@@ -3061,7 +3228,8 @@ async function handleResponsesInner(
3061
3228
  previewSelectionAdmission?.release();
3062
3229
  }
3063
3230
 
3064
- // Native fallback can consume ciphertext, so recover only after final route selection.
3231
+ // Native fallback and explicitly trusted direct Responses routes can consume ciphertext,
3232
+ // so recover only after final route selection.
3065
3233
  if (
3066
3234
  inboundWire === "responses"
3067
3235
  &&
@@ -3070,6 +3238,7 @@ async function handleResponsesInner(
3070
3238
  && agentTaskRecovery
3071
3239
  && !isCanonicalOpenAiForwardProvider(route.provider)
3072
3240
  && !options.comboAttempt
3241
+ && !canPassThroughEncryptedV2AgentTask(route, inboundWire)
3073
3242
  ) {
3074
3243
  let recovered = false;
3075
3244
  try {
@@ -3193,9 +3362,16 @@ async function handleResponsesInner(
3193
3362
 
3194
3363
  if (options.abortSignal?.aborted) return clientCancelledResponse();
3195
3364
 
3196
- // Encrypted child tasks may only reach the canonical native backend. This check
3197
- // runs against the FINAL route so native-only fallback can rescue a routed primary.
3198
- if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
3365
+ // Encrypted child tasks may reach the canonical native backend or an explicitly trusted
3366
+ // direct Responses route. This runs against the FINAL route so native-only fallback can
3367
+ // rescue an incompatible primary without weakening combo behavior.
3368
+ const finalRouteCanPassThroughEncryptedTask = !options.comboAttempt
3369
+ && canPassThroughEncryptedV2AgentTask(route, inboundWire);
3370
+ if (
3371
+ (route.combo !== undefined || !isCanonicalOpenAiForwardProvider(route.provider))
3372
+ && !finalRouteCanPassThroughEncryptedTask
3373
+ && unreadableEncryptedAgentTask
3374
+ ) {
3199
3375
  return unreadableEncryptedAgentTaskResponse();
3200
3376
  }
3201
3377
 
@@ -3234,6 +3410,12 @@ async function handleResponsesInner(
3234
3410
  }
3235
3411
 
3236
3412
  if (options.abortSignal?.aborted) return clientCancelledResponse();
3413
+ // Resolve aliases/combo children before refusing helpers; do not spend main auth or host budget.
3414
+ if (isCanonicalOpenAiForwardProvider(route.provider)
3415
+ && isCodexReserveHelperUnsupported(options.codexAuthPolicy ?? config, route.modelId,
3416
+ options.admission, options.visionDescribeTerminal === true)) {
3417
+ return formatErrorResponse(400, "invalid_request_error", CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE);
3418
+ }
3237
3419
  // Refuse an input that cannot plausibly fit the model context window before spending auth,
3238
3420
  // circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
3239
3421
  //
@@ -3339,7 +3521,10 @@ async function handleResponsesInner(
3339
3521
  * tolerates project discovery failing, so a stored account can legitimately have no project;
3340
3522
  * sending that account's bearer with the FAILED account's project is worse than not rotating.
3341
3523
  */
3342
- const applyFailoverSnapshot = (snapshot: OAuthAccessSnapshot): boolean => {
3524
+ const applyFailoverSnapshot = (
3525
+ snapshot: OAuthAccessSnapshot,
3526
+ retryParsed: OcxParsedRequest = parsed,
3527
+ ): boolean => {
3343
3528
  if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
3344
3529
  let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
3345
3530
  if (route.providerName === "github-copilot") {
@@ -3352,7 +3537,14 @@ async function handleResponsesInner(
3352
3537
  }
3353
3538
  if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
3354
3539
  route.provider = rotatedProvider;
3355
- if (route.providerName === "kiro") parsed._kiroAuthContext = { ...(snapshot.kiro ?? {}) };
3540
+ if (route.providerName === "kiro") {
3541
+ const kiroContext = { ...(snapshot.kiro ?? {}) };
3542
+ // Terminal-guard continuations are rebuilt from a shallow clone. Updating only the
3543
+ // outer request pairs the new bearer with the failed account's region/profile on
3544
+ // the retry. Keep both owners synchronized; for ordinary paths they are identical.
3545
+ parsed._kiroAuthContext = kiroContext;
3546
+ if (retryParsed !== parsed) retryParsed._kiroAuthContext = { ...kiroContext };
3547
+ }
3356
3548
  // Re-stamp: a request that rotated accounts must be attributed to the account that actually
3357
3549
  // served it. All three rotation sites funnel through here, so this is the only re-stamp
3358
3550
  // needed -- and putting it anywhere else would let one of the three drift.
@@ -3453,6 +3645,14 @@ async function handleResponsesInner(
3453
3645
  if (isGenericFailoverProvider(route.providerName, route.provider)) {
3454
3646
  genericFailoverAccountId = resolved.accountId;
3455
3647
  }
3648
+ // Anthropic is excluded from isGenericFailoverProvider -- its own pool owns affinity and
3649
+ // a fail-closed local-cli credential rule -- so without this stamp its identity is
3650
+ // dropped whenever the pool flag is off, and a later 429 has no account to cool. Reactive
3651
+ // failover needs only the id: no affinity bind, no promotion, no quota-ranked pick. Those
3652
+ // are proactive and stay behind anthropicAccountPool.enabled.
3653
+ if (route.providerName === "anthropic" && hasAnthropicFailoverQuorum()) {
3654
+ anthropicPoolAccountId = resolved.accountId;
3655
+ }
3456
3656
  // Captured beside the account it fences, so the two can never disagree.
3457
3657
  if (hasPassiveAccountQuota(route.providerName)) {
3458
3658
  passiveQuotaWriterGeneration = captureConfigGeneration();
@@ -3567,6 +3767,17 @@ async function handleResponsesInner(
3567
3767
  }
3568
3768
  const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
3569
3769
 
3770
+ const rawInput = (parsed._rawBody as { input?: unknown }).input;
3771
+ if (!isPassthrough && Array.isArray(rawInput) && rawInput.some(
3772
+ item => item !== null && typeof item === "object" && item.type === "computer_call_output",
3773
+ )) {
3774
+ return formatErrorResponse(
3775
+ 400,
3776
+ "invalid_request_error",
3777
+ "computer_call_output requires a Responses passthrough route; send screenshots as user input_image content on translated routes.",
3778
+ );
3779
+ }
3780
+
3570
3781
  if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
3571
3782
  return formatErrorResponse(
3572
3783
  400,
@@ -3585,6 +3796,8 @@ async function handleResponsesInner(
3585
3796
  req.headers,
3586
3797
  config,
3587
3798
  {
3799
+ admission: options.admission,
3800
+ codexAuthPolicy: options.codexAuthPolicy,
3588
3801
  // Account-qualified native routes are passthrough, so their in-turn helper is vision.
3589
3802
  // Scope its cooldown and outcome to the helper model, not the routed text model.
3590
3803
  ...(route.codexAccountId !== undefined
@@ -3614,11 +3827,12 @@ async function handleResponsesInner(
3614
3827
  // call must never plan another describe. The flag arrives from the Chat
3615
3828
  // surface (whose bridge rebuilds headers) or as the raw header for native
3616
3829
  // Responses callers. Marked + text-only routed model → strip, depth cap 1.
3617
- const visionDescribeTerminal = options.visionDescribeTerminal === true
3618
- || req.headers.get("x-opencodex-vision-describe") === "1";
3830
+ const visionDescribeTerminal = options.visionDescribeTerminal === true;
3619
3831
  const visionPlan = visionDescribeTerminal
3620
3832
  ? undefined
3621
- : planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
3833
+ : planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar, {
3834
+ admission: options.admission, codexAuthPolicy: options.codexAuthPolicy,
3835
+ });
3622
3836
  const recordSidecarOutcome = openAiSidecar?.recordOutcome;
3623
3837
  if (visionPlan) {
3624
3838
  await describeImagesInPlace(
@@ -3793,7 +4007,7 @@ async function handleResponsesInner(
3793
4007
  // The guard needs a catalog to compare against, so it stands down when the request omits one.
3794
4008
  // An explicit empty catalog is still authoritative: it declares that no client tools may be
3795
4009
  // called. A passthrough request can legitimately omit `tools` entirely and still receive a call
3796
- // the client understands — `tests/github-copilot-stream-contract.test.ts` sends
4010
+ // the client understands — `tests/providers/github-copilot/github-copilot-stream-contract.test.ts` sends
3797
4011
  // `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
3798
4012
  // `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
3799
4013
  // too because the proxy cannot establish the caller's declared authorization boundary.
@@ -4062,6 +4276,15 @@ async function handleResponsesInner(
4062
4276
  releaseCodexAuthContextProbeLease(authCtx);
4063
4277
  return clientCancelledResponse();
4064
4278
  }
4279
+ const localRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
4280
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
4281
+ });
4282
+ if (localRefusal) {
4283
+ releaseUpstreamHostAdmission(hostAdmissionLease);
4284
+ hostAdmissionLease = null;
4285
+ releaseCodexAuthContextProbeLease(authCtx);
4286
+ return localRefusal;
4287
+ }
4065
4288
  const outcome = classifyTransportFailureKind(err);
4066
4289
  // Host-level evidence stands regardless of pool membership: a direct
4067
4290
  // forward send has no pool accounting, but the reachability failure is
@@ -4113,6 +4336,9 @@ async function handleResponsesInner(
4113
4336
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4114
4337
  providerName: route.providerName,
4115
4338
  modelId: route.modelId,
4339
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
4340
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
4341
+ ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
4116
4342
  }),
4117
4343
  route.provider.authMode === "forward")
4118
4344
  // Every real attempt response — including an intermediate 5xx the
@@ -4186,6 +4412,9 @@ async function handleResponsesInner(
4186
4412
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4187
4413
  providerName: route.providerName,
4188
4414
  modelId: route.modelId,
4415
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
4416
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
4417
+ ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
4189
4418
  }),
4190
4419
  route.provider.authMode === "forward")
4191
4420
  .then(response => {
@@ -4215,10 +4444,10 @@ async function handleResponsesInner(
4215
4444
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
4216
4445
  const poolAuthCtx = authCtx.kind === "pool" ? authCtx : undefined;
4217
4446
  const poolReplay = poolAuthCtx
4218
- ? await refreshPoolForwardAuth({ req, route, authCtx: poolAuthCtx, substituteMainCredential, options })
4447
+ ? await refreshPoolForwardAuth({ req, config, route, authCtx: poolAuthCtx, substituteMainCredential, options })
4219
4448
  : undefined;
4220
4449
  const replay = poolReplay
4221
- ?? await refreshNativeMainForwardAuth({ req, route, authCtx, substituteMainCredential, options });
4450
+ ?? await refreshNativeMainForwardAuth({ req, config, route, authCtx, substituteMainCredential, options });
4222
4451
  if (!replay.ok) {
4223
4452
  // Compact already records this; core historically returned without recording,
4224
4453
  // so a dead grant stayed selectable and every request repeated the same doomed
@@ -4287,6 +4516,9 @@ async function handleResponsesInner(
4287
4516
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4288
4517
  providerName: route.providerName,
4289
4518
  modelId: route.modelId,
4519
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
4520
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
4521
+ ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
4290
4522
  }),
4291
4523
  codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
4292
4524
  ),
@@ -4393,6 +4625,9 @@ async function handleResponsesInner(
4393
4625
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4394
4626
  providerName: route.providerName,
4395
4627
  modelId: route.modelId,
4628
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
4629
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
4630
+ ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
4396
4631
  }),
4397
4632
  route.provider.authMode === "forward")
4398
4633
  .then(res => {
@@ -4409,6 +4644,43 @@ async function handleResponsesInner(
4409
4644
  }
4410
4645
  }
4411
4646
 
4647
+ // Native Responses returns before the generic adapter's OAuth rotation loop. Keep
4648
+ // the same quorum, cooldown and request budget here, before any client bytes flow.
4649
+ if (
4650
+ upstreamResponse.status === 429
4651
+ && genericFailoverAccountId
4652
+ && genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
4653
+ && isGenericOAuthFailoverEnabled(config, route.providerName)
4654
+ ) {
4655
+ const nextAccountId = rotateGenericOAuthAccountOn429(
4656
+ config, route.providerName, genericFailoverAccountId,
4657
+ upstreamResponse.headers.get("retry-after"),
4658
+ );
4659
+ let snapshot: OAuthAccessSnapshot | undefined;
4660
+ if (nextAccountId) {
4661
+ try { snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); }
4662
+ catch { /* Keep the original 429 body readable when the next credential is unavailable. */ }
4663
+ }
4664
+ if (snapshot && applyFailoverSnapshot(snapshot)) {
4665
+ genericFailoverAccountId = snapshot.accountId;
4666
+ genericFailovers += 1;
4667
+ sentOAuthSnapshot = snapshot;
4668
+ replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
4669
+ route.provider = resolveProviderTransport(
4670
+ route.providerName, route.provider, parsed.options.promptCacheKey, snapshot.apiBaseUrl,
4671
+ );
4672
+ bindRouteReasoningReplayScope({
4673
+ parsed, providerName: route.providerName, provider: route.provider,
4674
+ adapterName: "openai-responses", oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
4675
+ });
4676
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
4677
+ const result = await rebuildAndRefetch("oauth-account-429");
4678
+ if ("failed" in result) return result.failed;
4679
+ upstreamResponse = result;
4680
+ continue passthroughRecovery;
4681
+ }
4682
+ }
4683
+
4412
4684
  // Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
4413
4685
  // passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
4414
4686
  // key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
@@ -4455,6 +4727,9 @@ async function handleResponsesInner(
4455
4727
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4456
4728
  providerName: route.providerName,
4457
4729
  modelId: route.modelId,
4730
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
4731
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
4732
+ ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
4458
4733
  }),
4459
4734
  route.provider.authMode === "forward")
4460
4735
  .then(res => {
@@ -4612,11 +4887,10 @@ async function handleResponsesInner(
4612
4887
  // Prefer primary when present, fall back to secondary for compatibility.
4613
4888
  const quotaMeta = { ...codexQuotaOutcomeMeta(upstreamResponse), ...(await codexDenialOutcomeMeta(upstreamResponse)) };
4614
4889
  const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
4615
- applyAccountQuotaFromUpstreamHeaders(
4616
- authCtx.accountId,
4617
- upstreamResponse.headers,
4618
- authCtx.writerGeneration,
4619
- );
4890
+ if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
4891
+ applyAccountQuotaFromUpstreamHeaders(authCtx.accountId, upstreamResponse.headers,
4892
+ authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined);
4893
+ }
4620
4894
  if (terminalBodyWillRecord) {
4621
4895
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
4622
4896
  terminalRecorder(status, httpStatusOverride);
@@ -4870,7 +5144,10 @@ async function handleResponsesInner(
4870
5144
  },
4871
5145
  onClientCancel: () => options.onNativePassthroughCancel?.(),
4872
5146
  onDone: () => unregisterTurn(turnAc),
4873
- }, inlineEagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
5147
+ }, {
5148
+ clientGoneSignal: options.abortSignal,
5149
+ ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
5150
+ });
4874
5151
  // When selected, this relay closes response.completed even if upstream
4875
5152
  // keeps the connection alive. Marked Codex WS traffic, Windows
4876
5153
  // forced-rewrite traffic, and Darwin explicit eager traffic apply
@@ -4889,7 +5166,10 @@ async function handleResponsesInner(
4889
5166
  linkAbortSignal(upstream, turnAc.signal);
4890
5167
  registerTurn(turnAc, options.turnAdmissionLease);
4891
5168
  const inspectionConsumerOptions = {
4892
- clientGoneSignal: clientGone.signal,
5169
+ // Request abort can reject the fetch body before the response cancel hook runs.
5170
+ clientGoneSignal: options.abortSignal
5171
+ ? AbortSignal.any([clientGone.signal, options.abortSignal])
5172
+ : clientGone.signal,
4893
5173
  drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
4894
5174
  upstream,
4895
5175
  pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
@@ -5128,6 +5408,36 @@ async function handleResponsesInner(
5128
5408
  }
5129
5409
  }
5130
5410
 
5411
+ // Tool results are PAIRED by call_id. parseRequest writes it into OcxToolResultMessage.toolCallId
5412
+ // (parser.ts:738/752) without validating it, because inputItemSchema's permissive catch-all
5413
+ // (schema.ts:106) accepts a tool item whose strict schema failed only for a missing call_id. A
5414
+ // translating adapter then consumes `toolCallId: string` holding undefined: kiro-wire.ts:32
5415
+ // TypeErrors, ollama-native.ts:334 throws, and anthropic.ts:775 sends
5416
+ // "[tool_result without adjacent tool_use: undefined]" upstream (issue #3259).
5417
+ //
5418
+ // This CANNOT move into the schema. parseRequest (:2812) runs before the passthrough branch
5419
+ // (:3719), so a parse-time rejection would also kill forward/key passthrough and routed
5420
+ // compaction — paths that never read context.messages, build from _rawBody, and already
5421
+ // degrade an unpaired output to "[tool output for unknown call]" on their own.
5422
+ //
5423
+ // Keyed on the adapter, not on position: routedCompaction skips the passthrough branch above
5424
+ // yet still builds from _rawBody (see the :3703 comment).
5425
+ if (!("passthrough" in adapter && adapter.passthrough)) {
5426
+ const unpaired = parsed.context.messages.find(
5427
+ message => message.role === "toolResult"
5428
+ && (typeof (message as { toolCallId?: unknown }).toolCallId !== "string"
5429
+ || (message as { toolCallId: string }).toolCallId.length === 0),
5430
+ );
5431
+ if (unpaired) {
5432
+ // Never interpolate the tool output: this message reaches the client and the logs.
5433
+ return formatErrorResponse(
5434
+ 400,
5435
+ "invalid_request_error",
5436
+ "tool result requires a non-empty string call_id",
5437
+ );
5438
+ }
5439
+ }
5440
+
5131
5441
  // Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
5132
5442
  // Routed-compaction turns must NOT hit the image bridge: compaction clears tools/_webSearch but
5133
5443
  // leaves _imageGeneration, so planImageBridge would activate and return a normal Responses
@@ -5139,7 +5449,9 @@ async function handleResponsesInner(
5139
5449
  // - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
5140
5450
  // can proceed for web-search-only turns
5141
5451
  const wsPlan = !routedCompaction
5142
- ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar)
5452
+ ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, {
5453
+ admission: options.admission, codexAuthPolicy: options.codexAuthPolicy,
5454
+ })
5143
5455
  : undefined;
5144
5456
  const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
5145
5457
  const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
@@ -5153,12 +5465,15 @@ async function handleResponsesInner(
5153
5465
  });
5154
5466
  if (rotated) {
5155
5467
  route.provider = rotated;
5156
- } else {
5157
- if (
5158
- !genericFailoverAccountId
5159
- || genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
5160
- || !isGenericOAuthFailoverEnabled(config, route.providerName)
5161
- ) return null;
5468
+ } else if (
5469
+ // A POSITIVE gate, not an early return. An early `return null` here made every later arm
5470
+ // unreachable: Anthropic never has a genericFailoverAccountId (isGenericFailoverProvider
5471
+ // excludes it), so its sidecar 429s died on this guard before the Anthropic arm below
5472
+ // could ever be considered.
5473
+ genericFailoverAccountId
5474
+ && genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
5475
+ && isGenericOAuthFailoverEnabled(config, route.providerName)
5476
+ ) {
5162
5477
  const nextAccountId = rotateGenericOAuthAccountOn429(
5163
5478
  config,
5164
5479
  route.providerName,
@@ -5174,6 +5489,39 @@ async function handleResponsesInner(
5174
5489
  } catch {
5175
5490
  return null;
5176
5491
  }
5492
+ } else if (
5493
+ // Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
5494
+ // web-search or image-bridge turn was terminal even with the pool fully enabled -- while
5495
+ // the very same 429 on the main response path rotated.
5496
+ anthropicPoolAccountId
5497
+ && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
5498
+ ) {
5499
+ const nextAccountId = rotateAnthropicAccountOn429(
5500
+ config,
5501
+ anthropicPoolAccountId,
5502
+ retryAfter,
5503
+ anthropicSessionKey,
5504
+ );
5505
+ if (!nextAccountId) return null;
5506
+ try {
5507
+ // Deliberately NOT applyFailoverSnapshot: that helper exists to pair per-account routing
5508
+ // metadata (Copilot origin, Antigravity project, Kiro context) with its bearer. Anthropic
5509
+ // carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
5510
+ // local-cli credential rule. Both existing Anthropic rotation sites apply the token the
5511
+ // same way.
5512
+ const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
5513
+ anthropicPoolAccountId = nextAccountId;
5514
+ anthropicPoolFailovers += 1;
5515
+ route.provider = { ...route.provider, apiKey: accessToken };
5516
+ promoteAnthropicActiveAccount(nextAccountId);
5517
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
5518
+ } catch {
5519
+ return null;
5520
+ }
5521
+ } else {
5522
+ // No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
5523
+ // credential. The 429 is terminal for this sidecar turn.
5524
+ return null;
5177
5525
  }
5178
5526
  const rotatedAdapter = resolveAdapter(
5179
5527
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
@@ -6042,6 +6390,37 @@ async function handleResponsesInner(
6042
6390
  continue recovery;
6043
6391
  }
6044
6392
 
6393
+ // Static API-key pools can recover a credential-scoped 401 without abandoning the
6394
+ // provider: one revoked or mistyped key says nothing about its siblings. OAuth providers
6395
+ // refresh above and never enter here — `hasKeyPoolFailover` rejects oauth/forward modes.
6396
+ // Runs after the OAuth replay so a refreshable token is never treated as a dead key.
6397
+ while (upstreamResponse.status === 401 && hasKeyPoolFailover(route.provider)) {
6398
+ const rotated = rotateProviderTransportOn401(config, route.providerName, route.provider, {
6399
+ now: Date.now(),
6400
+ attemptedKey: route.provider.apiKey,
6401
+ promptCacheKey: parsed.options.promptCacheKey,
6402
+ });
6403
+ if (!rotated) break;
6404
+ // Release the failed response's socket before retrying; unread bodies otherwise linger
6405
+ // until runtime cleanup (one per rotated key).
6406
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
6407
+ route.provider = rotated;
6408
+ invalidateSameTargetRequest();
6409
+ activeAdapter = resolveAdapter(
6410
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6411
+ config.cacheRetention,
6412
+ );
6413
+ bindRouteReasoningReplayScope({
6414
+ parsed,
6415
+ providerName: route.providerName,
6416
+ provider: route.provider,
6417
+ adapterName: activeAdapter.name,
6418
+ });
6419
+ const result = await rebuildAndRefetch("key-401");
6420
+ if ("failed" in result) return result.failed;
6421
+ upstreamResponse = result;
6422
+ }
6423
+
6045
6424
  // Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
6046
6425
  // 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
6047
6426
  // so wait (Retry-After or the fixed interval) and replay the IDENTICAL request on the
@@ -6117,7 +6496,6 @@ async function handleResponsesInner(
6117
6496
  while (
6118
6497
  upstreamResponse.status === 429
6119
6498
  && anthropicPoolAccountId
6120
- && isAnthropicAccountPoolEnabled(config)
6121
6499
  && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
6122
6500
  ) {
6123
6501
  const nextAccountId = rotateAnthropicAccountOn429(
@@ -6528,7 +6906,6 @@ async function handleResponsesInner(
6528
6906
  if (
6529
6907
  response.status === 429
6530
6908
  && anthropicPoolAccountId
6531
- && isAnthropicAccountPoolEnabled(config)
6532
6909
  && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
6533
6910
  ) {
6534
6911
  const nextAccountId = rotateAnthropicAccountOn429(
@@ -6559,6 +6936,48 @@ async function handleResponsesInner(
6559
6936
  }
6560
6937
  }
6561
6938
  }
6939
+ // Generic OAuth rotation for the continuation loop. The streaming loop grew this arm with
6940
+ // #2568 and this one did not, so an xAI/Cursor/Kimi/Copilot/Antigravity/Nous continuation
6941
+ // 429 stayed terminal even with failover fully active -- the same class of divergence the
6942
+ // two sidecars already produced once. Request-local state is shared with the other arms so
6943
+ // the per-request bound cannot be silently re-armed by reaching a different loop.
6944
+ if (
6945
+ response.status === 429
6946
+ && genericFailoverAccountId
6947
+ && genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
6948
+ && isGenericOAuthFailoverEnabled(config, route.providerName)
6949
+ ) {
6950
+ const nextAccountId = rotateGenericOAuthAccountOn429(
6951
+ config,
6952
+ route.providerName,
6953
+ genericFailoverAccountId,
6954
+ response.headers.get("retry-after"),
6955
+ );
6956
+ if (nextAccountId) {
6957
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
6958
+ try {
6959
+ // The FULL snapshot through the shared helper, never a bare bearer: Antigravity
6960
+ // pairs an account-matched projectId with its token and Kiro carries routing
6961
+ // metadata, so a token-only swap would mix one account's credential with another's
6962
+ // routing data.
6963
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
6964
+ genericFailoverAccountId = nextAccountId;
6965
+ genericFailovers += 1;
6966
+ if (applyFailoverSnapshot(snapshot, nextParsed)) {
6967
+ invalidateSameTargetRequest();
6968
+ activeAdapter = resolveAdapter(
6969
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6970
+ config.cacheRetention,
6971
+ );
6972
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
6973
+ nextContinuationRecoveryKind = "oauth-account-429";
6974
+ continue;
6975
+ }
6976
+ } catch {
6977
+ // fall through to emit continuation error below
6978
+ }
6979
+ }
6980
+ }
6562
6981
  if (shouldAttemptImageTierRetry({
6563
6982
  status: response.status,
6564
6983
  adapterName: activeAdapter.name,