@yansigit/opencodex 2.33.1 → 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 (177) 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 -3
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  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 +9 -9
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-shell.ts +103 -6
  17. package/src/adapters/cursor/native-exec.ts +20 -0
  18. package/src/adapters/cursor/protobuf-request.ts +74 -39
  19. package/src/adapters/cursor/request-builder.ts +32 -10
  20. package/src/adapters/cursor/tool-definitions.ts +13 -10
  21. package/src/adapters/cursor/tool-result-normalize.ts +35 -3
  22. package/src/adapters/cursor/types.ts +16 -4
  23. package/src/adapters/cursor.ts +97 -70
  24. package/src/adapters/google-antigravity-replay.ts +105 -25
  25. package/src/adapters/google-antigravity-wire.ts +5 -0
  26. package/src/adapters/google-errors.ts +41 -12
  27. package/src/adapters/google-http.ts +12 -11
  28. package/src/adapters/google.ts +156 -65
  29. package/src/adapters/image.ts +1 -1
  30. package/src/adapters/kiro-constants.ts +15 -0
  31. package/src/adapters/kiro-tools.ts +43 -15
  32. package/src/adapters/kiro.ts +54 -9
  33. package/src/adapters/openai-chat.ts +286 -242
  34. package/src/adapters/openai-responses.ts +334 -24
  35. package/src/adapters/run-turn-queue.ts +36 -1
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -26
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +27 -7
  44. package/src/cli/account-extended.ts +10 -3
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/init.ts +1 -0
  52. package/src/cli/models-runtime.ts +95 -0
  53. package/src/cli/models.ts +13 -7
  54. package/src/cli/provider-runtime.ts +16 -2
  55. package/src/cli/registry.ts +6 -1
  56. package/src/cli/telemetry-commands.ts +25 -0
  57. package/src/cli/v2.ts +34 -10
  58. package/src/codex/account-pause.ts +2 -1
  59. package/src/codex/account-priority.ts +3 -2
  60. package/src/codex/app-server-processes.ts +80 -6
  61. package/src/codex/auth-api.ts +48 -8
  62. package/src/codex/auth-context.ts +21 -18
  63. package/src/codex/catalog/aggregation.ts +6 -0
  64. package/src/codex/catalog/model-metadata.ts +13 -1
  65. package/src/codex/catalog/native-models.ts +5 -2
  66. package/src/codex/catalog/parsing.ts +16 -0
  67. package/src/codex/catalog/provider-fetch.ts +20 -37
  68. package/src/codex/catalog/sync.ts +127 -2
  69. package/src/codex/catalog.ts +1 -1
  70. package/src/codex/codex-write-lock.ts +3 -1
  71. package/src/codex/convergence-types.ts +1 -1
  72. package/src/codex/convergence.ts +22 -2
  73. package/src/codex/desired-state.ts +2 -2
  74. package/src/codex/desktop-app-restart.ts +18 -5
  75. package/src/codex/inject-coordination.ts +83 -0
  76. package/src/codex/inject.ts +14 -1
  77. package/src/codex/log-guard/inspect.ts +22 -4
  78. package/src/codex/model-entitlements.ts +9 -2
  79. package/src/codex/prompt-layers.ts +371 -25
  80. package/src/codex/prompt-text-probe.ts +238 -0
  81. package/src/codex/quota.ts +123 -18
  82. package/src/codex/routing.ts +9 -0
  83. package/src/codex/subagent-model-fallback.ts +198 -27
  84. package/src/codex/transition-state.ts +107 -8
  85. package/src/combos/types.ts +10 -0
  86. package/src/compatibility/openai-responses.ts +33 -1
  87. package/src/config/autonomous-remediation.ts +21 -0
  88. package/src/config/provider-validation.ts +14 -0
  89. package/src/config/rebase-provenance.ts +68 -0
  90. package/src/config.ts +191 -17
  91. package/src/generated/compatibility-version.json +229 -153
  92. package/src/images/loop.ts +5 -4
  93. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  94. package/src/lab/fabric/producer-child.ts +1 -1
  95. package/src/lib/config-ownership.ts +20 -0
  96. package/src/lib/errors.ts +11 -2
  97. package/src/lib/package-tree-integrity.ts +101 -0
  98. package/src/oauth/aistudio-credentials.ts +65 -0
  99. package/src/oauth/aistudio-native-daemon.ts +54 -0
  100. package/src/oauth/generic-account-failover.ts +231 -0
  101. package/src/oauth/index.ts +57 -5
  102. package/src/oauth/key-providers.ts +10 -1
  103. package/src/oauth/kiro.ts +45 -0
  104. package/src/oauth/login-cli.ts +19 -20
  105. package/src/oauth/types.ts +15 -0
  106. package/src/providers/codex-capacity.ts +5 -2
  107. package/src/providers/command-code-efforts.ts +38 -6
  108. package/src/providers/context-cap.ts +4 -3
  109. package/src/providers/default-aliases.ts +65 -0
  110. package/src/providers/derive.ts +28 -0
  111. package/src/providers/fastwire.ts +7 -1
  112. package/src/providers/model-presets.ts +119 -0
  113. package/src/providers/new-model-policy.ts +146 -0
  114. package/src/providers/provider-id-rewrite.ts +2 -1
  115. package/src/providers/quota.ts +73 -14
  116. package/src/providers/registry.ts +160 -68
  117. package/src/providers/slug-codec.ts +52 -0
  118. package/src/responses/code-mode-helper-compat.ts +50 -0
  119. package/src/responses/custom-tool-compat.ts +34 -10
  120. package/src/responses/parser.ts +4 -0
  121. package/src/responses/schema.ts +5 -1
  122. package/src/responses/thought-signature-replay.ts +17 -0
  123. package/src/router.ts +40 -2
  124. package/src/routing/analytics.ts +1 -0
  125. package/src/routing/quota.ts +10 -0
  126. package/src/server/auth-cors.ts +23 -0
  127. package/src/server/chat-completions.ts +24 -16
  128. package/src/server/chat-native-sse.ts +3 -3
  129. package/src/server/chat-native.ts +30 -11
  130. package/src/server/claude-messages.ts +1 -1
  131. package/src/server/effort-policy.ts +16 -0
  132. package/src/server/index.ts +152 -80
  133. package/src/server/lifecycle.ts +52 -1
  134. package/src/server/management/agent-settings-routes.ts +31 -15
  135. package/src/server/management/codex-prompt-routes.ts +570 -0
  136. package/src/server/management/combo-routes.ts +2 -1
  137. package/src/server/management/config-routes.ts +27 -9
  138. package/src/server/management/context.ts +9 -0
  139. package/src/server/management/logs-usage-routes.ts +1 -1
  140. package/src/server/management/model-routes.ts +266 -0
  141. package/src/server/management/provider-routes.ts +137 -3
  142. package/src/server/management/routing-profile-routes.ts +2 -2
  143. package/src/server/management-api.ts +2 -0
  144. package/src/server/relay-eager.ts +147 -20
  145. package/src/server/relay.ts +251 -19
  146. package/src/server/request-log-conversation.ts +21 -0
  147. package/src/server/request-log.ts +46 -20
  148. package/src/server/responses/collaboration.ts +42 -5
  149. package/src/server/responses/combo-stream-preflight.ts +10 -3
  150. package/src/server/responses/core.ts +571 -137
  151. package/src/server/responses/empty-completion-guard.ts +35 -0
  152. package/src/server/responses/fetch-helpers.ts +14 -6
  153. package/src/server/responses/input-admission.ts +3 -1
  154. package/src/server/responses/passthrough-error.ts +33 -9
  155. package/src/server/responses/responses-field-backfill.ts +105 -13
  156. package/src/server/responses/ws-upstream.ts +35 -5
  157. package/src/server/responses-custom-tool-repair.ts +52 -7
  158. package/src/server/responses-terminal-repair.ts +25 -4
  159. package/src/server/sse-frame-buffer.ts +31 -4
  160. package/src/server/ws-bridge.ts +15 -4
  161. package/src/telemetry/dispatcher.ts +44 -0
  162. package/src/telemetry/fingerprint.ts +24 -0
  163. package/src/telemetry/hook.ts +43 -0
  164. package/src/telemetry/ledger.ts +54 -0
  165. package/src/telemetry/types.ts +23 -0
  166. package/src/types/config.ts +66 -14
  167. package/src/types/provider.ts +77 -0
  168. package/src/types/request.ts +16 -10
  169. package/src/types/tools.ts +20 -25
  170. package/src/types.ts +1 -0
  171. package/src/usage/expected-prices.ts +43 -24
  172. package/src/usage/log.ts +17 -7
  173. package/src/web-search/exa-executor.ts +40 -9
  174. package/src/web-search/loop.ts +5 -4
  175. package/gui/dist/assets/index-CIDo4y4k.js +0 -102
  176. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
  177. package/src/server/aistudio-ws-hub.ts +0 -295
