@yansigit/opencodex 2.33.0 → 2.35.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -15,6 +15,7 @@ import {
15
15
  } from "../../config";
16
16
  import { parseRequest } from "../../responses/parser";
17
17
  import { googleProviderOptionsRouteError } from "../../responses/google-provider-options";
18
+ import { interceptRuntimeFailure } from "../../telemetry/hook";
18
19
  import type { ProviderAdapter } from "../../adapters/base";
19
20
  import {
20
21
  bindReasoningReplayScope,
@@ -32,6 +33,7 @@ import {
32
33
  import { awaitThoughtSignatureDurability, thoughtSignatureReplaySalt } from "../../responses/thought-signature-replay";
33
34
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
34
35
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
36
+ import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
35
37
  import {
36
38
  copyPreviousResponseReplayProvenance,
37
39
  expandPreviousResponseInput,
@@ -74,6 +76,13 @@ import {
74
76
  } from "../../combos";
75
77
  import { isDebugEnabled, isInjectionDebugEnabled } from "../../lib/debug-settings";
76
78
  import { debugStreamDiagnostic } from "../../lib/debug";
79
+ import {
80
+ CYBER_POLICY_ERROR_CODE,
81
+ CYBER_POLICY_FALLBACK_MESSAGE,
82
+ adapterFailureFromMessage,
83
+ isCyberPolicyCode,
84
+ isCyberPolicyMessage,
85
+ } from "../../lib/errors";
77
86
  import { injectionDebugLog } from "../../lib/injection-debug-log";
78
87
  import { OcxRequestValidationError } from "../../lib/errors";
79
88
  import { resolveClientRetryAfter } from "../../lib/retry-after";
@@ -133,6 +142,13 @@ import {
133
142
  rotateCursorAccountOnAuth,
134
143
  } from "../../oauth/cursor-routing";
135
144
  import { getAccountCredential } from "../../oauth/store";
145
+ import {
146
+ failoverAccountSnapshot,
147
+ GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
148
+ isGenericFailoverProvider,
149
+ isGenericOAuthFailoverEnabled,
150
+ rotateGenericOAuthAccountOn429,
151
+ } from "../../oauth/generic-account-failover";
136
152
  import { buildWebSearchTool, mediaBridgeWillRun, planWebSearch, resolveCcaInTurnGrounding, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
137
153
  import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
138
154
  import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
@@ -157,10 +173,12 @@ import {
157
173
  } from "../../codex/auth-context";
158
174
  import {
159
175
  entitledCodexAccountIdsForModel,
176
+ isDirectCallerEntitledToCodexModel,
160
177
  invalidateCodexModelEntitlementsForAccount,
161
178
  resolveCodexModelEntitlements,
162
179
  } from "../../codex/model-entitlements";
163
180
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
181
+ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
164
182
  import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
165
183
  import {
166
184
  computeQuotaCooldown,
@@ -230,6 +248,10 @@ import {
230
248
  applySubagentModelFallback,
231
249
  maybePrimeSubagentQuota,
232
250
  recordSubagentQuotaFailureForThreadSpawn,
251
+ resolveSubagentFallbackChain,
252
+ subagentFallbackNeedsModelEntitlements,
253
+ type SubagentModelEligibleAccountIds,
254
+ type SubagentPoolAccountPreview,
233
255
  } from "../../codex/subagent-model-fallback";
234
256
  import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
235
257
  import {
@@ -249,6 +271,7 @@ import {
249
271
  } from "../request-log";
250
272
  import {
251
273
  conversationIdFromResponsesRequest,
274
+ inboundClientThreadIdFromRequest,
252
275
  normalizeLogConversationId,
253
276
  reasoningReplayConversationIdFromResponsesRequest,
254
277
  sessionIdHeaderFromRequest,
@@ -323,7 +346,7 @@ import {
323
346
  payloadRewriteAsBlockRewrite,
324
347
  relaySseWithBlockRewrite,
325
348
  } from "../sse-payload-rewrite";
326
- import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
349
+ import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
327
350
  import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
328
351
  import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
329
352
  import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
@@ -350,6 +373,7 @@ import { responsesJsonToSseStream } from "../responses-json-events";
350
373
  import { guardTerminalEventStream } from "./terminal-guard";
351
374
  import {
352
375
  emptyCompletionRetryEnabled,
376
+ observeEmptyCompletion,
353
377
  guardEmptyCompletionEventStream,
354
378
  } from "./empty-completion-guard";
355
379
  import { preflightComboStreamResponse } from "./combo-stream-preflight";
@@ -718,6 +742,48 @@ export async function readDisplaySafeErrorText(
718
742
  }
719
743
  }
720
744
 
745
+ interface NormalizedUpstreamErrorText {
746
+ safeText: string;
747
+ message?: string;
748
+ type?: string;
749
+ code?: string;
750
+ cyberPolicy: boolean;
751
+ }
752
+
753
+ /**
754
+ * Extract the structured provider error envelope without making `error.type` authoritative.
755
+ * Policy identity comes from the dedicated code (or the legacy message fallback); a credible
756
+ * upstream type is only carried through so callers do not erase provider diagnostics.
757
+ */
758
+ function normalizeUpstreamErrorText(text: string, fallback: string): NormalizedUpstreamErrorText {
759
+ const safeText = redactSecretString(text).slice(0, 500).trim() || fallback;
760
+ let message: string | undefined;
761
+ let type: string | undefined;
762
+ let code: string | undefined;
763
+ try {
764
+ const parsed = JSON.parse(text) as Record<string, unknown>;
765
+ const response = parsed.response && typeof parsed.response === "object" && !Array.isArray(parsed.response)
766
+ ? parsed.response as Record<string, unknown>
767
+ : undefined;
768
+ const candidates = [parsed.error, response?.error, response?.last_error, parsed.last_error, parsed];
769
+ const source = candidates.find((candidate): candidate is Record<string, unknown> => {
770
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) return false;
771
+ const record = candidate as Record<string, unknown>;
772
+ return [record.message, record.type, record.code].some(value => typeof value === "string");
773
+ });
774
+ if (!source) return { safeText, cyberPolicy: isCyberPolicyMessage(safeText) };
775
+ if (typeof source.message === "string" && source.message.trim()) {
776
+ message = redactSecretString(source.message.trim()).slice(0, 500);
777
+ }
778
+ if (typeof source.type === "string" && source.type.trim()) type = source.type.trim();
779
+ if (typeof source.code === "string" && source.code.trim()) code = source.code.trim();
780
+ } catch {
781
+ /* non-JSON upstream body — retain the bounded display-safe text */
782
+ }
783
+ const cyberPolicy = isCyberPolicyCode(code) || isCyberPolicyMessage(message ?? safeText);
784
+ return { safeText, message, type, code, cyberPolicy };
785
+ }
786
+
721
787
  function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
722
788
  parsed._stripReasoningEncryptedContent = true;
723
789
  }
@@ -929,11 +995,15 @@ export function codexAccountGatedCanonicalWireModel(modelId: string): string | u
929
995
  return undefined;
930
996
  }
931
997
 
932
- function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult): void {
998
+ function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult, logCtx?: RequestLogContext): void {
933
999
  if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
934
1000
  const wireModel = codexAccountGatedCanonicalWireModel(route.modelId);
935
1001
  if (!wireModel) return;
936
1002
 
1003
+ if (logCtx) {
1004
+ logCtx.preserveResolvedModelFromRoute = true;
1005
+ delete logCtx.resolvedModel;
1006
+ }
937
1007
  parsed.modelId = wireModel;
938
1008
  if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
939
1009
  const raw = parsed._rawBody as Record<string, unknown>;
@@ -1223,6 +1293,34 @@ export function codexForwardTerminalOutcomeRecorder(
1223
1293
  };
1224
1294
  }
1225
1295
 
1296
+ function subagentSpawnFailureMessageForTerminal(
1297
+ status: ResponsesTerminalStatus,
1298
+ httpStatusOverride?: number,
1299
+ logCtx?: RequestLogContext,
1300
+ ): string | number | undefined {
1301
+ if (status !== "failed") return undefined;
1302
+ const statusCode = httpStatusOverride ?? logCtx?.terminalHttpStatus;
1303
+ if (statusCode === 429 || statusCode === 402) return statusCode;
1304
+ if (typeof statusCode === "number" && statusCode >= 500 && statusCode < 600) return statusCode;
1305
+ const terminalMessage = logCtx?.upstreamError?.trim();
1306
+ if (terminalMessage) return terminalMessage;
1307
+ return undefined;
1308
+ }
1309
+
1310
+ function recordSubagentSpawnFailureForTerminal(
1311
+ headers: Headers,
1312
+ model: string,
1313
+ status: ResponsesTerminalStatus,
1314
+ config: OcxConfig,
1315
+ accountId: string | null,
1316
+ httpStatusOverride?: number,
1317
+ logCtx?: RequestLogContext,
1318
+ ): void {
1319
+ const failureMessage = subagentSpawnFailureMessageForTerminal(status, httpStatusOverride, logCtx);
1320
+ if (failureMessage === undefined) return;
1321
+ recordSubagentQuotaFailureForThreadSpawn(headers, model, failureMessage, config, accountId);
1322
+ }
1323
+
1226
1324
 
1227
1325
 
1228
1326
  export function decodeRequestErrorResponse(err: unknown, label: string): Response {
@@ -1287,6 +1385,10 @@ export interface HandleResponsesOptions {
1287
1385
  /** One-shot TTFT callback: first non-empty model output observed (WP4). */
1288
1386
  onFirstOutput?: () => void;
1289
1387
  onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
1388
+ /** Internal deterministic seam for account-gated native fallback tests. */
1389
+ resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
1390
+ /** Internal deterministic seam for Direct account-gated native auth tests. */
1391
+ isDirectCallerEntitledToCodexModel?: typeof isDirectCallerEntitledToCodexModel;
1290
1392
  recordTerminalOutcomes?: boolean;
1291
1393
  setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
1292
1394
  onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
@@ -1370,27 +1472,30 @@ export async function consumeComboFailure(
1370
1472
  let classificationText = fallback;
1371
1473
  let usage: OcxUsage | undefined;
1372
1474
  let upstreamCode: string | undefined;
1475
+ let upstreamMessage: string | undefined;
1476
+ let upstreamType: string | undefined;
1373
1477
  try {
1374
1478
  const body = await readBoundedResponseBody(response, { signal });
1375
1479
  usage = usageFromComboFailureText(body.text);
1376
1480
  if (body.displaySafe) {
1377
- const safeText = redactSecretString(body.text).slice(0, 500);
1378
- if (safeText) classificationText = safeText;
1379
- try {
1380
- const parsed = JSON.parse(body.text) as { error?: { code?: unknown } | string };
1381
- const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.code : undefined;
1382
- if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
1383
- } catch {
1384
- /* non-JSON upstream body — message-only classification */
1385
- }
1481
+ const normalized = normalizeUpstreamErrorText(body.text, fallback);
1482
+ classificationText = normalized.safeText;
1483
+ upstreamCode = normalized.code;
1484
+ upstreamMessage = normalized.message;
1485
+ upstreamType = normalized.type;
1386
1486
  }
1387
1487
  } catch (error) {
1388
1488
  if (signal?.aborted) throw error;
1389
1489
  classificationText = fallback;
1390
1490
  }
1391
- const message = classificationText === fallback
1392
- ? fallback
1393
- : `${fallback}: ${classificationText}`;
1491
+ const cyberFailure = isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(classificationText);
1492
+ const normalizedUpstreamCode = cyberFailure ? CYBER_POLICY_ERROR_CODE : upstreamCode;
1493
+ const message = cyberFailure
1494
+ ? upstreamMessage
1495
+ ?? (isCyberPolicyCode(upstreamCode) ? CYBER_POLICY_FALLBACK_MESSAGE : classificationText)
1496
+ : classificationText === fallback
1497
+ ? fallback
1498
+ : `${fallback}: ${classificationText}`;
1394
1499
  const upstreamRetryAfter = response.headers.get("retry-after");
1395
1500
  // Client response may get the synthetic "2" fallback; cooldown metadata must not —
1396
1501
  // otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
@@ -1408,13 +1513,18 @@ export async function consumeComboFailure(
1408
1513
  includeDefault: false,
1409
1514
  });
1410
1515
  return {
1411
- response: formatErrorResponse(response.status, "upstream_error", message, {
1412
- ...(upstreamCode !== undefined ? { code: upstreamCode } : {}),
1413
- ...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
1414
- }),
1516
+ response: formatErrorResponse(
1517
+ response.status,
1518
+ cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
1519
+ message,
1520
+ {
1521
+ ...(normalizedUpstreamCode !== undefined ? { code: normalizedUpstreamCode } : {}),
1522
+ ...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
1523
+ },
1524
+ ),
1415
1525
  classificationText,
1416
- ...(upstreamCode !== undefined ? { upstreamCode } : {}),
1417
- ...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
1526
+ ...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
1527
+ ...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
1418
1528
  ...(usage ? { usage } : {}),
1419
1529
  };
1420
1530
  }
@@ -1578,6 +1688,8 @@ async function resolveResponsesCodexAuth(
1578
1688
  modelId: route.modelId,
1579
1689
  substituteMainCredentialForDirect: substituteMainCredential,
1580
1690
  beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
1691
+ resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
1692
+ isDirectCallerEntitledToCodexModel: options.isDirectCallerEntitledToCodexModel,
1581
1693
  });
1582
1694
  options.onCodexAuthContextResolved?.(authCtx);
1583
1695
  } else {
@@ -1616,6 +1728,25 @@ async function resolveResponsesCodexAuth(
1616
1728
  }
1617
1729
  }
1618
1730
 
1731
+ async function resolveSubagentFallbackModelEligibility(args: {
1732
+ config: OcxConfig;
1733
+ fallbackChain: readonly string[] | null;
1734
+ nativeMainReadsForbidden: boolean;
1735
+ resolver: typeof resolveCodexModelEntitlements;
1736
+ }): Promise<SubagentModelEligibleAccountIds | undefined> {
1737
+ if (!subagentFallbackNeedsModelEntitlements(args.fallbackChain, args.config)) return undefined;
1738
+ const excludeAccountIds = args.nativeMainReadsForbidden
1739
+ ? new Set([MAIN_CODEX_ACCOUNT_ID])
1740
+ : undefined;
1741
+ const snapshot = await args.resolver(args.config, { excludeAccountIds });
1742
+ return (modelId) => {
1743
+ const entitledAccountIds = entitledCodexAccountIdsForModel(snapshot, modelId);
1744
+ return entitledAccountIds
1745
+ ? new Set([...entitledAccountIds].filter(accountId => !excludeAccountIds?.has(accountId)))
1746
+ : undefined;
1747
+ };
1748
+ }
1749
+
1619
1750
  /**
1620
1751
  * Apply every route-dependent request mutation against the final selected route.
1621
1752
  * Must run only after subagent fallback has settled the model/provider.
@@ -1660,6 +1791,7 @@ async function applyFinalRouteRequestNormalization(args: {
1660
1791
  logCtx.provider = route.providerName;
1661
1792
  logCtx.providerAdapter = route.provider.adapter;
1662
1793
  logCtx.routeDecision = route.routeDecision;
1794
+ if (route.routeReason === "model-alias" || route.modelId !== responseModelId && responseModelId.includes("/")) logCtx.requestedAlias = responseModelId;
1663
1795
 
1664
1796
  if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
1665
1797
  parsed.stream = false;
@@ -1702,7 +1834,15 @@ async function applyFinalRouteRequestNormalization(args: {
1702
1834
  );
1703
1835
  const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
1704
1836
  const callerTier = parsed.options.serviceTier;
1705
- parsed.options.tierObservation = tierObservationContext(fastPolicy, config.fastMode, callerTier);
1837
+ // The ChatGPT-internal Codex backend echoes `service_tier: "default"` even on turns it
1838
+ // scheduled as priority, so its echo cannot confirm OR deny Fast. Believing it reported every
1839
+ // Fast request as `response-declined` (#2558). The public API's echo stays authoritative.
1840
+ parsed.options.tierObservation = tierObservationContext(
1841
+ fastPolicy,
1842
+ config.fastMode,
1843
+ callerTier,
1844
+ isCanonicalOpenAiForwardProvider(route.provider) ? false : undefined,
1845
+ );
1706
1846
  parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
1707
1847
  parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
1708
1848
  if (fastPolicy.capability === true && fastPolicy.fastWire === null) {
@@ -1759,6 +1899,19 @@ async function applyFinalRouteRequestNormalization(args: {
1759
1899
  }
1760
1900
  }
1761
1901
 
1902
+ {
1903
+ const { sanitizeEffortForModel, supportedLadderFor } = await import("../effort-policy");
1904
+ const previousEffort = parsed.options.reasoning;
1905
+ const ladder = supportedLadderFor(route);
1906
+ if (ladder !== undefined && ladder.length === 0 && previousEffort) {
1907
+ sanitizeEffortForModel(parsed, ladder);
1908
+ logCtx.requestedEffort = `${logCtx.requestedEffort ?? previousEffort}->none`;
1909
+ if (isInjectionDebugEnabled()) {
1910
+ injectionDebugLog(`[opencodex] ${route.modelId}: stripped reasoning effort for effortless model`);
1911
+ }
1912
+ }
1913
+ }
1914
+
1762
1915
  {
1763
1916
  const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
1764
1917
  const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
@@ -1803,7 +1956,7 @@ export async function handleComboResponses(
1803
1956
  // Expand previous_response_id before image policy and child dispatch so a
1804
1957
  // continuation that only references prior images still fails closed when
1805
1958
  // imageInput is disabled (and so targets see the full replayed input).
1806
- const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
1959
+ const inboundClientThreadId = inboundClientThreadIdFromRequest(req.headers);
1807
1960
  const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
1808
1961
  const scopeMismatch = previousResponseScopeMismatch(body);
1809
1962
  if (scopeMismatch) {
@@ -2254,7 +2407,8 @@ async function handleResponsesInner(
2254
2407
  let unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
2255
2408
  (body as { input?: unknown } | undefined)?.input,
2256
2409
  );
2257
- const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
2410
+ const inboundClientThreadId = inboundClientThreadIdFromRequest(req.headers);
2411
+ const cursorClientThreadId = codexPoolAffinityKey(req.headers);
2258
2412
  const originalBody = body;
2259
2413
  if (options.comboReplaySnapshot) {
2260
2414
  copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
@@ -2325,6 +2479,7 @@ async function handleResponsesInner(
2325
2479
  parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
2326
2480
  }
2327
2481
  }
2482
+ if (cursorClientThreadId) parsed._cursorClientThreadId = cursorClientThreadId;
2328
2483
  } catch (err) {
2329
2484
  if (isTranslatorBudgetExceededError(err)) {
2330
2485
  return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
@@ -2447,7 +2602,12 @@ async function handleResponsesInner(
2447
2602
  // also fail closed without polling quota upstream. Cached fallback state can still select a
2448
2603
  // provider with native continuation support below.
2449
2604
  const threadSpawn = isThreadSpawnRequest(req.headers);
2450
- const previewSelectionAdmission = threadSpawn && route.codexAccountId === undefined
2605
+ const initialSubagentFallbackChain = threadSpawn && !options.comboAttempt
2606
+ ? resolveSubagentFallbackChain(parsed, config)
2607
+ : null;
2608
+ const previewSelectionAdmission = threadSpawn
2609
+ && !options.comboAttempt
2610
+ && (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
2451
2611
  ? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
2452
2612
  : undefined;
2453
2613
  const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
@@ -2459,7 +2619,8 @@ async function handleResponsesInner(
2459
2619
  };
2460
2620
  let selectedForwardHeaders = req.headers;
2461
2621
  let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
2462
- let subagentFallbackPreviewAccountId: string | null | undefined;
2622
+ let subagentFallbackAccountPreview: SubagentPoolAccountPreview | undefined;
2623
+ let subagentFallbackModelEligibleAccountIdsForModel: SubagentModelEligibleAccountIds | undefined;
2463
2624
  let subagentQuotaFailureModel = parsed.modelId;
2464
2625
  const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
2465
2626
  const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
@@ -2477,28 +2638,47 @@ async function handleResponsesInner(
2477
2638
  // normalization (virtual models, effort caps, service tier, wire protocol).
2478
2639
  // Preview the preferred Codex account without acquiring a probe lease or refreshing
2479
2640
  // tokens — auth is resolved only after the final route is selected.
2480
- if (threadSpawn && !options.comboAttempt && route.codexAccountId === undefined) {
2641
+ if (
2642
+ threadSpawn
2643
+ && !options.comboAttempt
2644
+ && (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
2645
+ ) {
2481
2646
  // The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
2482
2647
  // so the preview must read the same scope slot — an undefined scope would map to the
2483
2648
  // "legacy" affinity bucket and never find a binding made under "shared" or a native
2484
2649
  // model scope, making the preview diverge from the account that actually authenticates.
2485
- const previewAccountId = previewCodexAccountForRequest(
2650
+ const fallbackChain = initialSubagentFallbackChain;
2651
+ subagentFallbackModelEligibleAccountIdsForModel = await resolveSubagentFallbackModelEligibility({
2652
+ config,
2653
+ fallbackChain,
2654
+ nativeMainReadsForbidden,
2655
+ resolver: options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements,
2656
+ });
2657
+ const fallbackNow = Date.now();
2658
+ subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
2486
2659
  poolAffinityKey,
2487
2660
  config,
2488
- Date.now(),
2489
- codexQuotaScopeForModel(route.modelId),
2490
- previewSelectionOptions,
2661
+ previewNow,
2662
+ codexQuotaScopeForModel(modelId),
2663
+ { ...previewSelectionOptions, modelEligibleAccountIds },
2664
+ );
2665
+ const previewAccountId = route.codexAccountId ?? subagentFallbackAccountPreview(
2666
+ route.modelId,
2667
+ fallbackNow,
2668
+ subagentFallbackModelEligibleAccountIdsForModel?.(route.modelId),
2491
2669
  );
2492
- subagentFallbackPreviewAccountId = previewAccountId;
2493
2670
  subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
2494
2671
  const fallback = applySubagentModelFallback(
2495
2672
  parsed,
2496
2673
  req.headers,
2497
2674
  config,
2498
2675
  previewAccountId,
2499
- Date.now(),
2676
+ fallbackNow,
2500
2677
  unreadableEncryptedAgentTask,
2501
2678
  previewSelectionOptions,
2679
+ subagentFallbackAccountPreview,
2680
+ subagentFallbackModelEligibleAccountIdsForModel,
2681
+ fallbackChain,
2502
2682
  );
2503
2683
  if (fallback) {
2504
2684
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
@@ -2565,6 +2745,7 @@ async function handleResponsesInner(
2565
2745
  "_cursorConversationId",
2566
2746
  "_clientThreadId",
2567
2747
  "_promptCacheKeyIsSharedCohort",
2748
+ "_cursorClientThreadId",
2568
2749
  "_reasoningReplayScope",
2569
2750
  "_cursorIsolateConversation",
2570
2751
  ];
@@ -2582,15 +2763,47 @@ async function handleResponsesInner(
2582
2763
  // The ciphertext-only pass intentionally excludes routed candidates. Once recovery
2583
2764
  // makes the assignment readable, run selection again with the full configured chain
2584
2765
  // and keep the route in sync with any newly selected fallback.
2585
- const fallback = applySubagentModelFallback(
2586
- parsed,
2587
- req.headers,
2588
- config,
2589
- subagentFallbackPreviewAccountId,
2590
- Date.now(),
2591
- false,
2592
- previewSelectionOptions,
2593
- );
2766
+ const recoverySelectionAdmission = codexAccountSelectionForTurn(options.turnAdmissionLease)?.();
2767
+ const fallback = (() => {
2768
+ try {
2769
+ const recoveryNativeMainBlocked = isNativeMainTrafficBlocked();
2770
+ const recoverySelectionOptions = {
2771
+ nativeMainSelectionOnly: !recoveryNativeMainBlocked
2772
+ && recoverySelectionAdmission?.mainProfileDraining === true,
2773
+ };
2774
+ const recoveryNow = Date.now();
2775
+ // Carry the entitlement filter through recovery too (#2509/#2623). The scope was
2776
+ // already re-previewed per candidate here; the ELIGIBLE-ACCOUNT set was not, so a
2777
+ // recovered assignment could select an account that is not entitled to the model
2778
+ // and then fail closed at final auth — the same class of stale-selection bug as
2779
+ // the quota scope, one layer over.
2780
+ subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
2781
+ poolAffinityKey,
2782
+ config,
2783
+ previewNow,
2784
+ codexQuotaScopeForModel(modelId),
2785
+ { ...recoverySelectionOptions, modelEligibleAccountIds },
2786
+ );
2787
+ const recoveryPreviewAccountId = subagentFallbackAccountPreview(
2788
+ parsed.modelId,
2789
+ recoveryNow,
2790
+ subagentFallbackModelEligibleAccountIdsForModel?.(parsed.modelId),
2791
+ );
2792
+ return applySubagentModelFallback(
2793
+ parsed,
2794
+ req.headers,
2795
+ config,
2796
+ recoveryPreviewAccountId,
2797
+ recoveryNow,
2798
+ false,
2799
+ recoverySelectionOptions,
2800
+ subagentFallbackAccountPreview,
2801
+ subagentFallbackModelEligibleAccountIdsForModel,
2802
+ );
2803
+ } finally {
2804
+ recoverySelectionAdmission?.release();
2805
+ }
2806
+ })();
2594
2807
  if (fallback) {
2595
2808
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
2596
2809
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
@@ -2713,7 +2926,7 @@ async function handleResponsesInner(
2713
2926
  }
2714
2927
 
2715
2928
  route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
2716
- applyCodexAccountGatedWireNormalization(parsed, route);
2929
+ applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
2717
2930
  logCtx.provider = route.codexAccountNamespace
2718
2931
  ? `${route.providerName}-${route.codexAccountNamespace}`
2719
2932
  : formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
@@ -2753,6 +2966,42 @@ async function handleResponsesInner(
2753
2966
  : undefined;
2754
2967
  let cursorPoolAccountId: string | null = null;
2755
2968
  let cursorPoolFailovers = 0;
2969
+ // Generic OAuth rotation (#2568) for providers with no pool of their own. Bound to the account
2970
+ // the request actually used, so a concurrent rotation cannot cool an innocent replacement.
2971
+ let genericFailoverAccountId: string | null = null;
2972
+ let genericFailovers = 0;
2973
+ /**
2974
+ * Apply a rotated account's FULL credential snapshot to the live route (#2568d).
2975
+ *
2976
+ * One helper for all three rotation sites on purpose. Each site used to inline the same four
2977
+ * lines, and the divergence that produced was the bug: `apiKey` was swapped while the routing
2978
+ * metadata paired with it stayed behind.
2979
+ *
2980
+ * Returns false when the snapshot cannot be used safely, and the caller must then abandon the
2981
+ * rotation rather than send a half-applied identity:
2982
+ *
2983
+ * - Copilot pins its bearer to an account-scoped regional origin, so transport is re-resolved
2984
+ * with the new account's `apiBaseUrl` instead of inheriting the previous account's host.
2985
+ * - A Cloud Code Assist provider needs an account-matched project. Antigravity's refresh path
2986
+ * tolerates project discovery failing, so a stored account can legitimately have no project;
2987
+ * sending that account's bearer with the FAILED account's project is worse than not rotating.
2988
+ */
2989
+ const applyFailoverSnapshot = (snapshot: OAuthAccessSnapshot): boolean => {
2990
+ if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
2991
+ let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
2992
+ if (route.providerName === "github-copilot") {
2993
+ rotatedProvider = resolveProviderTransport(
2994
+ route.providerName,
2995
+ rotatedProvider,
2996
+ parsed.options.promptCacheKey,
2997
+ snapshot.apiBaseUrl,
2998
+ ) as OcxProviderConfig;
2999
+ }
3000
+ if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
3001
+ route.provider = rotatedProvider;
3002
+ if (route.providerName === "kiro") parsed._kiroAuthContext = { ...(snapshot.kiro ?? {}) };
3003
+ return true;
3004
+ };
2756
3005
  const oauthSessionKeyParts = {
2757
3006
  sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2758
3007
  threadIdHeader: req.headers.get("thread-id"),
@@ -2794,6 +3043,9 @@ async function handleResponsesInner(
2794
3043
  promoteAnthropicActiveAccount(selection.accountId);
2795
3044
  route.provider = { ...route.provider, apiKey: accessToken };
2796
3045
  logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
3046
+ if (selection.accountId) {
3047
+ logCtx.accountLogLabel = selection.accountId;
3048
+ }
2797
3049
  } else if (
2798
3050
  route.providerName === "cursor"
2799
3051
  && route.provider.authMode === "oauth"
@@ -2824,6 +3076,9 @@ async function handleResponsesInner(
2824
3076
  };
2825
3077
  route.provider = { ...route.provider, apiKey: resolved.accessToken };
2826
3078
  logCtx.provider = formatCursorProviderForLog("cursor", selection.accountId);
3079
+ if (selection.accountId) {
3080
+ logCtx.accountLogLabel = selection.accountId;
3081
+ }
2827
3082
  } else {
2828
3083
  let resolved: OAuthAccessSnapshot;
2829
3084
  if (isAntigravityOAuth) {
@@ -2853,6 +3108,14 @@ async function handleResponsesInner(
2853
3108
  };
2854
3109
  if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
2855
3110
  route.provider = { ...route.provider, apiKey: resolved.accessToken };
3111
+ if (resolved.accountId) {
3112
+ logCtx.accountLogLabel = resolved.accountId;
3113
+ }
3114
+ // Remember which account actually served this request so a 429 cools THAT one, not
3115
+ // whichever account is active by the time the response comes back (#2568).
3116
+ if (isGenericFailoverProvider(route.providerName, route.provider)) {
3117
+ genericFailoverAccountId = resolved.accountId;
3118
+ }
2856
3119
  if (route.providerName === "kiro") {
2857
3120
  // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
2858
3121
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
@@ -2964,6 +3227,7 @@ async function handleResponsesInner(
2964
3227
  (logCtx.attempts ??= []).push(attempt);
2965
3228
  }
2966
3229
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
3230
+ let runTurnAdapter = adapter;
2967
3231
  if (adapter.runTurn) {
2968
3232
  recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
2969
3233
  }
@@ -3109,8 +3373,9 @@ async function handleResponsesInner(
3109
3373
  delete parsed.options.parallelToolCalls;
3110
3374
  // The compaction turn is a plain prose summary; a surviving structured-output format
3111
3375
  // would force schema-constrained JSON into the synthetic compaction item. The flag and
3112
- // the raw `text` controls go too: Kiro's capability guard reads both and would reject
3113
- // the turn outright, and the key-mode openai-responses adapter builds from _rawBody.
3376
+ // the raw `text` control go too: the key-mode openai-responses adapter builds from
3377
+ // _rawBody, so a surviving format there would still reach the upstream. (The Kiro
3378
+ // guard no longer reads _rawBody.text; it refuses structured output only.)
3114
3379
  delete parsed.options.textFormat;
3115
3380
  delete parsed._structuredOutput;
3116
3381
  if (parsed._rawBody && typeof parsed._rawBody === "object") {
@@ -3182,7 +3447,9 @@ async function handleResponsesInner(
3182
3447
  // the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
3183
3448
  // it here escaped every catch up to the Bun handler, so the same request produced an
3184
3449
  // unstructured 500 — and no request log — depending only on whether a rotation ran first.
3185
- if (error instanceof NamespaceToolCollisionError) {
3450
+ // Same shape for a tool_choice this proxy cannot honor: the destination rejects a schema the
3451
+ // catalog had to drop, so the selector naming it is a client input error, not a 500.
3452
+ if (error instanceof NamespaceToolCollisionError || error instanceof XaiToolSchemaCompatibilityError) {
3186
3453
  return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
3187
3454
  }
3188
3455
  throw error;
@@ -3303,10 +3570,16 @@ async function handleResponsesInner(
3303
3570
  const rememberPassthroughResponseChecked = rememberPassthroughResponse
3304
3571
  ? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
3305
3572
  if (inspectionSawUndeclaredTool) return;
3573
+ const restoredResponse = restoreRoutedCustomCalls(
3574
+ response,
3575
+ routedCustomToolNames,
3576
+ routedCustomToolRepairNames,
3577
+ declaredWireToolNames,
3578
+ ).value as { id?: unknown; output?: unknown; status?: unknown };
3306
3579
  if (
3307
3580
  undeclaredToolGuardActive
3308
3581
  && undeclaredToolCallNameInResponse(
3309
- response,
3582
+ restoredResponse,
3310
3583
  declaredWireToolNames,
3311
3584
  declaredNamelessClientCallTypes,
3312
3585
  providerExecutedCallTypes,
@@ -3314,7 +3587,7 @@ async function handleResponsesInner(
3314
3587
  ) {
3315
3588
  return;
3316
3589
  }
3317
- rememberPassthroughResponse(response);
3590
+ rememberPassthroughResponse(restoredResponse);
3318
3591
  }
3319
3592
  : undefined;
3320
3593
  recordAdapterReasoning(logCtx, request);
@@ -3788,7 +4061,7 @@ async function handleResponsesInner(
3788
4061
  }
3789
4062
  const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
3790
4063
  const resolvedModel = headers.get("openai-model")?.trim();
3791
- if (resolvedModel) logCtx.resolvedModel = resolvedModel;
4064
+ if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
3792
4065
  if (isUsageDebugEnabled()) {
3793
4066
  const upstreamContentType = upstreamResponse.headers.get("content-type");
3794
4067
  if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
@@ -3829,18 +4102,15 @@ async function handleResponsesInner(
3829
4102
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
3830
4103
  terminalRecorder(status, httpStatusOverride);
3831
4104
  if (status === "failed") {
3832
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
3833
- || logCtx.terminalHttpStatus === 429
3834
- || logCtx.terminalHttpStatus === 402
3835
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
3836
- : undefined;
3837
- if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
3838
- recordSubagentQuotaFailureForThreadSpawn(
4105
+ if (!isFixedCodexAccount(authCtx)) {
4106
+ recordSubagentSpawnFailureForTerminal(
3839
4107
  req.headers,
3840
4108
  subagentQuotaFailureModel,
3841
- quotaFailureMessage,
4109
+ status,
3842
4110
  config,
3843
4111
  subagentFallbackAccountId,
4112
+ httpStatusOverride,
4113
+ logCtx,
3844
4114
  );
3845
4115
  }
3846
4116
  }
@@ -3963,6 +4233,7 @@ async function handleResponsesInner(
3963
4233
  routedCustomToolNames,
3964
4234
  translatorBudget,
3965
4235
  routedCustomToolRepairNames,
4236
+ declaredWireToolNames,
3966
4237
  )
3967
4238
  : undefined,
3968
4239
  routedToolSearchNames.size > 0
@@ -4012,19 +4283,17 @@ async function handleResponsesInner(
4012
4283
  const reportNativeTerminal = recordTerminalOutcomes
4013
4284
  ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
4014
4285
  terminalRecorder?.(status, httpStatusOverride);
4286
+ if (status === "failed") interceptRuntimeFailure(new Error(logCtx.upstreamError ?? "upstream response stream failed"), { provider: route.providerName, model: route.modelId, config: config.autonomousRemediation });
4015
4287
  if (status === "failed") {
4016
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
4017
- || logCtx.terminalHttpStatus === 429
4018
- || logCtx.terminalHttpStatus === 402
4019
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
4020
- : undefined;
4021
- if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
4022
- recordSubagentQuotaFailureForThreadSpawn(
4288
+ if (!isFixedCodexAccount(authCtx)) {
4289
+ recordSubagentSpawnFailureForTerminal(
4023
4290
  req.headers,
4024
4291
  subagentQuotaFailureModel,
4025
- quotaFailureMessage,
4292
+ status,
4026
4293
  config,
4027
4294
  subagentFallbackAccountId,
4295
+ httpStatusOverride,
4296
+ logCtx,
4028
4297
  );
4029
4298
  }
4030
4299
  }
@@ -4095,19 +4364,17 @@ async function handleResponsesInner(
4095
4364
  // client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
4096
4365
  const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
4097
4366
  terminalRecorder?.(status, httpStatusOverride);
4367
+ if (status === "failed") interceptRuntimeFailure(new Error(logCtx.upstreamError ?? "upstream response stream failed"), { provider: route.providerName, model: route.modelId, config: config.autonomousRemediation });
4098
4368
  if (status === "failed") {
4099
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
4100
- || logCtx.terminalHttpStatus === 429
4101
- || logCtx.terminalHttpStatus === 402
4102
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
4103
- : undefined;
4104
- if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
4105
- recordSubagentQuotaFailureForThreadSpawn(
4369
+ if (!isFixedCodexAccount(authCtx)) {
4370
+ recordSubagentSpawnFailureForTerminal(
4106
4371
  req.headers,
4107
4372
  subagentQuotaFailureModel,
4108
- quotaFailureMessage,
4373
+ status,
4109
4374
  config,
4110
4375
  subagentFallbackAccountId,
4376
+ httpStatusOverride,
4377
+ logCtx,
4111
4378
  );
4112
4379
  }
4113
4380
  }
@@ -4173,6 +4440,7 @@ async function handleResponsesInner(
4173
4440
  restoredNamespace,
4174
4441
  routedCustomToolNames,
4175
4442
  routedCustomToolRepairNames,
4443
+ declaredWireToolNames,
4176
4444
  );
4177
4445
  const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
4178
4446
  restored,
@@ -4339,6 +4607,49 @@ async function handleResponsesInner(
4339
4607
  : undefined;
4340
4608
  if (ccaInTurnGrounding) parsed._ccaInTurnGrounding = ccaInTurnGrounding;
4341
4609
  const canRunWebSearch = webSearchWinsMedia && !ccaInTurnGrounding;
4610
+ const rotateSidecarProviderOn429 = async (retryAfter: string | null): Promise<ProviderAdapter | null> => {
4611
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
4612
+ retryAfter,
4613
+ now: Date.now(),
4614
+ attemptedKey: route.provider.apiKey,
4615
+ promptCacheKey: parsed.options.promptCacheKey,
4616
+ });
4617
+ if (rotated) {
4618
+ route.provider = rotated;
4619
+ } else {
4620
+ if (
4621
+ !genericFailoverAccountId
4622
+ || genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
4623
+ || !isGenericOAuthFailoverEnabled(config, route.providerName)
4624
+ ) return null;
4625
+ const nextAccountId = rotateGenericOAuthAccountOn429(
4626
+ config,
4627
+ route.providerName,
4628
+ genericFailoverAccountId,
4629
+ retryAfter,
4630
+ );
4631
+ if (!nextAccountId) return null;
4632
+ try {
4633
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
4634
+ genericFailoverAccountId = nextAccountId;
4635
+ genericFailovers += 1;
4636
+ if (!applyFailoverSnapshot(snapshot)) return null;
4637
+ } catch {
4638
+ return null;
4639
+ }
4640
+ }
4641
+ const rotatedAdapter = resolveAdapter(
4642
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4643
+ config.cacheRetention,
4644
+ );
4645
+ bindRouteReasoningReplayScope({
4646
+ parsed,
4647
+ providerName: route.providerName,
4648
+ provider: route.provider,
4649
+ adapterName: rotatedAdapter.name,
4650
+ });
4651
+ return rotatedAdapter;
4652
+ };
4342
4653
  if (hasMediaPlan && webSearchWinsMedia) {
4343
4654
  // Web search takes priority when both are active — the media bridge cannot run
4344
4655
  // alongside runWithWebSearch. Surface a runtime signal so the user knows their
@@ -4432,27 +4743,7 @@ async function handleResponsesInner(
4432
4743
  if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
4433
4744
  }
4434
4745
  },
4435
- on429: retryAfter => {
4436
- const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
4437
- retryAfter,
4438
- now: Date.now(),
4439
- attemptedKey: route.provider.apiKey,
4440
- promptCacheKey: parsed.options.promptCacheKey,
4441
- });
4442
- if (!rotated) return null;
4443
- route.provider = rotated;
4444
- const rotatedAdapter = resolveAdapter(
4445
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4446
- config.cacheRetention,
4447
- );
4448
- bindRouteReasoningReplayScope({
4449
- parsed,
4450
- providerName: route.providerName,
4451
- provider: route.provider,
4452
- adapterName: rotatedAdapter.name,
4453
- });
4454
- return rotatedAdapter;
4455
- },
4746
+ on429: rotateSidecarProviderOn429,
4456
4747
  retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
4457
4748
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
4458
4749
  ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
@@ -4519,31 +4810,11 @@ async function handleResponsesInner(
4519
4810
  }
4520
4811
  },
4521
4812
  recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
4522
- connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
4813
+ connectTimeoutMs: config.connectTimeoutMs ?? Math.max(200_000, wsPlan.routedModelStallTimeoutMs),
4523
4814
  routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
4524
4815
  stallTimeoutSec: wsPlan.stallTimeoutSec,
4525
4816
  streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
4526
- on429: retryAfter => {
4527
- const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
4528
- retryAfter,
4529
- now: Date.now(),
4530
- attemptedKey: route.provider.apiKey,
4531
- promptCacheKey: parsed.options.promptCacheKey,
4532
- });
4533
- if (!rotated) return null;
4534
- route.provider = rotated;
4535
- const rotatedAdapter = resolveAdapter(
4536
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4537
- config.cacheRetention,
4538
- );
4539
- bindRouteReasoningReplayScope({
4540
- parsed,
4541
- providerName: route.providerName,
4542
- provider: route.provider,
4543
- adapterName: rotatedAdapter.name,
4544
- });
4545
- return rotatedAdapter;
4546
- },
4817
+ on429: rotateSidecarProviderOn429,
4547
4818
  retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
4548
4819
  onCompletedResponse: commitReasoningReplayServingRoute,
4549
4820
  });
@@ -4619,7 +4890,7 @@ async function handleResponsesInner(
4619
4890
  pacingSlotAcquired: true,
4620
4891
  },
4621
4892
  );
4622
- await adapter.runTurn?.(
4893
+ await runTurnAdapter.runTurn?.(
4623
4894
  parsed,
4624
4895
  {
4625
4896
  headers: selectedForwardHeaders,
@@ -4652,6 +4923,77 @@ async function handleResponsesInner(
4652
4923
  }
4653
4924
  };
4654
4925
  const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
4926
+ const rotateRunTurnAdapterOnPreflight429 = async (
4927
+ error: Extract<AdapterEvent, { type: "error" }>,
4928
+ ): Promise<boolean> => {
4929
+ const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus;
4930
+ if (
4931
+ status !== 429
4932
+ || !genericFailoverAccountId
4933
+ || genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
4934
+ || !isGenericOAuthFailoverEnabled(config, route.providerName)
4935
+ ) return false;
4936
+ const nextAccountId = rotateGenericOAuthAccountOn429(
4937
+ config,
4938
+ route.providerName,
4939
+ genericFailoverAccountId,
4940
+ null,
4941
+ );
4942
+ if (!nextAccountId) return false;
4943
+ try {
4944
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
4945
+ genericFailoverAccountId = nextAccountId;
4946
+ genericFailovers += 1;
4947
+ if (!applyFailoverSnapshot(snapshot)) return false;
4948
+ // A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
4949
+ // client-visible bytes, so replay is safe, but carrying its account identity into the next
4950
+ // account would not be. Let the rotated adapter derive a fresh identity and conversation.
4951
+ parsed._cursorIdentityScope = undefined;
4952
+ parsed._cursorConversationId = undefined;
4953
+ if (parsed._providerContinuation?.cursor) {
4954
+ const { cursor: _discardedCursor, ...otherProviderState } = parsed._providerContinuation;
4955
+ parsed._providerContinuation = otherProviderState;
4956
+ }
4957
+ const rotatedProvider = resolveWireProtocolOverride(
4958
+ route.providerName,
4959
+ route.modelId,
4960
+ route.provider,
4961
+ inboundWire,
4962
+ );
4963
+ const rotatedAdapter = resolveAdapter(rotatedProvider, config.cacheRetention);
4964
+ if (!rotatedAdapter.runTurn) return false;
4965
+ runTurnAdapter = rotatedAdapter;
4966
+ bindRouteReasoningReplayScope({
4967
+ parsed,
4968
+ providerName: route.providerName,
4969
+ provider: rotatedProvider,
4970
+ adapterName: rotatedAdapter.name,
4971
+ oauthCredentialSnapshot: { accountId: snapshot.accountId, generation: snapshot.generation },
4972
+ codexAuthContext: authCtx,
4973
+ forwardHeaders: selectedForwardHeaders,
4974
+ });
4975
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
4976
+ return true;
4977
+ } catch {
4978
+ return false;
4979
+ }
4980
+ };
4981
+ const preflightRunTurnFailover = async (
4982
+ firstSource: AsyncIterable<AdapterEvent>,
4983
+ ): Promise<AsyncIterable<AdapterEvent>> => {
4984
+ let source = firstSource;
4985
+ while (true) {
4986
+ const preflight = await preflightAdapterEvents(source);
4987
+ if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
4988
+ return preflight.stream;
4989
+ }
4990
+ const retryQueue = createAdapterEventQueue({
4991
+ onBacklogExceeded: () => runTurnAbort.abort(),
4992
+ });
4993
+ void runTurnAttempt(retryQueue, "oauth-account-429");
4994
+ source = retryQueue.stream();
4995
+ }
4996
+ };
4655
4997
  // The empty-completion retry re-runs the turn against a fresh queue: the
4656
4998
  // first queue is closed once its attempt settles, and pushing into it after
4657
4999
  // close is a silent no-op.
@@ -4669,6 +5011,11 @@ async function handleResponsesInner(
4669
5011
  let eventSource: AsyncIterable<AdapterEvent> = diagnoseAdapterEvents(
4670
5012
  queue.stream(), adapter.name, diagnosticRequestId, logCtx, adapterDiagnosticState,
4671
5013
  );
5014
+ if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
5015
+ // Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
5016
+ // replayed transparently; after any output reaches the bridge, a later error stays terminal.
5017
+ eventSource = await preflightRunTurnFailover(eventSource);
5018
+ }
4672
5019
  if (options.comboAttempt) {
4673
5020
  const preflight = await preflightAdapterEvents(eventSource);
4674
5021
  if (preflight.error || preflight.empty) {
@@ -4686,7 +5033,16 @@ async function handleResponsesInner(
4686
5033
  // signal — run the adapter transport again against a fresh queue.
4687
5034
  continuation: runTurnRetrySource,
4688
5035
  })
4689
- : eventSource;
5036
+ // Guard off (the default): leave the stream alone, but record that the turn ended
5037
+ // empty so the user has something to correlate instead of an unexplained blank
5038
+ // result (#2472). Retrying by default would re-send a turn that may already have had
5039
+ // billable side effects, so the honest default is observability, not recovery.
5040
+ : observeEmptyCompletion(eventSource, () => {
5041
+ console.warn(
5042
+ `[opencodex] ${route.providerName}/${route.modelId} completed with no output text `
5043
+ + "and no tool call. Set \"emptyCompletionRetry\": true to retry such turns once.",
5044
+ );
5045
+ });
4690
5046
  const sseStream = bridgeToResponsesSSE(
4691
5047
  guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
4692
5048
  () => {
@@ -4740,15 +5096,22 @@ async function handleResponsesInner(
4740
5096
 
4741
5097
  await runTurn();
4742
5098
  const firstAttemptEvents = await queue.collect();
5099
+ let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
5100
+ if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
5101
+ runTurnEvents = [];
5102
+ for await (const event of await preflightRunTurnFailover(
5103
+ (async function* () { yield* firstAttemptEvents; })(),
5104
+ )) runTurnEvents.push(event);
5105
+ }
4743
5106
  let events: AdapterEvent[];
4744
5107
  if (emptyCompletionGuardEnabled) {
4745
5108
  events = [];
4746
5109
  for await (const event of guardEmptyCompletionEventStream({
4747
- firstEvents: (async function* () { yield* firstAttemptEvents; })(),
5110
+ firstEvents: (async function* () { yield* runTurnEvents; })(),
4748
5111
  continuation: runTurnRetrySource,
4749
5112
  })) events.push(event);
4750
5113
  } else {
4751
- events = firstAttemptEvents;
5114
+ events = runTurnEvents;
4752
5115
  }
4753
5116
  if (options.comboAttempt) {
4754
5117
  const firstMeaningful = events.find(event => event.type !== "heartbeat");
@@ -5194,6 +5557,7 @@ async function handleResponsesInner(
5194
5557
  invalidateSameTargetRequest();
5195
5558
  promoteAnthropicActiveAccount(nextAccountId);
5196
5559
  logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
5560
+ logCtx.accountLogLabel = nextAccountId;
5197
5561
  activeAdapter = resolveAdapter(
5198
5562
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5199
5563
  config.cacheRetention,
@@ -5252,6 +5616,7 @@ async function handleResponsesInner(
5252
5616
  forwardHeaders: selectedForwardHeaders,
5253
5617
  });
5254
5618
  logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5619
+ logCtx.accountLogLabel = nextAccountId;
5255
5620
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5256
5621
  const result = await rebuildAndRefetch("cursor-oauth-auth");
5257
5622
  if ("failed" in result) return result.failed;
@@ -5297,6 +5662,7 @@ async function handleResponsesInner(
5297
5662
  forwardHeaders: selectedForwardHeaders,
5298
5663
  });
5299
5664
  logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5665
+ logCtx.accountLogLabel = nextAccountId;
5300
5666
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5301
5667
  const result = await rebuildAndRefetch("cursor-oauth-429");
5302
5668
  if ("failed" in result) return result.failed;
@@ -5305,6 +5671,49 @@ async function handleResponsesInner(
5305
5671
  break;
5306
5672
  }
5307
5673
  }
5674
+ // Generic OAuth account failover (#2568) for providers with no pool of their own.
5675
+ // Presence is consent since #2568d: rotation is ON by default once two or more eligible
5676
+ // accounts are stored for the provider, because a second deliberate login is read as the
5677
+ // operator asking for it. A single-account install is still a strict no-op, and an
5678
+ // explicit `oauthAccountFailover.enabled: false` (global or per provider) still wins --
5679
+ // see isGenericOAuthFailoverEnabled in src/oauth/generic-account-failover.ts. Codex and
5680
+ // Anthropic are excluded by isGenericFailoverProvider: their pools own quota scopes,
5681
+ // probe leases and affinity that this must not reimplement.
5682
+ while (
5683
+ upstreamResponse.status === 429
5684
+ && genericFailoverAccountId
5685
+ && genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
5686
+ && isGenericOAuthFailoverEnabled(config, route.providerName)
5687
+ ) {
5688
+ const nextAccountId = rotateGenericOAuthAccountOn429(
5689
+ config,
5690
+ route.providerName,
5691
+ genericFailoverAccountId,
5692
+ upstreamResponse.headers.get("retry-after"),
5693
+ );
5694
+ if (!nextAccountId) break;
5695
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
5696
+ try {
5697
+ // The FULL snapshot, not just the bearer: Antigravity pairs an account-matched
5698
+ // projectId with its token and Kiro carries routing metadata, so a token-only swap
5699
+ // would mix one account's credential with another's routing data.
5700
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
5701
+ genericFailoverAccountId = nextAccountId;
5702
+ genericFailovers += 1;
5703
+ if (!applyFailoverSnapshot(snapshot)) break;
5704
+ invalidateSameTargetRequest();
5705
+ activeAdapter = resolveAdapter(
5706
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5707
+ config.cacheRetention,
5708
+ );
5709
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5710
+ const result = await rebuildAndRefetch("oauth-account-429");
5711
+ if ("failed" in result) return result.failed;
5712
+ upstreamResponse = result;
5713
+ } catch {
5714
+ break;
5715
+ }
5716
+ }
5308
5717
  // Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
5309
5718
  // or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
5310
5719
  // the missing authoritative signal. Rebuild once through the same sanitation path used by a
@@ -5380,29 +5789,41 @@ async function handleResponsesInner(
5380
5789
  // Upstreams occasionally echo request details in error bodies — scrub token-shaped
5381
5790
  // material before it reaches the client-facing error surface.
5382
5791
  const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
5383
- const message = enrichOpenCodeZenRateLimitMessage(
5384
- `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
5385
- {
5792
+ const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
5793
+ const message = normalized.cyberPolicy
5794
+ ? normalized.message
5795
+ ?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
5796
+ : enrichOpenCodeZenRateLimitMessage(
5797
+ `Provider error ${upstreamResponse.status}: ${normalized.safeText}`,
5798
+ {
5799
+ status: upstreamResponse.status,
5800
+ providerName: route.providerName,
5801
+ baseUrl: route.provider.baseUrl,
5802
+ adapter: route.provider.adapter,
5803
+ authMode: route.provider.authMode,
5804
+ hasApiKey: Boolean(route.provider.apiKey?.trim()),
5805
+ upstreamRetryAfter,
5806
+ // This recovery path is the HTTP Responses wire; custom runTurn transports
5807
+ // never reach enrichOpenCodeZenRateLimitMessage here.
5808
+ supportsHttpSameKeyRetry: true,
5809
+ },
5810
+ );
5811
+ const retryAfter = normalized.cyberPolicy
5812
+ ? undefined
5813
+ : resolveClientRetryAfter({
5386
5814
  status: upstreamResponse.status,
5387
- providerName: route.providerName,
5388
- baseUrl: route.provider.baseUrl,
5389
- adapter: route.provider.adapter,
5390
- authMode: route.provider.authMode,
5391
- hasApiKey: Boolean(route.provider.apiKey?.trim()),
5815
+ message,
5392
5816
  upstreamRetryAfter,
5393
- // This recovery path is the HTTP Responses wire; custom runTurn transports
5394
- // never reach enrichOpenCodeZenRateLimitMessage here.
5395
- supportsHttpSameKeyRetry: true,
5817
+ });
5818
+ return formatErrorResponse(
5819
+ upstreamResponse.status,
5820
+ normalized.cyberPolicy ? (normalized.type ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
5821
+ message,
5822
+ {
5823
+ ...(normalized.cyberPolicy ? { code: CYBER_POLICY_ERROR_CODE } : {}),
5824
+ ...(retryAfter !== undefined ? { retryAfter } : {}),
5396
5825
  },
5397
5826
  );
5398
- const retryAfter = resolveClientRetryAfter({
5399
- status: upstreamResponse.status,
5400
- message,
5401
- upstreamRetryAfter,
5402
- });
5403
- return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
5404
- ...(retryAfter !== undefined ? { retryAfter } : {}),
5405
- });
5406
5827
  }
5407
5828
  }
5408
5829
 
@@ -5689,6 +6110,7 @@ async function handleResponsesInner(
5689
6110
  invalidateSameTargetRequest();
5690
6111
  promoteAnthropicActiveAccount(nextAccountId);
5691
6112
  logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
6113
+ logCtx.accountLogLabel = nextAccountId;
5692
6114
  activeAdapter = resolveAdapter(
5693
6115
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5694
6116
  config.cacheRetention,
@@ -5751,6 +6173,7 @@ async function handleResponsesInner(
5751
6173
  adapterName: activeAdapter.name,
5752
6174
  });
5753
6175
  logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
6176
+ logCtx.accountLogLabel = nextAccountId;
5754
6177
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5755
6178
  nextContinuationRecoveryKind = "cursor-oauth-auth";
5756
6179
  continue;
@@ -5800,6 +6223,7 @@ async function handleResponsesInner(
5800
6223
  adapterName: activeAdapter.name,
5801
6224
  });
5802
6225
  logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
6226
+ logCtx.accountLogLabel = nextAccountId;
5803
6227
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5804
6228
  nextContinuationRecoveryKind = "cursor-oauth-429";
5805
6229
  continue;
@@ -5825,10 +6249,21 @@ async function handleResponsesInner(
5825
6249
 
5826
6250
  if (!response.ok) {
5827
6251
  const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
6252
+ const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
5828
6253
  yield {
5829
6254
  type: "error",
5830
- status: response.status,
5831
- message: `Provider continuation error ${response.status}: ${redactSecretString(errorText.slice(0, 500))}`,
6255
+ status: normalized.cyberPolicy ? 400 : response.status,
6256
+ message: normalized.cyberPolicy
6257
+ ? normalized.message
6258
+ ?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
6259
+ : `Provider continuation error ${response.status}: ${normalized.safeText}`,
6260
+ ...(normalized.cyberPolicy
6261
+ ? {
6262
+ errorType: normalized.type ?? CYBER_POLICY_ERROR_CODE,
6263
+ code: CYBER_POLICY_ERROR_CODE,
6264
+ retryable: false,
6265
+ }
6266
+ : {}),
5832
6267
  };
5833
6268
  return;
5834
6269
  }