@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906

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 (260) 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-VGeQEZ_v.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 +54 -24
  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/identity.ts +8 -2
  26. package/src/adapters/kiro-calibration.ts +181 -0
  27. package/src/adapters/kiro.ts +135 -3
  28. package/src/adapters/openai-responses.ts +259 -29
  29. package/src/adapters/responses-code-mode.ts +59 -0
  30. package/src/adapters/tool-catalog-nudge.ts +1 -1
  31. package/src/adapters/xai-schema-analysis.ts +86 -0
  32. package/src/adapters/xai-tool-schema.ts +2 -87
  33. package/src/adapters/xai-web-search.ts +1 -1
  34. package/src/bridge.ts +47 -13
  35. package/src/chat/inbound.ts +11 -3
  36. package/src/claude/inbound-content-options.ts +60 -0
  37. package/src/claude/inbound-model-options.ts +142 -0
  38. package/src/claude/inbound-records.ts +7 -0
  39. package/src/claude/inbound.ts +10 -202
  40. package/src/claude/model-info.ts +45 -0
  41. package/src/cli/account-auth.ts +49 -9
  42. package/src/cli/account-extended.ts +7 -1
  43. package/src/cli/capabilities.ts +15 -4
  44. package/src/cli/claude.ts +232 -39
  45. package/src/cli/config-command.ts +9 -1
  46. package/src/cli/dispatch.ts +5 -1
  47. package/src/cli/doctor.ts +10 -0
  48. package/src/cli/effort.ts +372 -0
  49. package/src/cli/export-command.ts +3 -9
  50. package/src/cli/help.ts +1 -0
  51. package/src/cli/index.ts +13 -0
  52. package/src/cli/init.ts +4 -0
  53. package/src/cli/model-selection-guidance.ts +30 -0
  54. package/src/cli/models-runtime.ts +3 -2
  55. package/src/cli/models.ts +8 -3
  56. package/src/cli/observe.ts +3 -1
  57. package/src/cli/opencode.ts +4 -1
  58. package/src/cli/provider-runtime.ts +65 -0
  59. package/src/cli/provider.ts +8 -0
  60. package/src/cli/registry.ts +16 -2
  61. package/src/cli/runtime-api.ts +3 -1
  62. package/src/cli/star-prompt.ts +22 -6
  63. package/src/cli/status-probes.ts +168 -0
  64. package/src/cli/status.ts +5 -168
  65. package/src/clients/config-export/constants.ts +69 -0
  66. package/src/clients/config-export/contracts.ts +154 -0
  67. package/src/clients/config-export/dsh.ts +132 -0
  68. package/src/clients/config-export/fast-models.ts +29 -0
  69. package/src/clients/config-export/mcode.ts +83 -0
  70. package/src/clients/config-export/model-metadata.ts +108 -0
  71. package/src/clients/config-export/omp.ts +104 -0
  72. package/src/clients/config-export/zcode.ts +92 -0
  73. package/src/clients/config-export.ts +18 -710
  74. package/src/codex/account-lifecycle.ts +20 -3
  75. package/src/codex/account-usability.ts +2 -0
  76. package/src/codex/auth-api.ts +272 -32
  77. package/src/codex/auth-context.ts +328 -24
  78. package/src/codex/catalog/effort.ts +44 -5
  79. package/src/codex/catalog/metadata.ts +149 -14
  80. package/src/codex/catalog/native-models.ts +116 -4
  81. package/src/codex/catalog/parsing.ts +122 -8
  82. package/src/codex/catalog/provider-fetch.ts +154 -23
  83. package/src/codex/catalog/reserve.ts +52 -0
  84. package/src/codex/catalog/sync.ts +89 -16
  85. package/src/codex/catalog.ts +1 -1
  86. package/src/codex/convergence-types.ts +1 -0
  87. package/src/codex/convergence.ts +2 -0
  88. package/src/codex/data/upstream-models.json +169 -0
  89. package/src/codex/forward-transport-headers.ts +25 -0
  90. package/src/codex/inject.ts +99 -34
  91. package/src/codex/injected-marker.ts +30 -4
  92. package/src/codex/journal.ts +14 -0
  93. package/src/codex/legacy-config-keys.ts +68 -0
  94. package/src/codex/log-guard/inspect-schema.ts +137 -0
  95. package/src/codex/log-guard/inspect.ts +2 -134
  96. package/src/codex/loopback-target.ts +54 -0
  97. package/src/codex/main-account-cache.ts +63 -1
  98. package/src/codex/main-account-hard-lock.ts +52 -0
  99. package/src/codex/main-account.ts +3 -1
  100. package/src/codex/management-convergence.ts +3 -0
  101. package/src/codex/model-entitlements.ts +54 -4
  102. package/src/codex/project-config-warnings.ts +92 -2
  103. package/src/codex/prompt-layers/encoding.ts +80 -0
  104. package/src/codex/prompt-layers/paths.ts +54 -0
  105. package/src/codex/prompt-layers/revision.ts +55 -0
  106. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  107. package/src/codex/prompt-layers/toml-read.ts +181 -0
  108. package/src/codex/prompt-layers.ts +14 -520
  109. package/src/codex/quota-auto-refresh-state.ts +16 -0
  110. package/src/codex/quota-auto-refresh.ts +219 -0
  111. package/src/codex/quota-types.ts +51 -0
  112. package/src/codex/quota.ts +252 -93
  113. package/src/codex/reserve-availability.ts +177 -0
  114. package/src/codex/routing.ts +28 -9
  115. package/src/codex/shim.ts +53 -11
  116. package/src/codex/subagent-model-fallback.ts +23 -3
  117. package/src/combos/failover.ts +125 -7
  118. package/src/combos/identifiers.ts +89 -0
  119. package/src/combos/index.ts +4 -0
  120. package/src/combos/resolve.ts +80 -9
  121. package/src/combos/types.ts +20 -93
  122. package/src/config/subagent-models.ts +24 -0
  123. package/src/config.ts +156 -13
  124. package/src/generated/compatibility-version.json +474 -178
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/integrations/journal.ts +65 -4
  127. package/src/integrations/store.ts +5 -0
  128. package/src/lab/events/limits.ts +4 -0
  129. package/src/lib/destination-policy.ts +31 -2
  130. package/src/lib/errors.ts +39 -0
  131. package/src/lib/provider-outbound.ts +69 -3
  132. package/src/lib/proxy-env.ts +22 -0
  133. package/src/lib/redact-folding.ts +176 -0
  134. package/src/lib/redact.ts +2 -175
  135. package/src/lib/state-store-sweeper.ts +20 -6
  136. package/src/lib/token-estimate.ts +94 -27
  137. package/src/lib/windows-user-principal.ts +53 -5
  138. package/src/oauth/account-quota-rank.ts +40 -1
  139. package/src/oauth/anthropic-routing.ts +99 -3
  140. package/src/oauth/chatgpt-device.ts +187 -0
  141. package/src/oauth/chatgpt.ts +31 -4
  142. package/src/oauth/generic-account-failover.ts +36 -13
  143. package/src/oauth/index.ts +140 -29
  144. package/src/oauth/log.ts +3 -0
  145. package/src/oauth/login-cli.ts +5 -0
  146. package/src/oauth/meta-muse.ts +117 -15
  147. package/src/oauth/pool-settings-capability.ts +15 -4
  148. package/src/providers/api-keys.ts +8 -10
  149. package/src/providers/default-aliases.ts +39 -0
  150. package/src/providers/derive.ts +10 -2
  151. package/src/providers/fastwire.ts +36 -7
  152. package/src/providers/initial-model-selection-runtime.ts +90 -0
  153. package/src/providers/initial-model-selection.ts +120 -0
  154. package/src/providers/key-failover.ts +134 -54
  155. package/src/providers/key-store.ts +11 -1
  156. package/src/providers/label.ts +1 -1
  157. package/src/providers/model-discovery.ts +76 -0
  158. package/src/providers/model-rename-startup.ts +72 -8
  159. package/src/providers/muse-subscription-usage.ts +95 -0
  160. package/src/providers/openai-sidecar.ts +17 -5
  161. package/src/providers/openai-tiers-destination.ts +102 -0
  162. package/src/providers/openai-tiers.ts +2 -99
  163. package/src/providers/opencode-go-transport.ts +41 -0
  164. package/src/providers/quota-key-accounts.ts +141 -0
  165. package/src/providers/quota-types.ts +9 -0
  166. package/src/providers/quota.ts +625 -98
  167. package/src/providers/registry.ts +60 -17
  168. package/src/providers/xai-responses-opt-in.ts +31 -5
  169. package/src/quota/reset-activation.ts +81 -0
  170. package/src/quota/reset-detector.ts +305 -0
  171. package/src/quota/reset-notify-config.ts +162 -0
  172. package/src/quota/reset-observer.ts +125 -0
  173. package/src/quota/reset-poller.ts +160 -0
  174. package/src/quota/reset-seen-store.ts +385 -0
  175. package/src/quota/reset-sinks.ts +199 -0
  176. package/src/quota/window-mapping.ts +106 -0
  177. package/src/responses/apply-patch-envelope.ts +46 -0
  178. package/src/responses/code-mode-helper-compat.ts +39 -1
  179. package/src/responses/custom-tool-compat.ts +10 -4
  180. package/src/responses/hosted-tool-policy.ts +12 -4
  181. package/src/responses/parser-content.ts +133 -0
  182. package/src/responses/parser-text-format.ts +24 -0
  183. package/src/responses/parser-tools.ts +188 -0
  184. package/src/responses/parser.ts +3 -326
  185. package/src/responses/state.ts +124 -28
  186. package/src/router.ts +48 -13
  187. package/src/routing/analytics.ts +1 -0
  188. package/src/routing/capability.ts +17 -4
  189. package/src/server/auth-cors.ts +7 -1
  190. package/src/server/background-lifecycle.ts +23 -1
  191. package/src/server/chat-completions.ts +25 -3
  192. package/src/server/claude-messages.ts +62 -5
  193. package/src/server/effort-row.ts +1 -1
  194. package/src/server/fast-row.ts +295 -0
  195. package/src/server/gui-static.ts +30 -4
  196. package/src/server/index.ts +122 -28
  197. package/src/server/live.ts +18 -4
  198. package/src/server/management/agent-settings-routes.ts +2 -2
  199. package/src/server/management/combo-routes.ts +37 -9
  200. package/src/server/management/config-routes.ts +93 -2
  201. package/src/server/management/integration-routes.ts +108 -0
  202. package/src/server/management/model-routes.ts +13 -3
  203. package/src/server/management/model-rows.ts +20 -1
  204. package/src/server/management/native-integration-routes.ts +4 -1
  205. package/src/server/management/oauth-account-routes.ts +45 -10
  206. package/src/server/management/provider-routes.ts +34 -3
  207. package/src/server/management/quota-reset-routes.ts +57 -0
  208. package/src/server/management/route-registry.ts +7 -4
  209. package/src/server/management/shared.ts +19 -5
  210. package/src/server/management/system-routes.ts +3 -2
  211. package/src/server/management-api.ts +14 -2
  212. package/src/server/ports.ts +12 -2
  213. package/src/server/relay-eager.ts +38 -23
  214. package/src/server/relay.ts +4 -0
  215. package/src/server/request-log.ts +6 -0
  216. package/src/server/responses/codex-ws-correlation.ts +65 -0
  217. package/src/server/responses/codex-ws-exchange.ts +261 -0
  218. package/src/server/responses/codex-ws-metadata.ts +134 -0
  219. package/src/server/responses/codex-ws-pool.ts +162 -0
  220. package/src/server/responses/codex-ws-request.ts +87 -0
  221. package/src/server/responses/codex-ws-session.ts +93 -0
  222. package/src/server/responses/codex-ws-wire.ts +144 -0
  223. package/src/server/responses/collaboration.ts +41 -1
  224. package/src/server/responses/compact.ts +105 -12
  225. package/src/server/responses/core.ts +510 -57
  226. package/src/server/responses/empty-completion-guard.ts +4 -0
  227. package/src/server/responses/fetch-helpers.ts +10 -3
  228. package/src/server/responses/input-admission.ts +16 -9
  229. package/src/server/responses/responses-field-backfill.ts +1 -1
  230. package/src/server/responses/ws-upstream.ts +34 -318
  231. package/src/server/responses-custom-tool-repair.ts +20 -4
  232. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  233. package/src/server/safe-response-headers.ts +23 -0
  234. package/src/server/search.ts +9 -0
  235. package/src/server/subagent-models-startup.ts +27 -0
  236. package/src/server/system-env-shell.ts +238 -0
  237. package/src/server/system-env.ts +7 -234
  238. package/src/server/ws-bridge.ts +3 -25
  239. package/src/server/xai-responses-startup.ts +21 -0
  240. package/src/service-manager-probe.ts +1 -1
  241. package/src/service.ts +55 -16
  242. package/src/types/config.ts +108 -12
  243. package/src/types/provider.ts +36 -7
  244. package/src/types/request.ts +8 -0
  245. package/src/types/tools.ts +26 -1
  246. package/src/types.ts +2 -0
  247. package/src/update/notify.ts +8 -2
  248. package/src/usage/cost.ts +38 -28
  249. package/src/usage/expected-prices.ts +34 -15
  250. package/src/usage/log.ts +2 -0
  251. package/src/usage/model-identity.ts +26 -0
  252. package/src/usage/summary.ts +15 -1
  253. package/src/vision/describe.ts +6 -0
  254. package/src/vision/image-rewrite.ts +108 -0
  255. package/src/vision/index.ts +19 -306
  256. package/src/vision/plan.ts +205 -0
  257. package/src/web-search/executor.ts +6 -0
  258. package/src/web-search/index.ts +8 -1
  259. package/gui/dist/assets/index-B2YjLA-i.css +0 -1
  260. package/gui/dist/assets/index-aPup8CKb.js +0 -112