@@ -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 {
@@ -324,7 +346,7 @@ import {
324
346
  payloadRewriteAsBlockRewrite,
325
347
  relaySseWithBlockRewrite,
326
348
  } from "../sse-payload-rewrite";
327
- import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
349
+ import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
328
350
  import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
329
351
  import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
330
352
  import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
@@ -351,6 +373,7 @@ import { responsesJsonToSseStream } from "../responses-json-events";
351
373
  import { guardTerminalEventStream } from "./terminal-guard";
352
374
  import {
353
375
  emptyCompletionRetryEnabled,
376
+ observeEmptyCompletion,
354
377
  guardEmptyCompletionEventStream,
355
378
  } from "./empty-completion-guard";
356
379
  import { preflightComboStreamResponse } from "./combo-stream-preflight";
@@ -719,6 +742,48 @@ export async function readDisplaySafeErrorText(
719
742
  }
720
743
  }
721
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
+
722
787
  function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
723
788
  parsed._stripReasoningEncryptedContent = true;
724
789
  }
@@ -930,11 +995,15 @@ export function codexAccountGatedCanonicalWireModel(modelId: string): string | u
930
995
  return undefined;
931
996
  }
932
997
 
933
- function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult): void {
998
+ function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult, logCtx?: RequestLogContext): void {
934
999
  if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
935
1000
  const wireModel = codexAccountGatedCanonicalWireModel(route.modelId);
936
1001
  if (!wireModel) return;
937
1002
 
1003
+ if (logCtx) {
1004
+ logCtx.preserveResolvedModelFromRoute = true;
1005
+ delete logCtx.resolvedModel;
1006
+ }
938
1007
  parsed.modelId = wireModel;
939
1008
  if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
940
1009
  const raw = parsed._rawBody as Record<string, unknown>;
@@ -1224,6 +1293,34 @@ export function codexForwardTerminalOutcomeRecorder(
1224
1293
  };
1225
1294
  }
1226
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
+
1227
1324
 
1228
1325
 
1229
1326
  export function decodeRequestErrorResponse(err: unknown, label: string): Response {
@@ -1288,6 +1385,10 @@ export interface HandleResponsesOptions {
1288
1385
  /** One-shot TTFT callback: first non-empty model output observed (WP4). */
1289
1386
  onFirstOutput?: () => void;
1290
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;
1291
1392
  recordTerminalOutcomes?: boolean;
1292
1393
  setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
1293
1394
  onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
@@ -1371,27 +1472,30 @@ export async function consumeComboFailure(
1371
1472
  let classificationText = fallback;
1372
1473
  let usage: OcxUsage | undefined;
1373
1474
  let upstreamCode: string | undefined;
1475
+ let upstreamMessage: string | undefined;
1476
+ let upstreamType: string | undefined;
1374
1477
  try {
1375
1478
  const body = await readBoundedResponseBody(response, { signal });
1376
1479
  usage = usageFromComboFailureText(body.text);
1377
1480
  if (body.displaySafe) {
1378
- const safeText = redactSecretString(body.text).slice(0, 500);
1379
- if (safeText) classificationText = safeText;
1380
- try {
1381
- const parsed = JSON.parse(body.text) as { error?: { code?: unknown } | string };
1382
- const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.code : undefined;
1383
- if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
1384
- } catch {
1385
- /* non-JSON upstream body — message-only classification */
1386
- }
1481
+ const normalized = normalizeUpstreamErrorText(body.text, fallback);
1482
+ classificationText = normalized.safeText;
1483
+ upstreamCode = normalized.code;
1484
+ upstreamMessage = normalized.message;
1485
+ upstreamType = normalized.type;
1387
1486
  }
1388
1487
  } catch (error) {
1389
1488
  if (signal?.aborted) throw error;
1390
1489
  classificationText = fallback;
1391
1490
  }
1392
- const message = classificationText === fallback
1393
- ? fallback
1394
- : `${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}`;
1395
1499
  const upstreamRetryAfter = response.headers.get("retry-after");
1396
1500
  // Client response may get the synthetic "2" fallback; cooldown metadata must not —
1397
1501
  // otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
@@ -1409,13 +1513,18 @@ export async function consumeComboFailure(
1409
1513
  includeDefault: false,
1410
1514
  });
1411
1515
  return {
1412
- response: formatErrorResponse(response.status, "upstream_error", message, {
1413
- ...(upstreamCode !== undefined ? { code: upstreamCode } : {}),
1414
- ...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
1415
- }),
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
+ ),
1416
1525
  classificationText,
1417
- ...(upstreamCode !== undefined ? { upstreamCode } : {}),
1418
- ...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
1526
+ ...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
1527
+ ...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
1419
1528
  ...(usage ? { usage } : {}),
1420
1529
  };
1421
1530
  }
@@ -1579,6 +1688,8 @@ async function resolveResponsesCodexAuth(
1579
1688
  modelId: route.modelId,
1580
1689
  substituteMainCredentialForDirect: substituteMainCredential,
1581
1690
  beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
1691
+ resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
1692
+ isDirectCallerEntitledToCodexModel: options.isDirectCallerEntitledToCodexModel,
1582
1693
  });
1583
1694
  options.onCodexAuthContextResolved?.(authCtx);
1584
1695
  } else {
@@ -1617,6 +1728,25 @@ async function resolveResponsesCodexAuth(
1617
1728
  }
1618
1729
  }