@@ -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,
@@ -219,6 +228,9 @@ import {
219
228
  waitForProviderRequestSlot,
220
229
  } from "../../providers/request-pacing";
221
230
  import { slugsEquivalent } from "../../providers/slug-codec";
231
+ import { isMuseSubscriptionUsagePayload, parseMuseSubscriptionUsage } from "../../providers/muse-subscription-usage";
232
+ import { hasPassiveAccountQuota, recordPassiveAccountQuota } from "../../providers/quota";
233
+ import { captureConfigGeneration } from "../../lib/state-store-sweeper";
222
234
  import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
223
235
  import { isUsageDebugEnabled } from "../../usage/debug";
224
236
  import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
@@ -234,10 +246,12 @@ import {
234
246
  rateLimitRetryDelayMs,
235
247
  rateLimitRetryPolicyFor,
236
248
  rotateProviderTransportOn429,
249
+ rotateProviderTransportOn401,
237
250
  transientRetryPolicyFor,
238
251
  } from "../../providers/key-failover";
239
252
  import { shouldAttemptImageTierRetry } from "../image-retry";
240
253
  import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
254
+ import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
241
255
  import type { WsData } from "../ws-bridge";
242
256
  import {
243
257
  codexAccountSelectionForTurn,
@@ -284,6 +298,7 @@ import {
284
298
  conversationIdFromResponsesRequest,
285
299
  normalizeLogConversationId,
286
300
  reasoningReplayConversationIdFromResponsesRequest,
301
+ sessionLaneIdFromRequest,
287
302
  sessionIdHeaderFromRequest,
288
303
  } from "../request-log-conversation";
289
304
  import type { AttemptRecoveryKind } from "../../usage/log";
@@ -329,6 +344,7 @@ import {
329
344
  } from "../responses-image-gen-repair";
330
345
  import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
331
346
  import { parseRequestEffortRowId } from "../effort-row";
347
+ import { parseSyntheticRowId } from "../fast-row";
332
348
  import {
333
349
  collectSelfNamedNamespaceScrubAuthorization,
334
350
  createSelfNamedToolCallNamespaceScrubRewrite,
@@ -858,6 +874,13 @@ export function usesCodexForwardPoolAuth(
858
874
  && provider.authMode === "forward" && provider.adapter === "openai-responses";
859
875
  }
860
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
+
861
884
  export function preAuthUpstreamHostCircuitKey(
862
885
  route: Pick<RouteResult, "provider" | "providerName" | "codexAccountMode" | "codexAccountId">,
863
886
  config: OcxConfig,
@@ -965,6 +988,9 @@ interface CodexPoolAccountRetryArgs {
965
988
  parsed: OcxParsedRequest;
966
989
  logCtx: RequestLogContext;
967
990
  options: {
991
+ admission?: DataPlaneAdmission;
992
+ codexAuthPolicy?: CodexAuthPolicyConfig;
993
+ visionDescribeTerminal?: boolean;
968
994
  abortSignal?: AbortSignal;
969
995
  onCodexAuthContextResolved?: (ctx: CodexAuthContext) => void;
970
996
  deferCodexResetDerivedCooldown?: boolean;
@@ -1161,6 +1187,8 @@ async function retryCodexPoolOnAlternateAccount(
1161
1187
  "pool",
1162
1188
  {
1163
1189
  excludeAccountId: firstAuthCtx.accountId,
1190
+ admission: options.admission,
1191
+ codexAuthPolicy: options.codexAuthPolicy,
1164
1192
  modelId: route.modelId,
1165
1193
  requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
1166
1194
  beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
@@ -1210,6 +1238,7 @@ async function retryCodexPoolOnAlternateAccount(
1210
1238
  firstAuthCtx.accountId,
1211
1239
  firstResponse.headers,
1212
1240
  firstAuthCtx.writerGeneration,
1241
+ firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
1213
1242
  );
1214
1243
  }
1215
1244
  const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
@@ -1231,7 +1260,7 @@ async function retryCodexPoolOnAlternateAccount(
1231
1260
  // Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
1232
1261
  // ordinary requests must block the first account before the alternate send.
1233
1262
  if (!deferFirstOutcome) recordFirstOutcome();
1234
- const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx);
1263
+ const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
1235
1264
  const retryProvider = applyCodexAuthContextToProvider(
1236
1265
  stripCodexRuntimeProviderFields(route.provider),
1237
1266
  retryAuthCtx,
@@ -1300,6 +1329,9 @@ async function retryCodexPoolOnAlternateAccount(
1300
1329
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1301
1330
  providerName: route.providerName,
1302
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,
1303
1335
  }),
1304
1336
  // Credential-bearing forward send: never follow a redirect into a
1305
1337
  // dead-host rejection after the credential was seen (#914).
@@ -1469,6 +1501,8 @@ export interface ConsumedComboFailure {
1469
1501
  upstreamCode?: string;
1470
1502
  /** Valid numeric/date value used only for cooldown calculation. */
1471
1503
  retryAfter?: string;
1504
+ /** Upstream Codex quota-window reset timestamps used for combo cooldowns. */
1505
+ resetAt?: string[];
1472
1506
  /** Reserved for 040 usage attribution without adding another body read. */
1473
1507
  usage?: OcxUsage;
1474
1508
  }
@@ -1476,6 +1510,8 @@ export interface ConsumedComboFailure {
1476
1510
 
1477
1511
 
1478
1512
  export interface HandleResponsesOptions {
1513
+ /** Original live policy owner; separate from caller-specific routing/sidecar snapshots. */
1514
+ codexAuthPolicy?: CodexAuthPolicyConfig;
1479
1515
  turnAdmissionLease?: AdmissionLease;
1480
1516
  /**
1481
1517
  * How the caller proved data-plane admission (#1686).
@@ -1585,9 +1621,26 @@ export async function consumeComboFailure(
1585
1621
  let upstreamCode: string | undefined;
1586
1622
  let upstreamMessage: string | undefined;
1587
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;
1588
1629
  try {
1589
- 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
+ });
1590
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
+ }
1591
1644
  if (body.displaySafe) {
1592
1645
  const normalized = normalizeUpstreamErrorText(body.text, fallback);
1593
1646
  classificationText = normalized.safeText;
@@ -1608,18 +1661,24 @@ export async function consumeComboFailure(
1608
1661
  ? fallback
1609
1662
  : `${fallback}: ${classificationText}`;
1610
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;
1611
1670
  // Client response may get the synthetic "2" fallback; cooldown metadata must not —
1612
1671
  // otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
1613
1672
  const clientRetryAfter = resolveClientRetryAfter({
1614
1673
  status: response.status,
1615
1674
  message,
1616
- upstreamRetryAfter,
1675
+ upstreamRetryAfter: effectiveRetryAfter,
1617
1676
  now,
1618
1677
  });
1619
1678
  const cooldownRetryAfter = resolveClientRetryAfter({
1620
1679
  status: response.status,
1621
1680
  message,
1622
- upstreamRetryAfter,
1681
+ upstreamRetryAfter: effectiveRetryAfter,
1623
1682
  now,
1624
1683
  includeDefault: false,
1625
1684
  });
@@ -1636,6 +1695,14 @@ export async function consumeComboFailure(
1636
1695
  classificationText,
1637
1696
  ...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
1638
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
+ : {}),
1639
1706
  ...(usage ? { usage } : {}),
1640
1707
  };
1641
1708
  }
@@ -1751,6 +1818,31 @@ function unreadableEncryptedAgentTaskResponse(): Response {
1751
1818
  );
1752
1819
  }
1753
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
+
1754
1846
  type ResponsesAuthResolution =
1755
1847
  | { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean }
1756
1848
  | { ok: false; response: Response };
@@ -1798,6 +1890,8 @@ async function resolveResponsesCodexAuth(
1798
1890
  let authCtx: CodexAuthContext;
1799
1891
  if (route.codexAccountMode) {
1800
1892
  authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
1893
+ admission: options.admission,
1894
+ codexAuthPolicy: options.codexAuthPolicy,
1801
1895
  accountId: route.codexAccountId,
1802
1896
  modelId: route.modelId,
1803
1897
  substituteMainCredentialForDirect: substituteMainCredential,
@@ -1826,6 +1920,23 @@ async function resolveResponsesCodexAuth(
1826
1920
  authCtx = { kind: "main", accountId: null };
1827
1921
  options.onCodexAuthContextResolved?.(undefined);
1828
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);
1829
1940
  if (!isCodexAuthContextUsable(authCtx, config)) {
1830
1941
  releaseCodexAuthContextProbeLease(authCtx);
1831
1942
  return {
@@ -1836,11 +1947,7 @@ async function resolveResponsesCodexAuth(
1836
1947
  return {
1837
1948
  ok: true,
1838
1949
  authCtx,
1839
- headers: await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
1840
- substituteMainCredential,
1841
- signal: options.abortSignal,
1842
- nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
1843
- }),
1950
+ headers,
1844
1951
  substituteMainCredential,
1845
1952
  };
1846
1953
  } catch (err) {
@@ -1883,6 +1990,7 @@ function isTerminalPoolRefreshFailure(error: unknown): boolean {
1883
1990
  */
1884
1991
  async function refreshPoolForwardAuth(args: {
1885
1992
  req: Request;
1993
+ config: OcxConfig;
1886
1994
  route: RouteResult;
1887
1995
  authCtx: CodexAuthContext & { kind: "pool" };
1888
1996
  substituteMainCredential: boolean;
@@ -1891,7 +1999,7 @@ async function refreshPoolForwardAuth(args: {
1891
1999
  | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
1892
2000
  | { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
1893
2001
  > {
1894
- const { req, route, authCtx, substituteMainCredential, options } = args;
2002
+ const { req, config, route, authCtx, substituteMainCredential, options } = args;
1895
2003
  try {
1896
2004
  const refreshed = await forceRefreshCodexPoolToken(authCtx.accountId, {
1897
2005
  rejectedGeneration: authCtx.generation,
@@ -1929,6 +2037,9 @@ async function refreshPoolForwardAuth(args: {
1929
2037
  route.codexAccountMode,
1930
2038
  );
1931
2039
  const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
2040
+ admission: options.admission,
2041
+ config: options.codexAuthPolicy ?? config,
2042
+ modelId: route.modelId,
1932
2043
  substituteMainCredential,
1933
2044
  signal: options.abortSignal,
1934
2045
  nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
@@ -1955,6 +2066,7 @@ async function refreshPoolForwardAuth(args: {
1955
2066
 
1956
2067
  async function refreshNativeMainForwardAuth(args: {
1957
2068
  req: Request;
2069
+ config: OcxConfig;
1958
2070
  route: RouteResult;
1959
2071
  authCtx: CodexAuthContext;
1960
2072
  substituteMainCredential: boolean;
@@ -1963,7 +2075,7 @@ async function refreshNativeMainForwardAuth(args: {
1963
2075
  | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
1964
2076
  | { ok: false; response: Response }
1965
2077
  > {
1966
- const { req, route, authCtx, substituteMainCredential, options } = args;
2078
+ const { req, config, route, authCtx, substituteMainCredential, options } = args;
1967
2079
  if (authCtx.kind !== "main-pool") {
1968
2080
  return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
1969
2081
  }
@@ -1986,6 +2098,9 @@ async function refreshNativeMainForwardAuth(args: {
1986
2098
  route.codexAccountMode,
1987
2099
  );
1988
2100
  const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
2101
+ admission: options.admission,
2102
+ config: options.codexAuthPolicy ?? config,
2103
+ modelId: route.modelId,
1989
2104
  substituteMainCredential,
1990
2105
  signal: options.abortSignal,
1991
2106
  nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
@@ -1995,7 +2110,9 @@ async function refreshNativeMainForwardAuth(args: {
1995
2110
  if (options.abortSignal?.aborted || req.signal.aborted) {
1996
2111
  return { ok: false, response: clientCancelledResponse() };
1997
2112
  }
1998
- return { ok: false, response: nativeMainRefreshFailureResponse(error) };
2113
+ return { ok: false, response: mapCodexAuthContextErrorToResponse(error, {
2114
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
2115
+ }) ?? nativeMainRefreshFailureResponse(error) };
1999
2116
  }
2000
2117
  }
2001
2118
 
@@ -2056,6 +2173,7 @@ async function applyFinalRouteRequestNormalization(args: {
2056
2173
 
2057
2174
  // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
2058
2175
  // this request will actually use (#404).
2176
+ route.provider = resolveOpenCodeGoTransport(route.provider, sessionLaneIdFromRequest(req.headers));
2059
2177
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
2060
2178
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
2061
2179
  logCtx.model = route.modelId;
@@ -2287,6 +2405,15 @@ export async function handleComboResponses(
2287
2405
  comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
2288
2406
  const initialNow = Date.now();
2289
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
+ });
2290
2417
 
2291
2418
  if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
2292
2419
  const recovery = agentTaskRecoveryConfig(config);
@@ -2304,9 +2431,7 @@ export async function handleComboResponses(
2304
2431
  );
2305
2432
  return unreadableEncryptedAgentTaskResponse();
2306
2433
  }
2307
- pick = pickComboTarget(config, comboId, {
2308
- eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
2309
- });
2434
+ pick = await pickWithWait({ now: initialNow });
2310
2435
  if (!pick) {
2311
2436
  discardEncryptedAgentTaskRecovery(
2312
2437
  req,
@@ -2314,7 +2439,9 @@ export async function handleComboResponses(
2314
2439
  config,
2315
2440
  { parentThreadId: inboundClientThreadId },
2316
2441
  );
2317
- return comboUnavailable(comboId);
2442
+ return options.abortSignal?.aborted
2443
+ ? clientCancelledResponse()
2444
+ : comboUnavailable(comboId);
2318
2445
  }
2319
2446
  let recovered = false;
2320
2447
  try {
@@ -2344,14 +2471,16 @@ export async function handleComboResponses(
2344
2471
  comboPayloadReadable = true;
2345
2472
  comboReplaySnapshot.recoveredPlaintext = true;
2346
2473
  } else {
2347
- pick = pickComboTarget(config, comboId, {
2348
- eligible: target => payloadEligible(target)
2349
- && !isComboTargetInCooldown(comboId, target, initialNow),
2474
+ pick = await pickWithWait({
2475
+ eligible: payloadEligible,
2476
+ now: initialNow,
2350
2477
  });
2351
2478
  }
2352
2479
 
2353
2480
  if (!pick) {
2354
- return comboUnavailable(comboId);
2481
+ return options.abortSignal?.aborted
2482
+ ? clientCancelledResponse()
2483
+ : comboUnavailable(comboId);
2355
2484
  }
2356
2485
  // One immutable combo selection trace, before any child dispatch; child
2357
2486
  // adoption below must never replace it with a concrete child route trace.
@@ -2555,9 +2684,12 @@ export async function handleComboResponses(
2555
2684
  console.warn(
2556
2685
  `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
2557
2686
  );
2687
+ const failureNow = Date.now();
2558
2688
  const nextPick = advanceComboAfterFailure(config, pick, {
2559
2689
  retryAfter: failure.retryAfter,
2560
- now: Date.now(),
2690
+ resetAt: failure.resetAt,
2691
+ cooldownMs: combo.cooldownMs,
2692
+ now: failureNow,
2561
2693
  cooldownScope: comboFailureCooldownScope(failure.response.status, failure.classificationText, {
2562
2694
  code: failure.upstreamCode,
2563
2695
  }),
@@ -2566,8 +2698,19 @@ export async function handleComboResponses(
2566
2698
  code: failure.upstreamCode,
2567
2699
  message: failure.classificationText,
2568
2700
  });
2569
- if (!nextPick) adoptFailedChildLog(childLog);
2570
- 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
+ }
2571
2714
  }
2572
2715
  if (
2573
2716
  lastFailure?.status === 413
@@ -2685,7 +2828,13 @@ export async function handleResponses(
2685
2828
  const ownsBudget = options.translatorBudget === undefined;
2686
2829
  const translatorBudget = options.translatorBudget ?? createTranslatorBudget();
2687
2830
  try {
2688
- 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
+ });
2689
2838
  return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
2690
2839
  } catch (error) {
2691
2840
  if (ownsBudget) translatorBudget.dispose();
@@ -2722,10 +2871,22 @@ async function handleResponsesInner(
2722
2871
  }
2723
2872
  // An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
2724
2873
  // as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
2725
- const comboEffortRow = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
2874
+ const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
2726
2875
  && typeof (body as { model?: unknown }).model === "string"
2727
- ? parseRequestEffortRowId((body as { model: string }).model, config)
2728
- : 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
+ }
2729
2890
  if (comboEffortRow) {
2730
2891
  const raw = body as Record<string, unknown>;
2731
2892
  raw.model = comboEffortRow.baseId;
@@ -2791,7 +2952,15 @@ async function handleResponsesInner(
2791
2952
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
2792
2953
  try {
2793
2954
  parsed = parseRequest(body);
2794
- 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
+ }
2795
2964
  if (effortRow) {
2796
2965
  parsed.modelId = effortRow.baseId;
2797
2966
  parsed.options.reasoning = effortRow.effort;
@@ -3029,6 +3198,7 @@ async function handleResponsesInner(
3029
3198
  subagentFallbackAccountPreview,
3030
3199
  subagentFallbackModelEligibleAccountIdsForModel,
3031
3200
  fallbackChain,
3201
+ candidateRoute => canPassThroughEncryptedV2AgentTask(candidateRoute, inboundWire),
3032
3202
  );
3033
3203
  if (fallback) {
3034
3204
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
@@ -3058,7 +3228,8 @@ async function handleResponsesInner(
3058
3228
  previewSelectionAdmission?.release();
3059
3229
  }
3060
3230
 
3061
- // 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.
3062
3233
  if (
3063
3234
  inboundWire === "responses"
3064
3235
  &&
@@ -3067,6 +3238,7 @@ async function handleResponsesInner(
3067
3238
  && agentTaskRecovery
3068
3239
  && !isCanonicalOpenAiForwardProvider(route.provider)
3069
3240
  && !options.comboAttempt
3241
+ && !canPassThroughEncryptedV2AgentTask(route, inboundWire)
3070
3242
  ) {
3071
3243
  let recovered = false;
3072
3244
  try {
@@ -3190,9 +3362,16 @@ async function handleResponsesInner(
3190
3362
 
3191
3363
  if (options.abortSignal?.aborted) return clientCancelledResponse();
3192
3364
 
3193
- // Encrypted child tasks may only reach the canonical native backend. This check
3194
- // runs against the FINAL route so native-only fallback can rescue a routed primary.
3195
- 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
+ ) {
3196
3375
  return unreadableEncryptedAgentTaskResponse();
3197
3376
  }
3198
3377
 
@@ -3231,6 +3410,12 @@ async function handleResponsesInner(
3231
3410
  }
3232
3411
 
3233
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
+ }
3234
3419
  // Refuse an input that cannot plausibly fit the model context window before spending auth,
3235
3420
  // circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
3236
3421
  //
@@ -3308,6 +3493,13 @@ async function handleResponsesInner(
3308
3493
  // the request actually used, so a concurrent rotation cannot cool an innocent replacement.
3309
3494
  let genericFailoverAccountId: string | null = null;
3310
3495
  let genericFailovers = 0;
3496
+ /**
3497
+ * Config generation captured where the serving credential is RESOLVED, not where the
3498
+ * quota is written. A streaming turn is a long await, so a generation captured at write
3499
+ * time cannot see a config or account change that happened earlier in the same turn —
3500
+ * the case the fence exists for. Stays 0 for every provider without a passive quota.
3501
+ */
3502
+ let passiveQuotaWriterGeneration = 0;
3311
3503
  /**
3312
3504
  * Apply a rotated account's FULL credential snapshot to the live route (#2568d).
3313
3505
  *
@@ -3329,7 +3521,10 @@ async function handleResponsesInner(
3329
3521
  * tolerates project discovery failing, so a stored account can legitimately have no project;
3330
3522
  * sending that account's bearer with the FAILED account's project is worse than not rotating.
3331
3523
  */
3332
- const applyFailoverSnapshot = (snapshot: OAuthAccessSnapshot): boolean => {
3524
+ const applyFailoverSnapshot = (
3525
+ snapshot: OAuthAccessSnapshot,
3526
+ retryParsed: OcxParsedRequest = parsed,
3527
+ ): boolean => {
3333
3528
  if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
3334
3529
  let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
3335
3530
  if (route.providerName === "github-copilot") {
@@ -3342,7 +3537,14 @@ async function handleResponsesInner(
3342
3537
  }
3343
3538
  if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
3344
3539
  route.provider = rotatedProvider;
3345
- 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
+ }
3346
3548
  // Re-stamp: a request that rotated accounts must be attributed to the account that actually
3347
3549
  // served it. All three rotation sites funnel through here, so this is the only re-stamp
3348
3550
  // needed -- and putting it anywhere else would let one of the three drift.
@@ -3443,6 +3645,18 @@ async function handleResponsesInner(
3443
3645
  if (isGenericFailoverProvider(route.providerName, route.provider)) {
3444
3646
  genericFailoverAccountId = resolved.accountId;
3445
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
+ }
3656
+ // Captured beside the account it fences, so the two can never disagree.
3657
+ if (hasPassiveAccountQuota(route.providerName)) {
3658
+ passiveQuotaWriterGeneration = captureConfigGeneration();
3659
+ }
3446
3660
  if (route.providerName === "kiro") {
3447
3661
  // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
3448
3662
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
@@ -3553,6 +3767,17 @@ async function handleResponsesInner(
3553
3767
  }
3554
3768
  const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
3555
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
+
3556
3781
  if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
3557
3782
  return formatErrorResponse(
3558
3783
  400,
@@ -3571,6 +3796,8 @@ async function handleResponsesInner(
3571
3796
  req.headers,
3572
3797
  config,
3573
3798
  {
3799
+ admission: options.admission,
3800
+ codexAuthPolicy: options.codexAuthPolicy,
3574
3801
  // Account-qualified native routes are passthrough, so their in-turn helper is vision.
3575
3802
  // Scope its cooldown and outcome to the helper model, not the routed text model.
3576
3803
  ...(route.codexAccountId !== undefined
@@ -3600,11 +3827,12 @@ async function handleResponsesInner(
3600
3827
  // call must never plan another describe. The flag arrives from the Chat
3601
3828
  // surface (whose bridge rebuilds headers) or as the raw header for native
3602
3829
  // Responses callers. Marked + text-only routed model → strip, depth cap 1.
3603
- const visionDescribeTerminal = options.visionDescribeTerminal === true
3604
- || req.headers.get("x-opencodex-vision-describe") === "1";
3830
+ const visionDescribeTerminal = options.visionDescribeTerminal === true;
3605
3831
  const visionPlan = visionDescribeTerminal
3606
3832
  ? undefined
3607
- : 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
+ });
3608
3836
  const recordSidecarOutcome = openAiSidecar?.recordOutcome;
3609
3837
  if (visionPlan) {
3610
3838
  await describeImagesInPlace(
@@ -3779,7 +4007,7 @@ async function handleResponsesInner(
3779
4007
  // The guard needs a catalog to compare against, so it stands down when the request omits one.
3780
4008
  // An explicit empty catalog is still authoritative: it declares that no client tools may be
3781
4009
  // called. A passthrough request can legitimately omit `tools` entirely and still receive a call
3782
- // 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
3783
4011
  // `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
3784
4012
  // `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
3785
4013
  // too because the proxy cannot establish the caller's declared authorization boundary.
@@ -3888,7 +4116,27 @@ async function handleResponsesInner(
3888
4116
  // check sees nothing undeclared, and the refused turn enters continuation state anyway. So the
3889
4117
  // rejection is sticky for the whole turn, set from every parsed payload on the inspection side.
3890
4118
  let inspectionSawUndeclaredTool = false;
4119
+ const passiveQuotaObserved = hasPassiveAccountQuota(route.providerName)
4120
+ && route.provider.authMode === "oauth";
3891
4121
  const noteInspectedPayload = (payload: unknown) => {
4122
+ // Meta reports subscription usage ONLY as an in-stream event; there is no endpoint
4123
+ // to poll (003 §E probed 17 paths, all 404). Observed here rather than behind a
4124
+ // dedicated inspector handler because onParsedPayload already reaches every
4125
+ // passthrough shape -- eager relay and both tee consumers -- through this one
4126
+ // function.
4127
+ //
4128
+ // Placed BEFORE the undeclared-tool early return below, which is load-bearing: that
4129
+ // guard latches for the rest of the turn once it fires, and a turn that tripped it
4130
+ // still legitimately reports usage.
4131
+ if (passiveQuotaObserved && isMuseSubscriptionUsagePayload(payload)) {
4132
+ const quota = parseMuseSubscriptionUsage(payload);
4133
+ // Read at EVENT time, not at handler construction: failover rebinds this, and the
4134
+ // quota belongs to the account that actually served the turn.
4135
+ const servingAccountId = genericFailoverAccountId;
4136
+ if (quota && servingAccountId) {
4137
+ recordPassiveAccountQuota(route.providerName, servingAccountId, quota, passiveQuotaWriterGeneration);
4138
+ }
4139
+ }
3892
4140
  // Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth
3893
4141
  // provider) every name looks undeclared, and flipping this would stop recording continuation
3894
4142
  // state for exactly the passthrough traffic the guard deliberately stands down for.
@@ -4028,6 +4276,15 @@ async function handleResponsesInner(
4028
4276
  releaseCodexAuthContextProbeLease(authCtx);
4029
4277
  return clientCancelledResponse();
4030
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
+ }
4031
4288
  const outcome = classifyTransportFailureKind(err);
4032
4289
  // Host-level evidence stands regardless of pool membership: a direct
4033
4290
  // forward send has no pool accounting, but the reachability failure is
@@ -4079,6 +4336,9 @@ async function handleResponsesInner(
4079
4336
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4080
4337
  providerName: route.providerName,
4081
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,
4082
4342
  }),
4083
4343
  route.provider.authMode === "forward")
4084
4344
  // Every real attempt response — including an intermediate 5xx the
@@ -4152,6 +4412,9 @@ async function handleResponsesInner(
4152
4412
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4153
4413
  providerName: route.providerName,
4154
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,
4155
4418
  }),
4156
4419
  route.provider.authMode === "forward")
4157
4420
  .then(response => {
@@ -4181,10 +4444,10 @@ async function handleResponsesInner(
4181
4444
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
4182
4445
  const poolAuthCtx = authCtx.kind === "pool" ? authCtx : undefined;
4183
4446
  const poolReplay = poolAuthCtx
4184
- ? await refreshPoolForwardAuth({ req, route, authCtx: poolAuthCtx, substituteMainCredential, options })
4447
+ ? await refreshPoolForwardAuth({ req, config, route, authCtx: poolAuthCtx, substituteMainCredential, options })
4185
4448
  : undefined;
4186
4449
  const replay = poolReplay
4187
- ?? await refreshNativeMainForwardAuth({ req, route, authCtx, substituteMainCredential, options });
4450
+ ?? await refreshNativeMainForwardAuth({ req, config, route, authCtx, substituteMainCredential, options });
4188
4451
  if (!replay.ok) {
4189
4452
  // Compact already records this; core historically returned without recording,
4190
4453
  // so a dead grant stayed selectable and every request repeated the same doomed
@@ -4253,6 +4516,9 @@ async function handleResponsesInner(
4253
4516
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4254
4517
  providerName: route.providerName,
4255
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,
4256
4522
  }),
4257
4523
  codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
4258
4524
  ),
@@ -4359,6 +4625,9 @@ async function handleResponsesInner(
4359
4625
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4360
4626
  providerName: route.providerName,
4361
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,
4362
4631
  }),
4363
4632
  route.provider.authMode === "forward")
4364
4633
  .then(res => {
@@ -4375,6 +4644,43 @@ async function handleResponsesInner(
4375
4644
  }
4376
4645
  }
4377
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
+
4378
4684
  // Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
4379
4685
  // passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
4380
4686
  // key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
@@ -4421,6 +4727,9 @@ async function handleResponsesInner(
4421
4727
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4422
4728
  providerName: route.providerName,
4423
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,
4424
4733
  }),
4425
4734
  route.provider.authMode === "forward")
4426
4735
  .then(res => {
@@ -4578,11 +4887,10 @@ async function handleResponsesInner(
4578
4887
  // Prefer primary when present, fall back to secondary for compatibility.
4579
4888
  const quotaMeta = { ...codexQuotaOutcomeMeta(upstreamResponse), ...(await codexDenialOutcomeMeta(upstreamResponse)) };
4580
4889
  const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
4581
- applyAccountQuotaFromUpstreamHeaders(
4582
- authCtx.accountId,
4583
- upstreamResponse.headers,
4584
- authCtx.writerGeneration,
4585
- );
4890
+ if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
4891
+ applyAccountQuotaFromUpstreamHeaders(authCtx.accountId, upstreamResponse.headers,
4892
+ authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined);
4893
+ }
4586
4894
  if (terminalBodyWillRecord) {
4587
4895
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
4588
4896
  terminalRecorder(status, httpStatusOverride);
@@ -4836,7 +5144,10 @@ async function handleResponsesInner(
4836
5144
  },
4837
5145
  onClientCancel: () => options.onNativePassthroughCancel?.(),
4838
5146
  onDone: () => unregisterTurn(turnAc),
4839
- }, inlineEagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
5147
+ }, {
5148
+ clientGoneSignal: options.abortSignal,
5149
+ ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
5150
+ });
4840
5151
  // When selected, this relay closes response.completed even if upstream
4841
5152
  // keeps the connection alive. Marked Codex WS traffic, Windows
4842
5153
  // forced-rewrite traffic, and Darwin explicit eager traffic apply
@@ -4855,7 +5166,10 @@ async function handleResponsesInner(
4855
5166
  linkAbortSignal(upstream, turnAc.signal);
4856
5167
  registerTurn(turnAc, options.turnAdmissionLease);
4857
5168
  const inspectionConsumerOptions = {
4858
- 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,
4859
5173
  drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
4860
5174
  upstream,
4861
5175
  pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
@@ -5094,6 +5408,36 @@ async function handleResponsesInner(
5094
5408
  }
5095
5409
  }
5096
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
+
5097
5441
  // Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
5098
5442
  // Routed-compaction turns must NOT hit the image bridge: compaction clears tools/_webSearch but
5099
5443
  // leaves _imageGeneration, so planImageBridge would activate and return a normal Responses
@@ -5105,7 +5449,9 @@ async function handleResponsesInner(
5105
5449
  // - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
5106
5450
  // can proceed for web-search-only turns
5107
5451
  const wsPlan = !routedCompaction
5108
- ? 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
+ })
5109
5455
  : undefined;
5110
5456
  const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
5111
5457
  const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
@@ -5119,12 +5465,15 @@ async function handleResponsesInner(
5119
5465
  });
5120
5466
  if (rotated) {
5121
5467
  route.provider = rotated;
5122
- } else {
5123
- if (
5124
- !genericFailoverAccountId
5125
- || genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
5126
- || !isGenericOAuthFailoverEnabled(config, route.providerName)
5127
- ) 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
+ ) {
5128
5477
  const nextAccountId = rotateGenericOAuthAccountOn429(
5129
5478
  config,
5130
5479
  route.providerName,
@@ -5140,6 +5489,39 @@ async function handleResponsesInner(
5140
5489
  } catch {
5141
5490
  return null;
5142
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;
5143
5525
  }
5144
5526
  const rotatedAdapter = resolveAdapter(
5145
5527
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
@@ -6008,6 +6390,37 @@ async function handleResponsesInner(
6008
6390
  continue recovery;
6009
6391
  }
6010
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
+
6011
6424
  // Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
6012
6425
  // 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
6013
6426
  // so wait (Retry-After or the fixed interval) and replay the IDENTICAL request on the
@@ -6083,7 +6496,6 @@ async function handleResponsesInner(
6083
6496
  while (
6084
6497
  upstreamResponse.status === 429
6085
6498
  && anthropicPoolAccountId
6086
- && isAnthropicAccountPoolEnabled(config)
6087
6499
  && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
6088
6500
  ) {
6089
6501
  const nextAccountId = rotateAnthropicAccountOn429(
@@ -6494,7 +6906,6 @@ async function handleResponsesInner(
6494
6906
  if (
6495
6907
  response.status === 429
6496
6908
  && anthropicPoolAccountId
6497
- && isAnthropicAccountPoolEnabled(config)
6498
6909
  && anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
6499
6910
  ) {
6500
6911
  const nextAccountId = rotateAnthropicAccountOn429(
@@ -6525,6 +6936,48 @@ async function handleResponsesInner(
6525
6936
  }
6526
6937
  }
6527
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
+ }
6528
6981
  if (shouldAttemptImageTierRetry({
6529
6982
  status: response.status,
6530
6983
  adapterName: activeAdapter.name,