1619
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
+
1620
1750
  /**
1621
1751
  * Apply every route-dependent request mutation against the final selected route.
1622
1752
  * Must run only after subagent fallback has settled the model/provider.
@@ -1661,6 +1791,7 @@ async function applyFinalRouteRequestNormalization(args: {
1661
1791
  logCtx.provider = route.providerName;
1662
1792
  logCtx.providerAdapter = route.provider.adapter;
1663
1793
  logCtx.routeDecision = route.routeDecision;
1794
+ if (route.routeReason === "model-alias" || route.modelId !== responseModelId && responseModelId.includes("/")) logCtx.requestedAlias = responseModelId;
1664
1795
 
1665
1796
  if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
1666
1797
  parsed.stream = false;
@@ -1703,7 +1834,15 @@ async function applyFinalRouteRequestNormalization(args: {
1703
1834
  );
1704
1835
  const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
1705
1836
  const callerTier = parsed.options.serviceTier;
1706
- 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
+ );
1707
1846
  parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
1708
1847
  parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
1709
1848
  if (fastPolicy.capability === true && fastPolicy.fastWire === null) {
@@ -1760,6 +1899,19 @@ async function applyFinalRouteRequestNormalization(args: {
1760
1899
  }
1761
1900
  }
1762
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
+
1763
1915
  {
1764
1916
  const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
1765
1917
  const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
@@ -2256,6 +2408,7 @@ async function handleResponsesInner(
2256
2408
  (body as { input?: unknown } | undefined)?.input,
2257
2409
  );
2258
2410
  const inboundClientThreadId = inboundClientThreadIdFromRequest(req.headers);
2411
+ const cursorClientThreadId = codexPoolAffinityKey(req.headers);
2259
2412
  const originalBody = body;
2260
2413
  if (options.comboReplaySnapshot) {
2261
2414
  copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
@@ -2326,6 +2479,7 @@ async function handleResponsesInner(
2326
2479
  parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
2327
2480
  }
2328
2481
  }
2482
+ if (cursorClientThreadId) parsed._cursorClientThreadId = cursorClientThreadId;
2329
2483
  } catch (err) {
2330
2484
  if (isTranslatorBudgetExceededError(err)) {
2331
2485
  return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
@@ -2448,7 +2602,12 @@ async function handleResponsesInner(
2448
2602
  // also fail closed without polling quota upstream. Cached fallback state can still select a
2449
2603
  // provider with native continuation support below.
2450
2604
  const threadSpawn = isThreadSpawnRequest(req.headers);
2451
- 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)
2452
2611
  ? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
2453
2612
  : undefined;
2454
2613
  const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
@@ -2460,7 +2619,8 @@ async function handleResponsesInner(
2460
2619
  };
2461
2620
  let selectedForwardHeaders = req.headers;
2462
2621
  let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
2463
- let subagentFallbackPreviewAccountId: string | null | undefined;
2622
+ let subagentFallbackAccountPreview: SubagentPoolAccountPreview | undefined;
2623
+ let subagentFallbackModelEligibleAccountIdsForModel: SubagentModelEligibleAccountIds | undefined;
2464
2624
  let subagentQuotaFailureModel = parsed.modelId;
2465
2625
  const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
2466
2626
  const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
@@ -2478,28 +2638,47 @@ async function handleResponsesInner(
2478
2638
  // normalization (virtual models, effort caps, service tier, wire protocol).
2479
2639
  // Preview the preferred Codex account without acquiring a probe lease or refreshing
2480
2640
  // tokens — auth is resolved only after the final route is selected.
2481
- if (threadSpawn && !options.comboAttempt && route.codexAccountId === undefined) {
2641
+ if (
2642
+ threadSpawn
2643
+ && !options.comboAttempt
2644
+ && (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
2645
+ ) {
2482
2646
  // The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
2483
2647
  // so the preview must read the same scope slot — an undefined scope would map to the
2484
2648
  // "legacy" affinity bucket and never find a binding made under "shared" or a native
2485
2649
  // model scope, making the preview diverge from the account that actually authenticates.
2486
- 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(
2487
2659
  poolAffinityKey,
2488
2660
  config,
2489
- Date.now(),
2490
- codexQuotaScopeForModel(route.modelId),
2491
- 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),
2492
2669
  );
2493
- subagentFallbackPreviewAccountId = previewAccountId;
2494
2670
  subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
2495
2671
  const fallback = applySubagentModelFallback(
2496
2672
  parsed,
2497
2673
  req.headers,
2498
2674
  config,
2499
2675
  previewAccountId,
2500
- Date.now(),
2676
+ fallbackNow,
2501
2677
  unreadableEncryptedAgentTask,
2502
2678
  previewSelectionOptions,
2679
+ subagentFallbackAccountPreview,
2680
+ subagentFallbackModelEligibleAccountIdsForModel,
2681
+ fallbackChain,
2503
2682
  );
2504
2683
  if (fallback) {
2505
2684
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
@@ -2566,6 +2745,7 @@ async function handleResponsesInner(
2566
2745
  "_cursorConversationId",
2567
2746
  "_clientThreadId",
2568
2747
  "_promptCacheKeyIsSharedCohort",
2748
+ "_cursorClientThreadId",
2569
2749
  "_reasoningReplayScope",
2570
2750
  "_cursorIsolateConversation",
2571
2751
  ];
@@ -2583,15 +2763,47 @@ async function handleResponsesInner(
2583
2763
  // The ciphertext-only pass intentionally excludes routed candidates. Once recovery
2584
2764
  // makes the assignment readable, run selection again with the full configured chain
2585
2765
  // and keep the route in sync with any newly selected fallback.
2586
- const fallback = applySubagentModelFallback(
2587
- parsed,
2588
- req.headers,
2589
- config,
2590
- subagentFallbackPreviewAccountId,
2591
- Date.now(),
2592
- false,
2593
- previewSelectionOptions,
2594
- );
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
+ })();
2595
2807
  if (fallback) {
2596
2808
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
2597
2809
  (logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
@@ -2714,7 +2926,7 @@ async function handleResponsesInner(
2714
2926
  }
2715
2927
 
2716
2928
  route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
2717
- applyCodexAccountGatedWireNormalization(parsed, route);
2929
+ applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
2718
2930
  logCtx.provider = route.codexAccountNamespace
2719
2931
  ? `${route.providerName}-${route.codexAccountNamespace}`
2720
2932
  : formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
@@ -2754,6 +2966,42 @@ async function handleResponsesInner(
2754
2966
  : undefined;
2755
2967
  let cursorPoolAccountId: string | null = null;
2756
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
+ };
2757
3005
  const oauthSessionKeyParts = {
2758
3006
  sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2759
3007
  threadIdHeader: req.headers.get("thread-id"),
@@ -2795,6 +3043,9 @@ async function handleResponsesInner(
2795
3043
  promoteAnthropicActiveAccount(selection.accountId);
2796
3044
  route.provider = { ...route.provider, apiKey: accessToken };
2797
3045
  logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
3046
+ if (selection.accountId) {
3047
+ logCtx.accountLogLabel = selection.accountId;
3048
+ }
2798
3049
  } else if (
2799
3050
  route.providerName === "cursor"
2800
3051
  && route.provider.authMode === "oauth"
@@ -2825,6 +3076,9 @@ async function handleResponsesInner(
2825
3076
  };
2826
3077
  route.provider = { ...route.provider, apiKey: resolved.accessToken };
2827
3078
  logCtx.provider = formatCursorProviderForLog("cursor", selection.accountId);
3079
+ if (selection.accountId) {
3080
+ logCtx.accountLogLabel = selection.accountId;
3081
+ }
2828
3082
  } else {
2829
3083
  let resolved: OAuthAccessSnapshot;
2830
3084
  if (isAntigravityOAuth) {
@@ -2854,6 +3108,14 @@ async function handleResponsesInner(
2854
3108
  };
2855
3109
  if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
2856
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
+ }
2857
3119
  if (route.providerName === "kiro") {
2858
3120
  // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
2859
3121
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
@@ -2965,6 +3227,7 @@ async function handleResponsesInner(
2965
3227
  (logCtx.attempts ??= []).push(attempt);
2966
3228
  }
2967
3229
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
3230
+ let runTurnAdapter = adapter;
2968
3231
  if (adapter.runTurn) {
2969
3232
  recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
2970
3233
  }
@@ -3110,8 +3373,9 @@ async function handleResponsesInner(
3110
3373
  delete parsed.options.parallelToolCalls;
3111
3374
  // The compaction turn is a plain prose summary; a surviving structured-output format
3112
3375
  // would force schema-constrained JSON into the synthetic compaction item. The flag and
3113
- // the raw `text` controls go too: Kiro's capability guard reads both and would reject
3114
- // 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.)
3115
3379
  delete parsed.options.textFormat;
3116
3380
  delete parsed._structuredOutput;
3117
3381
  if (parsed._rawBody && typeof parsed._rawBody === "object") {
@@ -3183,7 +3447,9 @@ async function handleResponsesInner(
3183
3447
  // the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
3184
3448
  // it here escaped every catch up to the Bun handler, so the same request produced an
3185
3449
  // unstructured 500 — and no request log — depending only on whether a rotation ran first.
3186
- 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) {
3187
3453
  return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
3188
3454
  }
3189
3455
  throw error;
@@ -3304,10 +3570,16 @@ async function handleResponsesInner(
3304
3570
  const rememberPassthroughResponseChecked = rememberPassthroughResponse
3305
3571
  ? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
3306
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 };
3307
3579
  if (
3308
3580
  undeclaredToolGuardActive
3309
3581
  && undeclaredToolCallNameInResponse(
3310
- response,
3582
+ restoredResponse,
3311
3583
  declaredWireToolNames,
3312
3584
  declaredNamelessClientCallTypes,
3313
3585
  providerExecutedCallTypes,
@@ -3315,7 +3587,7 @@ async function handleResponsesInner(
3315
3587
  ) {
3316
3588
  return;
3317
3589
  }
3318
- rememberPassthroughResponse(response);
3590
+ rememberPassthroughResponse(restoredResponse);
3319
3591
  }
3320
3592
  : undefined;
3321
3593
  recordAdapterReasoning(logCtx, request);
@@ -3789,7 +4061,7 @@ async function handleResponsesInner(
3789
4061
  }
3790
4062
  const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
3791
4063
  const resolvedModel = headers.get("openai-model")?.trim();
3792
- if (resolvedModel) logCtx.resolvedModel = resolvedModel;
4064
+ if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
3793
4065
  if (isUsageDebugEnabled()) {
3794
4066
  const upstreamContentType = upstreamResponse.headers.get("content-type");
3795
4067
  if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
@@ -3830,18 +4102,15 @@ async function handleResponsesInner(
3830
4102
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
3831
4103
  terminalRecorder(status, httpStatusOverride);
3832
4104
  if (status === "failed") {
3833
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
3834
- || logCtx.terminalHttpStatus === 429
3835
- || logCtx.terminalHttpStatus === 402
3836
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
3837
- : undefined;
3838
- if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
3839
- recordSubagentQuotaFailureForThreadSpawn(
4105
+ if (!isFixedCodexAccount(authCtx)) {
4106
+ recordSubagentSpawnFailureForTerminal(
3840
4107
  req.headers,
3841
4108
  subagentQuotaFailureModel,
3842
- quotaFailureMessage,
4109
+ status,
3843
4110
  config,
3844
4111
  subagentFallbackAccountId,
4112
+ httpStatusOverride,
4113
+ logCtx,
3845
4114
  );
3846
4115
  }
3847
4116
  }
@@ -3964,6 +4233,7 @@ async function handleResponsesInner(
3964
4233
  routedCustomToolNames,
3965
4234
  translatorBudget,
3966
4235
  routedCustomToolRepairNames,
4236
+ declaredWireToolNames,
3967
4237
  )
3968
4238
  : undefined,
3969
4239
  routedToolSearchNames.size > 0
@@ -4013,19 +4283,17 @@ async function handleResponsesInner(
4013
4283
  const reportNativeTerminal = recordTerminalOutcomes
4014
4284
  ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
4015
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 });
4016
4287
  if (status === "failed") {
4017
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
4018
- || logCtx.terminalHttpStatus === 429
4019
- || logCtx.terminalHttpStatus === 402
4020
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
4021
- : undefined;
4022
- if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
4023
- recordSubagentQuotaFailureForThreadSpawn(
4288
+ if (!isFixedCodexAccount(authCtx)) {
4289
+ recordSubagentSpawnFailureForTerminal(
4024
4290
  req.headers,
4025
4291
  subagentQuotaFailureModel,
4026
- quotaFailureMessage,
4292
+ status,
4027
4293
  config,
4028
4294
  subagentFallbackAccountId,
4295
+ httpStatusOverride,
4296
+ logCtx,
4029
4297
  );
4030
4298
  }
4031
4299
  }
@@ -4096,19 +4364,17 @@ async function handleResponsesInner(
4096
4364
  // client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
4097
4365
  const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
4098
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 });
4099
4368
  if (status === "failed") {
4100
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
4101
- || logCtx.terminalHttpStatus === 429
4102
- || logCtx.terminalHttpStatus === 402
4103
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
4104
- : undefined;
4105
- if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
4106
- recordSubagentQuotaFailureForThreadSpawn(
4369
+ if (!isFixedCodexAccount(authCtx)) {
4370
+ recordSubagentSpawnFailureForTerminal(
4107
4371
  req.headers,
4108
4372
  subagentQuotaFailureModel,
4109
- quotaFailureMessage,
4373
+ status,
4110
4374
  config,
4111
4375
  subagentFallbackAccountId,
4376
+ httpStatusOverride,
4377
+ logCtx,
4112
4378
  );
4113
4379
  }
4114
4380
  }
@@ -4174,6 +4440,7 @@ async function handleResponsesInner(
4174
4440
  restoredNamespace,
4175
4441
  routedCustomToolNames,
4176
4442
  routedCustomToolRepairNames,
4443
+ declaredWireToolNames,
4177
4444
  );
4178
4445
  const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
4179
4446
  restored,
@@ -4340,6 +4607,49 @@ async function handleResponsesInner(
4340
4607
  : undefined;
4341
4608
  if (ccaInTurnGrounding) parsed._ccaInTurnGrounding = ccaInTurnGrounding;
4342
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
+ };
4343
4653
  if (hasMediaPlan && webSearchWinsMedia) {
4344
4654
  // Web search takes priority when both are active — the media bridge cannot run
4345
4655
  // alongside runWithWebSearch. Surface a runtime signal so the user knows their
@@ -4433,27 +4743,7 @@ async function handleResponsesInner(
4433
4743
  if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
4434
4744
  }
4435
4745
  },
4436
- on429: retryAfter => {
4437
- const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
4438
- retryAfter,
4439
- now: Date.now(),
4440
- attemptedKey: route.provider.apiKey,
4441
- promptCacheKey: parsed.options.promptCacheKey,
4442
- });
4443
- if (!rotated) return null;
4444
- route.provider = rotated;
4445
- const rotatedAdapter = resolveAdapter(
4446
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4447
- config.cacheRetention,
4448
- );
4449
- bindRouteReasoningReplayScope({
4450
- parsed,
4451
- providerName: route.providerName,
4452
- provider: route.provider,
4453
- adapterName: rotatedAdapter.name,
4454
- });
4455
- return rotatedAdapter;
4456
- },
4746
+ on429: rotateSidecarProviderOn429,
4457
4747
  retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
4458
4748
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
4459
4749
  ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
@@ -4524,27 +4814,7 @@ async function handleResponsesInner(
4524
4814
  routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
4525
4815
  stallTimeoutSec: wsPlan.stallTimeoutSec,
4526
4816
  streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
4527
- on429: retryAfter => {
4528
- const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
4529
- retryAfter,
4530
- now: Date.now(),
4531
- attemptedKey: route.provider.apiKey,
4532
- promptCacheKey: parsed.options.promptCacheKey,
4533
- });
4534
- if (!rotated) return null;
4535
- route.provider = rotated;
4536
- const rotatedAdapter = resolveAdapter(
4537
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4538
- config.cacheRetention,
4539
- );
4540
- bindRouteReasoningReplayScope({
4541
- parsed,
4542
- providerName: route.providerName,
4543
- provider: route.provider,
4544
- adapterName: rotatedAdapter.name,
4545
- });
4546
- return rotatedAdapter;
4547
- },
4817
+ on429: rotateSidecarProviderOn429,
4548
4818
  retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
4549
4819
  onCompletedResponse: commitReasoningReplayServingRoute,
4550
4820
  });
@@ -4620,7 +4890,7 @@ async function handleResponsesInner(
4620
4890
  pacingSlotAcquired: true,
4621
4891
  },
4622
4892
  );
4623
- await adapter.runTurn?.(
4893
+ await runTurnAdapter.runTurn?.(
4624
4894
  parsed,
4625
4895
  {
4626
4896
  headers: selectedForwardHeaders,
@@ -4653,6 +4923,77 @@ async function handleResponsesInner(
4653
4923
  }
4654
4924
  };
4655
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
+ };
4656
4997
  // The empty-completion retry re-runs the turn against a fresh queue: the
4657
4998
  // first queue is closed once its attempt settles, and pushing into it after
4658
4999
  // close is a silent no-op.
@@ -4670,6 +5011,11 @@ async function handleResponsesInner(
4670
5011
  let eventSource: AsyncIterable<AdapterEvent> = diagnoseAdapterEvents(
4671
5012
  queue.stream(), adapter.name, diagnosticRequestId, logCtx, adapterDiagnosticState,
4672
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
+ }
4673
5019
  if (options.comboAttempt) {
4674
5020
  const preflight = await preflightAdapterEvents(eventSource);
4675
5021
  if (preflight.error || preflight.empty) {
@@ -4687,7 +5033,16 @@ async function handleResponsesInner(
4687
5033
  // signal — run the adapter transport again against a fresh queue.
4688
5034
  continuation: runTurnRetrySource,
4689
5035
  })
4690
- : 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
+ });
4691
5046
  const sseStream = bridgeToResponsesSSE(
4692
5047
  guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
4693
5048
  () => {
@@ -4741,15 +5096,22 @@ async function handleResponsesInner(
4741
5096
 
4742
5097
  await runTurn();
4743
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
+ }
4744
5106
  let events: AdapterEvent[];
4745
5107
  if (emptyCompletionGuardEnabled) {
4746
5108
  events = [];
4747
5109
  for await (const event of guardEmptyCompletionEventStream({
4748
- firstEvents: (async function* () { yield* firstAttemptEvents; })(),
5110
+ firstEvents: (async function* () { yield* runTurnEvents; })(),
4749
5111
  continuation: runTurnRetrySource,
4750
5112
  })) events.push(event);
4751
5113
  } else {
4752
- events = firstAttemptEvents;
5114
+ events = runTurnEvents;
4753
5115
  }
4754
5116
  if (options.comboAttempt) {
4755
5117
  const firstMeaningful = events.find(event => event.type !== "heartbeat");
@@ -5195,6 +5557,7 @@ async function handleResponsesInner(
5195
5557
  invalidateSameTargetRequest();
5196
5558
  promoteAnthropicActiveAccount(nextAccountId);
5197
5559
  logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
5560
+ logCtx.accountLogLabel = nextAccountId;
5198
5561
  activeAdapter = resolveAdapter(
5199
5562
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5200
5563
  config.cacheRetention,
@@ -5253,6 +5616,7 @@ async function handleResponsesInner(
5253
5616
  forwardHeaders: selectedForwardHeaders,
5254
5617
  });
5255
5618
  logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5619
+ logCtx.accountLogLabel = nextAccountId;
5256
5620
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5257
5621
  const result = await rebuildAndRefetch("cursor-oauth-auth");
5258
5622
  if ("failed" in result) return result.failed;
@@ -5298,6 +5662,7 @@ async function handleResponsesInner(
5298
5662
  forwardHeaders: selectedForwardHeaders,
5299
5663
  });
5300
5664
  logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5665
+ logCtx.accountLogLabel = nextAccountId;
5301
5666
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5302
5667
  const result = await rebuildAndRefetch("cursor-oauth-429");
5303
5668
  if ("failed" in result) return result.failed;
@@ -5306,6 +5671,49 @@ async function handleResponsesInner(
5306
5671
  break;
5307
5672
  }
5308
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
+ }
5309
5717
  // Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
5310
5718
  // or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
5311
5719
  // the missing authoritative signal. Rebuild once through the same sanitation path used by a
@@ -5381,29 +5789,41 @@ async function handleResponsesInner(
5381
5789
  // Upstreams occasionally echo request details in error bodies — scrub token-shaped
5382
5790
  // material before it reaches the client-facing error surface.
5383
5791
  const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
5384
- const message = enrichOpenCodeZenRateLimitMessage(
5385
- `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
5386
- {
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({
5387
5814
  status: upstreamResponse.status,
5388
- providerName: route.providerName,
5389
- baseUrl: route.provider.baseUrl,
5390
- adapter: route.provider.adapter,
5391
- authMode: route.provider.authMode,
5392
- hasApiKey: Boolean(route.provider.apiKey?.trim()),
5815
+ message,
5393
5816
  upstreamRetryAfter,
5394
- // This recovery path is the HTTP Responses wire; custom runTurn transports
5395
- // never reach enrichOpenCodeZenRateLimitMessage here.
5396
- 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 } : {}),
5397
5825
  },
5398
5826
  );
5399
- const retryAfter = resolveClientRetryAfter({
5400
- status: upstreamResponse.status,
5401
- message,
5402
- upstreamRetryAfter,
5403
- });
5404
- return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
5405
- ...(retryAfter !== undefined ? { retryAfter } : {}),
5406
- });
5407
5827
  }
5408
5828
  }
5409
5829
 
@@ -5690,6 +6110,7 @@ async function handleResponsesInner(
5690
6110
  invalidateSameTargetRequest();
5691
6111
  promoteAnthropicActiveAccount(nextAccountId);
5692
6112
  logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
6113
+ logCtx.accountLogLabel = nextAccountId;
5693
6114
  activeAdapter = resolveAdapter(
5694
6115
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5695
6116
  config.cacheRetention,
@@ -5752,6 +6173,7 @@ async function handleResponsesInner(
5752
6173
  adapterName: activeAdapter.name,
5753
6174
  });
5754
6175
  logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
6176
+ logCtx.accountLogLabel = nextAccountId;
5755
6177
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5756
6178
  nextContinuationRecoveryKind = "cursor-oauth-auth";
5757
6179
  continue;
@@ -5801,6 +6223,7 @@ async function handleResponsesInner(
5801
6223
  adapterName: activeAdapter.name,
5802
6224
  });
5803
6225
  logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
6226
+ logCtx.accountLogLabel = nextAccountId;
5804
6227
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5805
6228
  nextContinuationRecoveryKind = "cursor-oauth-429";
5806
6229
  continue;
@@ -5826,10 +6249,21 @@ async function handleResponsesInner(
5826
6249
 
5827
6250
  if (!response.ok) {
5828
6251
  const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
6252
+ const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
5829
6253
  yield {
5830
6254
  type: "error",
5831
- status: response.status,
5832
- 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
+ : {}),
5833
6267
  };
5834
6268
  return;
5835
6269
  }