@bitkyc08/opencodex 2.43.0 → 2.44.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 (138) hide show
  1. package/README.md +25 -17
  2. package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
  3. package/gui/dist/assets/index-ltx3L-WS.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/command-code.ts +23 -2
  7. package/src/adapters/cursor/native-exec.ts +63 -11
  8. package/src/adapters/cursor/tool-definitions.ts +1 -1
  9. package/src/adapters/cursor/tool-schemas.ts +59 -0
  10. package/src/adapters/kiro.ts +54 -8
  11. package/src/adapters/openai-chat.ts +23 -6
  12. package/src/adapters/openai-responses.ts +3 -1
  13. package/src/adapters/opencode-go.ts +35 -0
  14. package/src/claude/agents-inject.ts +12 -5
  15. package/src/claude/desktop-3p-library.ts +89 -0
  16. package/src/claude/desktop-3p.ts +166 -95
  17. package/src/claude/desktop-discovery-inputs.ts +44 -0
  18. package/src/claude/desktop-profile.ts +1 -1
  19. package/src/claude/desktop-remote-store-artifact.ts +185 -0
  20. package/src/claude/desktop-remote-store-io.ts +91 -0
  21. package/src/claude/desktop-remote-store-state.ts +127 -0
  22. package/src/claude/desktop-remote-store.ts +269 -0
  23. package/src/claude/inbound-model-options.ts +10 -2
  24. package/src/claude/inbound-records.ts +7 -0
  25. package/src/claude/inbound.ts +1 -1
  26. package/src/claude/model-info.ts +16 -3
  27. package/src/cli/account-api.ts +7 -1
  28. package/src/cli/aside-profiles.ts +17 -0
  29. package/src/cli/capabilities.ts +39 -0
  30. package/src/cli/claude-agent-startup-sync.ts +3 -1
  31. package/src/cli/claude-desktop.ts +127 -18
  32. package/src/cli/connect.ts +27 -14
  33. package/src/cli/dispatch.ts +30 -16
  34. package/src/cli/export-command.ts +3 -1
  35. package/src/cli/index.ts +28 -16
  36. package/src/cli/integrations.ts +46 -12
  37. package/src/cli/opencode.ts +11 -4
  38. package/src/cli/runtime-api.ts +7 -2
  39. package/src/cli/uninstall-client-state.ts +78 -0
  40. package/src/client/connect.ts +476 -223
  41. package/src/client/hub-client.ts +81 -0
  42. package/src/client/hub-relay.ts +38 -6
  43. package/src/client/lifecycle-lock.ts +129 -0
  44. package/src/client/state.ts +71 -42
  45. package/src/clients/aside-profiles.ts +224 -0
  46. package/src/codex/account-lifecycle.ts +5 -9
  47. package/src/codex/auth-api.ts +50 -4
  48. package/src/codex/catalog/metadata.ts +0 -2
  49. package/src/codex/catalog/provider-fetch.ts +7 -5
  50. package/src/codex/catalog/sync.ts +88 -24
  51. package/src/codex/convergence.ts +2 -0
  52. package/src/codex/inject.ts +45 -14
  53. package/src/codex/journal.ts +6 -6
  54. package/src/codex/quota-refresh-outcome.ts +27 -0
  55. package/src/codex/shim.ts +7 -1
  56. package/src/combos/resolve.ts +10 -7
  57. package/src/config.ts +18 -5
  58. package/src/generated/compatibility-version.json +209 -105
  59. package/src/images/loop.ts +5 -2
  60. package/src/integrations/aside-profile-context.ts +270 -0
  61. package/src/integrations/aside-profile-journal.ts +229 -0
  62. package/src/integrations/aside-profiles.ts +176 -0
  63. package/src/integrations/catalog-refresh.ts +40 -0
  64. package/src/integrations/config-io.ts +16 -1
  65. package/src/integrations/owned-refresh.ts +14 -2
  66. package/src/integrations/state.ts +3 -1
  67. package/src/integrations/writer.ts +3 -1
  68. package/src/lab/fabric/producer-isolate.ts +84 -46
  69. package/src/lib/account-selection-events.ts +32 -0
  70. package/src/lib/errors.ts +9 -1
  71. package/src/lib/provider-outbound.ts +2 -45
  72. package/src/lib/proxy-env.ts +67 -0
  73. package/src/oauth/account-quota-rank.ts +9 -1
  74. package/src/oauth/anthropic-routing.ts +91 -23
  75. package/src/oauth/generic-account-failover.ts +15 -53
  76. package/src/oauth/index.ts +67 -68
  77. package/src/oauth/pool-settings-capability.ts +2 -1
  78. package/src/oauth/store.ts +91 -7
  79. package/src/oauth/types.ts +8 -0
  80. package/src/providers/api-key-selection.ts +110 -0
  81. package/src/providers/api-keys.ts +38 -31
  82. package/src/providers/context-cap.ts +27 -3
  83. package/src/providers/key-failover.ts +27 -17
  84. package/src/providers/provider-id-rewrite.ts +10 -8
  85. package/src/providers/registry.ts +1 -0
  86. package/src/responses/custom-tool-compat.ts +10 -0
  87. package/src/responses/function-call-compat.ts +173 -0
  88. package/src/responses/namespace-tool-compat.ts +27 -4
  89. package/src/responses/parser.ts +8 -0
  90. package/src/responses/task-input.ts +36 -0
  91. package/src/responses/tool-name-aliases.ts +79 -0
  92. package/src/router.ts +2 -0
  93. package/src/routing/capability.ts +10 -3
  94. package/src/routing/compatibility/assemble.ts +16 -1
  95. package/src/routing/evaluator.ts +5 -1
  96. package/src/server/auth-cors.ts +3 -0
  97. package/src/server/chat-native.ts +44 -9
  98. package/src/server/claude-messages.ts +71 -36
  99. package/src/server/grok-responses-snapshot-repair.ts +333 -0
  100. package/src/server/index.ts +26 -10
  101. package/src/server/management/account-selection-stream.ts +70 -0
  102. package/src/server/management/aside-profile-routes.ts +231 -0
  103. package/src/server/management/config-routes.ts +36 -41
  104. package/src/server/management/context.ts +3 -0
  105. package/src/server/management/integration-routes.ts +61 -3
  106. package/src/server/management/logs-usage-routes.ts +1 -0
  107. package/src/server/management/model-routes.ts +20 -9
  108. package/src/server/management/model-rows.ts +10 -2
  109. package/src/server/management/oauth-account-routes.ts +7 -0
  110. package/src/server/management/provider-routes.ts +10 -8
  111. package/src/server/management/route-registry.ts +20 -2
  112. package/src/server/management-auth.ts +16 -0
  113. package/src/server/relay-eager.ts +17 -5
  114. package/src/server/relay.ts +43 -4
  115. package/src/server/request-log.ts +29 -1
  116. package/src/server/responses/agent-task-recovery-cache.ts +11 -0
  117. package/src/server/responses/agent-task-recovery.ts +24 -4
  118. package/src/server/responses/codex-ws-pool.ts +4 -4
  119. package/src/server/responses/codex-ws-session.ts +2 -2
  120. package/src/server/responses/collaboration.ts +2 -1
  121. package/src/server/responses/combo-stream-preflight.ts +26 -7
  122. package/src/server/responses/core.ts +668 -151
  123. package/src/server/responses/fetch-helpers.ts +6 -1
  124. package/src/server/responses/ws-upstream.ts +8 -3
  125. package/src/server/responses-custom-tool-repair.ts +29 -9
  126. package/src/server/responses-function-tool-repair.ts +183 -0
  127. package/src/server/responses-snapshot-codec.ts +14 -0
  128. package/src/server/responses-snapshot-repair.ts +1 -13
  129. package/src/server/responses-undeclared-tool-guard.ts +1 -68
  130. package/src/server/sse-payload-rewrite.ts +3 -1
  131. package/src/service.ts +6 -2
  132. package/src/types/config.ts +23 -16
  133. package/src/types/provider.ts +14 -3
  134. package/src/types/request.ts +2 -0
  135. package/src/usage/log.ts +9 -0
  136. package/src/web-search/loop.ts +5 -2
  137. package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
  138. package/gui/dist/assets/index-Djowl68T.js +0 -112
@@ -118,18 +118,19 @@ import {
118
118
  type OAuthAccessSnapshot,
119
119
  UnsupportedOAuthProviderError,
120
120
  } from "../../oauth";
121
+ import { captureOAuthAccountSelection, commitOAuthAccountSelection, credentialGeneration, getAccountCredentialWithStatus } from "../../oauth/store";
121
122
  import {
122
123
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
123
124
  anthropicSessionKeyFromParts,
124
- bindAnthropicSessionAffinity,
125
+ commitAnthropicSelectionRouting,
125
126
  formatAnthropicProviderForLog,
126
- getAnthropicPoolAccessToken,
127
+ getAnthropicPoolAccessSnapshot,
127
128
  getAnthropicPoolRetryAfterSeconds,
128
129
  isAnthropicAccountPoolEnabled,
129
130
  hasAnthropicFailoverQuorum,
130
- promoteAnthropicActiveAccount,
131
131
  resolveAnthropicAccountForSession,
132
132
  rotateAnthropicAccountOn429,
133
+ type AnthropicAccountSelectionReason,
133
134
  } from "../../oauth/anthropic-routing";
134
135
  import { stampOAuthAccountLabel } from "../../providers/label";
135
136
  import {
@@ -241,6 +242,7 @@ import {
241
242
  type InboundWire,
242
243
  } from "../../providers/registry";
243
244
  import type { AdapterRequest, ProviderAdapter } from "../../adapters/base";
245
+ import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../../providers/api-key-selection";
244
246
  import {
245
247
  hasKeyPoolFailover,
246
248
  rateLimitRetryDelayMs,
@@ -263,6 +265,7 @@ import {
263
265
  import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
264
266
  import { readBoundedResponseBody } from "../../lib/bounded-body";
265
267
  import {
268
+ ENCRYPTED_FUNCTION_OUTPUT_REJECTION,
266
269
  isRateLimitOrQuotaFailureMessage,
267
270
  upstreamErrorMessageFromPayload,
268
271
  } from "../../lib/errors";
@@ -291,6 +294,7 @@ import {
291
294
  recordAttemptRequestedEffort,
292
295
  requestLogSpeedLabel,
293
296
  sealRequestAttemptIdentity,
297
+ recordAttemptCredentialSource,
294
298
  usageFromResponsesPayload,
295
299
  type RequestLogContext,
296
300
  } from "../request-log";
@@ -318,6 +322,7 @@ import {
318
322
  agentTaskRecoveryConfig,
319
323
  discardEncryptedAgentTaskRecovery,
320
324
  recoverEncryptedAgentTask,
325
+ restoreCachedEncryptedAgentTasks,
321
326
  } from "./agent-task-recovery";
322
327
  import { relaySseEagerBounded } from "../relay-eager";
323
328
  import {
@@ -355,7 +360,7 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
355
360
  import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
356
361
  import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
357
362
  import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
358
- import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel, storedPoolReplayDispatchNotifier } from "./fetch-helpers";
363
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel, storedPoolReplayDispatchNotifier, type ProviderFetchOptions } from "./fetch-helpers";
359
364
  import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
360
365
  import {
361
366
  acquireUpstreamHostAdmission,
@@ -367,6 +372,7 @@ import {
367
372
  upstreamHostHealthKey,
368
373
  type UpstreamHostAdmissionLease,
369
374
  } from "../../codex/upstream-host-health";
375
+ import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
370
376
  import {
371
377
  createResponsesSnapshotBlockRewrite,
372
378
  hasResponsesSnapshotRepair,
@@ -380,6 +386,8 @@ import {
380
386
  } from "../sse-payload-rewrite";
381
387
  import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
382
388
  import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
389
+ import { collectFunctionCallRepairSchemas, repairFunctionCallsInJson } from "../../responses/function-call-compat";
390
+ import { createResponsesFunctionToolRepairBlockRewrite } from "../responses-function-tool-repair";
383
391
  import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
384
392
  import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
385
393
  import {
@@ -638,28 +646,96 @@ const OPAQUE_RESPONSES_INPUT_TYPES = new Set([
638
646
  "compaction_summary",
639
647
  "context_compaction",
640
648
  ]);
649
+ const FUNCTION_OUTPUT_TYPES = new Set(["function_call_output", "custom_tool_call_output"]);
650
+ // codex-app subagent results replay as agent_message items whose content parts may carry
651
+ // backend-minted encrypted_content; the ChatGPT backend decrypts them in its function-output
652
+ // path, so a cross-identity replay of those parts produces ENCRYPTED_FUNCTION_OUTPUT_REJECTION.
653
+ const AGENT_MESSAGE_TYPE = "agent_message";
654
+
655
+ function encryptedFunctionOutputParts(output: unknown): boolean {
656
+ return Array.isArray(output) && output.some(part => (
657
+ part !== null
658
+ && typeof part === "object"
659
+ && !Array.isArray(part)
660
+ && (part as { type?: unknown }).type === "encrypted_content"
661
+ && typeof (part as { encrypted_content?: unknown }).encrypted_content === "string"
662
+ && (part as { encrypted_content: string }).encrypted_content.length > 0
663
+ ));
664
+ }
641
665
 
642
- function outboundResponsesBodyCarriesOpaqueBlob(bodyText: string | undefined): boolean {
643
- if (!bodyText) return false;
666
+ function outboundResponsesInput(bodyText: string | undefined): unknown[] | undefined {
667
+ if (!bodyText) return undefined;
644
668
  try {
645
669
  const body = JSON.parse(bodyText) as unknown;
646
- if (!body || typeof body !== "object" || Array.isArray(body)) return false;
670
+ if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
647
671
  const input = (body as { input?: unknown }).input;
648
- if (!Array.isArray(input)) return false;
649
- return input.some(item => {
650
- if (!item || typeof item !== "object" || Array.isArray(item)) return false;
651
- const candidate = item as { type?: unknown; encrypted_content?: unknown };
652
- return typeof candidate.type === "string"
653
- && OPAQUE_RESPONSES_INPUT_TYPES.has(candidate.type)
654
- && typeof candidate.encrypted_content === "string"
655
- && candidate.encrypted_content.length > 0;
656
- });
672
+ return Array.isArray(input) ? input : undefined;
673
+ } catch {
674
+ return undefined;
675
+ }
676
+ }
677
+
678
+ function outboundResponsesBodyCarriesEncryptedFunctionOutput(bodyText: string | undefined): boolean {
679
+ const input = outboundResponsesInput(bodyText);
680
+ if (!input) return false;
681
+ return input.some(item => {
682
+ if (item === null || typeof item !== "object" || Array.isArray(item)) return false;
683
+ const candidate = item as { type?: unknown; output?: unknown; content?: unknown };
684
+ const type = String(candidate.type ?? "");
685
+ if (FUNCTION_OUTPUT_TYPES.has(type) && encryptedFunctionOutputParts(candidate.output)) return true;
686
+ return type === AGENT_MESSAGE_TYPE && encryptedFunctionOutputParts(candidate.content);
687
+ });
688
+ }
689
+
690
+ function outboundResponsesBodyCarriesOpaqueBlob(bodyText: string | undefined): boolean {
691
+ const input = outboundResponsesInput(bodyText);
692
+ if (!input) return false;
693
+ return input.some(item => {
694
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
695
+ const candidate = item as { type?: unknown; encrypted_content?: unknown; output?: unknown };
696
+ if (
697
+ typeof candidate.type === "string"
698
+ && OPAQUE_RESPONSES_INPUT_TYPES.has(candidate.type)
699
+ && typeof candidate.encrypted_content === "string"
700
+ && candidate.encrypted_content.length > 0
701
+ ) return true;
702
+ if (
703
+ typeof candidate.type === "string"
704
+ && FUNCTION_OUTPUT_TYPES.has(candidate.type)
705
+ && encryptedFunctionOutputParts(candidate.output)
706
+ ) return true;
707
+ return candidate.type === AGENT_MESSAGE_TYPE
708
+ && encryptedFunctionOutputParts((candidate as { content?: unknown }).content);
709
+ });
710
+ }
711
+
712
+ function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
713
+ if (bodyText.trim() === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
714
+ try {
715
+ const payload = JSON.parse(bodyText) as unknown;
716
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
717
+ const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
718
+ if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
719
+ if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
720
+ if (record.error === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
721
+ return record.error !== null
722
+ && typeof record.error === "object"
723
+ && !Array.isArray(record.error)
724
+ && (record.error as { message?: unknown }).message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
657
725
  } catch {
658
726
  return false;
659
727
  }
660
728
  }
661
729
 
662
730
  function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
731
+ if (isEncryptedFunctionOutputRejection(bodyText)) return true;
732
+ try {
733
+ if (upstreamErrorMessageFromPayload(JSON.parse(bodyText) as unknown) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) {
734
+ return true;
735
+ }
736
+ } catch {
737
+ /* invalid JSON bodies fall through to the exact nested envelope checks */
738
+ }
663
739
  try {
664
740
  const payload = JSON.parse(bodyText) as unknown;
665
741
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
@@ -704,8 +780,13 @@ export function shouldAttemptOpaqueBlobRecovery(args: {
704
780
  errorBody: string;
705
781
  alreadyAttempted: boolean;
706
782
  }): boolean {
707
- return args.status >= 400
708
- && args.status < 500
783
+ const acceptedStatus = (args.status >= 400 && args.status < 500)
784
+ || (
785
+ args.status === 502
786
+ && outboundResponsesBodyCarriesEncryptedFunctionOutput(args.outboundBody)
787
+ && isEncryptedFunctionOutputRejection(args.errorBody)
788
+ );
789
+ return acceptedStatus
709
790
  && args.adapterName === "openai-responses"
710
791
  && !args.alreadyAttempted
711
792
  && outboundResponsesBodyCarriesOpaqueBlob(args.outboundBody)
@@ -721,7 +802,7 @@ async function opaqueBlobRejectionBodyForRecovery(
721
802
  ): Promise<string | undefined> {
722
803
  if (
723
804
  response.status < 400
724
- || response.status >= 500
805
+ || (response.status >= 500 && response.status !== 502)
725
806
  || adapterName !== "openai-responses"
726
807
  || alreadyAttempted
727
808
  || !outboundResponsesBodyCarriesOpaqueBlob(outboundBody)
@@ -798,6 +879,50 @@ function normalizeUpstreamErrorText(text: string, fallback: string): NormalizedU
798
879
 
799
880
  function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
800
881
  parsed._stripReasoningEncryptedContent = true;
882
+ const rawBody = parsed._rawBody;
883
+ if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) return;
884
+ const input = (rawBody as { input?: unknown }).input;
885
+ if (!Array.isArray(input)) return;
886
+ const stripEncryptedParts = (parts: unknown[]): unknown[] => {
887
+ let changed = false;
888
+ const stripped = parts.map(part => {
889
+ if (
890
+ part !== null
891
+ && typeof part === "object"
892
+ && !Array.isArray(part)
893
+ && (part as { type?: unknown }).type === "encrypted_content"
894
+ && typeof (part as { encrypted_content?: unknown }).encrypted_content === "string"
895
+ && (part as { encrypted_content: string }).encrypted_content.length > 0
896
+ ) {
897
+ changed = true;
898
+ return { type: "input_text", text: "[encrypted content omitted]" };
899
+ }
900
+ return part;
901
+ });
902
+ return changed ? stripped : parts;
903
+ };
904
+ const strippedInput = input.map(item => {
905
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
906
+ const record = item as Record<string, unknown>;
907
+ const type = String(record.type ?? "");
908
+ if (FUNCTION_OUTPUT_TYPES.has(type) && Array.isArray(record.output)) {
909
+ const output = stripEncryptedParts(record.output);
910
+ return output !== record.output ? { ...record, output } : item;
911
+ }
912
+ if (type === AGENT_MESSAGE_TYPE && Array.isArray(record.content)) {
913
+ const content = stripEncryptedParts(record.content);
914
+ return content !== record.content ? { ...record, content } : item;
915
+ }
916
+ return item;
917
+ });
918
+ Object.assign(rawBody, { input: strippedInput });
919
+ }
920
+
921
+ function resetStreamedOpaqueBlobLogContext(logCtx: RequestLogContext): void {
922
+ delete logCtx.upstreamError;
923
+ delete logCtx.terminalHttpStatus;
924
+ delete logCtx.terminalErrorCode;
925
+ delete logCtx.terminalIncompleteReason;
801
926
  }
802
927
 
803
928
  type OpaqueBlobRecoveryGuard = { attempted: boolean };
@@ -1300,6 +1425,7 @@ async function retryCodexPoolOnAlternateAccount(
1300
1425
  retryAdapter.name,
1301
1426
  logCtx.accountLogLabel,
1302
1427
  );
1428
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name);
1303
1429
 
1304
1430
  const retrySameConfirmedAccount = outcomeStatus === 400
1305
1431
  && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)
@@ -2403,19 +2529,11 @@ export async function handleComboResponses(
2403
2529
  let comboPayloadReadable = false;
2404
2530
  const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
2405
2531
  comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
2406
- const initialNow = Date.now();
2407
- let pick: ReturnType<typeof pickComboTarget> = null;
2408
- const pickWithWait = (pickOptions: {
2409
- exclude?: Iterable<string>;
2410
- eligible?: (target: NonNullable<typeof combo>["targets"][number]) => boolean;
2411
- now?: number;
2412
- }) => pickComboTargetWithWait(config, comboId, {
2413
- ...pickOptions,
2414
- waitForCooldownMs: combo.waitForCooldownMs,
2415
- abortSignal: options.abortSignal,
2416
- });
2417
-
2418
- if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
2532
+ let encryptedTaskRecoveryAttempted = false;
2533
+ let storedPool401ReplayDispatched = false;
2534
+ const recoverUnreadableEncryptedTask = async (): Promise<boolean> => {
2535
+ if (encryptedTaskRecoveryAttempted) return false;
2536
+ encryptedTaskRecoveryAttempted = true;
2419
2537
  const recovery = agentTaskRecoveryConfig(config);
2420
2538
  if (
2421
2539
  (options.inboundWire ?? "responses") !== "responses"
@@ -2429,19 +2547,7 @@ export async function handleComboResponses(
2429
2547
  config,
2430
2548
  { parentThreadId: inboundClientThreadId },
2431
2549
  );
2432
- return unreadableEncryptedAgentTaskResponse();
2433
- }
2434
- pick = await pickWithWait({ now: initialNow });
2435
- if (!pick) {
2436
- discardEncryptedAgentTaskRecovery(
2437
- req,
2438
- (body as { input?: unknown } | undefined)?.input,
2439
- config,
2440
- { parentThreadId: inboundClientThreadId },
2441
- );
2442
- return options.abortSignal?.aborted
2443
- ? clientCancelledResponse()
2444
- : comboUnavailable(comboId);
2550
+ return false;
2445
2551
  }
2446
2552
  let recovered = false;
2447
2553
  try {
@@ -2466,15 +2572,45 @@ export async function handleComboResponses(
2466
2572
  config,
2467
2573
  { parentThreadId: inboundClientThreadId },
2468
2574
  );
2469
- return unreadableEncryptedAgentTaskResponse();
2575
+ return false;
2470
2576
  }
2471
2577
  comboPayloadReadable = true;
2472
2578
  comboReplaySnapshot.recoveredPlaintext = true;
2473
- } else {
2474
- pick = await pickWithWait({
2475
- eligible: payloadEligible,
2476
- now: initialNow,
2477
- });
2579
+ return true;
2580
+ };
2581
+ const initialNow = Date.now();
2582
+ const pickWithWait = (pickOptions: {
2583
+ exclude?: Iterable<string>;
2584
+ eligible?: (target: NonNullable<typeof combo>["targets"][number]) => boolean;
2585
+ now?: number;
2586
+ }) => pickComboTargetWithWait(config, comboId, {
2587
+ ...pickOptions,
2588
+ waitForCooldownMs: combo.waitForCooldownMs,
2589
+ abortSignal: options.abortSignal,
2590
+ });
2591
+ let pick = await pickWithWait({
2592
+ eligible: payloadEligible,
2593
+ now: initialNow,
2594
+ });
2595
+
2596
+ if (unreadableEncryptedAgentTask && !pick) {
2597
+ pick = await pickWithWait({ now: initialNow });
2598
+ if (!pick) {
2599
+ discardEncryptedAgentTaskRecovery(
2600
+ req,
2601
+ (body as { input?: unknown } | undefined)?.input,
2602
+ config,
2603
+ { parentThreadId: inboundClientThreadId },
2604
+ );
2605
+ return options.abortSignal?.aborted
2606
+ ? clientCancelledResponse()
2607
+ : comboUnavailable(comboId);
2608
+ }
2609
+ if (!(await recoverUnreadableEncryptedTask())) {
2610
+ return options.abortSignal?.aborted
2611
+ ? clientCancelledResponse()
2612
+ : unreadableEncryptedAgentTaskResponse();
2613
+ }
2478
2614
  }
2479
2615
 
2480
2616
  if (!pick) {
@@ -2532,7 +2668,6 @@ export async function handleComboResponses(
2532
2668
  attemptRetained = true;
2533
2669
  };
2534
2670
  let consumedChildFailure: ConsumedComboFailure | undefined;
2535
- let storedPool401ReplayDispatched = false;
2536
2671
  const callbackGate = createChildPassthroughCallbackGate(options);
2537
2672
  let response: Response;
2538
2673
  try {
@@ -2665,13 +2800,36 @@ export async function handleComboResponses(
2665
2800
  (logCtx.attempts ??= []).push(attempt);
2666
2801
  attemptRetained = true;
2667
2802
  lastFailure = failure.response;
2803
+ const failureDecision = comboFailureDecision(failure.response.status, failure.classificationText, {
2804
+ code: failure.upstreamCode,
2805
+ });
2668
2806
  if (storedPool401ReplayDispatched) {
2807
+ if (failureDecision === "hop" && unreadableEncryptedAgentTask && !comboPayloadReadable) {
2808
+ const recoveredTarget = await pickWithWait({
2809
+ exclude: pick.attempted,
2810
+ eligible: target => {
2811
+ try {
2812
+ const route = routeConcreteModel(config, `${target.provider}/${target.model}`);
2813
+ return route.codexAccountMode === undefined
2814
+ && !isCanonicalOpenAiForwardProvider(route.provider);
2815
+ } catch {
2816
+ return false;
2817
+ }
2818
+ },
2819
+ });
2820
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
2821
+ if (recoveredTarget && await recoverUnreadableEncryptedTask()) {
2822
+ pick = recoveredTarget;
2823
+ continue;
2824
+ }
2825
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
2826
+ }
2827
+ // Keep the spent Pool budget sticky even after a recovered routed child:
2828
+ // no later failure may reopen ordinary combo/native account hopping.
2669
2829
  adoptFailedChildLog(childLog);
2670
2830
  return lastFailure;
2671
2831
  }
2672
- if (comboFailureDecision(failure.response.status, failure.classificationText, {
2673
- code: failure.upstreamCode,
2674
- }) === "stop") {
2832
+ if (failureDecision === "stop") {
2675
2833
  adoptFailedChildLog(childLog);
2676
2834
  if (
2677
2835
  failure.response.status === 413
@@ -2685,6 +2843,7 @@ export async function handleComboResponses(
2685
2843
  `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
2686
2844
  );
2687
2845
  const failureNow = Date.now();
2846
+ const attemptedTargets = pick.attempted;
2688
2847
  const nextPick = advanceComboAfterFailure(config, pick, {
2689
2848
  retryAfter: failure.retryAfter,
2690
2849
  resetAt: failure.resetAt,
@@ -2708,6 +2867,18 @@ export async function handleComboResponses(
2708
2867
  });
2709
2868
  }
2710
2869
  if (!pick) {
2870
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
2871
+ if (unreadableEncryptedAgentTask && !comboPayloadReadable) {
2872
+ const recoveredTarget = await pickWithWait({
2873
+ exclude: attemptedTargets,
2874
+ now: failureNow,
2875
+ });
2876
+ if (recoveredTarget && await recoverUnreadableEncryptedTask()) {
2877
+ pick = recoveredTarget;
2878
+ continue;
2879
+ }
2880
+ }
2881
+ // Waiting or recovery may have observed cancellation after the check above.
2711
2882
  if (options.abortSignal?.aborted) return clientCancelledResponse();
2712
2883
  adoptFailedChildLog(childLog);
2713
2884
  }
@@ -2952,6 +3123,7 @@ async function handleResponsesInner(
2952
3123
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
2953
3124
  try {
2954
3125
  parsed = parseRequest(body);
3126
+ parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
2955
3127
  // Captured before any parser mutates it, so both grammars see the client's id.
2956
3128
  const { fastRow, effortRow } = parseSyntheticRowId(parsed.modelId, config);
2957
3129
  if (fastRow) {
@@ -3234,14 +3406,18 @@ async function handleResponsesInner(
3234
3406
  inboundWire === "responses"
3235
3407
  &&
3236
3408
  threadSpawn
3237
- && unreadableEncryptedAgentTask
3238
3409
  && agentTaskRecovery
3239
3410
  && !isCanonicalOpenAiForwardProvider(route.provider)
3240
3411
  && !options.comboAttempt
3241
3412
  && !canPassThroughEncryptedV2AgentTask(route, inboundWire)
3242
3413
  ) {
3243
- let recovered = false;
3244
- try {
3414
+ let recovered = restoreCachedEncryptedAgentTasks(
3415
+ req, (body as { input?: unknown } | undefined)?.input, config, { parentThreadId },
3416
+ ) > 0;
3417
+ unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
3418
+ (body as { input?: unknown } | undefined)?.input,
3419
+ );
3420
+ if (unreadableEncryptedAgentTask) try {
3245
3421
  recovered = await recoverEncryptedAgentTask(
3246
3422
  req,
3247
3423
  (body as { input?: unknown } | undefined)?.input,
@@ -3266,6 +3442,7 @@ async function handleResponsesInner(
3266
3442
  "_providerContinuationOwner",
3267
3443
  "_cursorConversationId",
3268
3444
  "_clientThreadId",
3445
+ "_promptCacheKeyIsSharedCohort",
3269
3446
  "_cursorClientThreadId",
3270
3447
  "_reasoningReplayScope",
3271
3448
  "_cursorIsolateConversation",
@@ -3275,6 +3452,7 @@ async function handleResponsesInner(
3275
3452
  (reparsed as unknown as Record<string, unknown>)[key] = parsed[key];
3276
3453
  }
3277
3454
  }
3455
+ bindTurnTerminationScope(reparsed, resolvedConversationId);
3278
3456
  parsed = reparsed;
3279
3457
  // The recovery mutated `body.input` in place, so `_rawBody` now carries decrypted task
3280
3458
  // text. Bar it from the continuation cache before any recording path can reach it —
@@ -3483,8 +3661,12 @@ async function handleResponsesInner(
3483
3661
 
3484
3662
  // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
3485
3663
  // existing openai-chat / anthropic adapters authenticate with no change.
3486
- const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
3487
- && route.provider.authMode === "oauth";
3664
+ const isOAuth401ReplayProvider = (
3665
+ route.providerName === "xai"
3666
+ || route.providerName === "github-copilot"
3667
+ || route.providerName === "kiro"
3668
+ || route.providerName === "google-antigravity"
3669
+ ) && route.provider.authMode === "oauth";
3488
3670
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
3489
3671
  let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
3490
3672
  let anthropicPoolAccountId: string | null = null;
@@ -3493,6 +3675,82 @@ async function handleResponsesInner(
3493
3675
  // the request actually used, so a concurrent rotation cannot cool an innocent replacement.
3494
3676
  let genericFailoverAccountId: string | null = null;
3495
3677
  let genericFailovers = 0;
3678
+ let oauthSelection = route.provider.authMode === "oauth"
3679
+ ? captureOAuthAccountSelection(route.providerName) : null;
3680
+ let servingOAuthSnapshot: OAuthAccessSnapshot | undefined;
3681
+ // These owners also serve early passthrough and sidecar sends. A dispatch-time
3682
+ // rebuild must update every later builder, without entering a later block's TDZ.
3683
+ let adapter: ProviderAdapter;
3684
+ let activeAdapter: ProviderAdapter;
3685
+ let runTurnAdapter: ProviderAdapter;
3686
+ let sameTargetRequest: AdapterRequest | undefined;
3687
+ let sameTargetParsed: OcxParsedRequest | undefined;
3688
+ let sameTargetToken = 0;
3689
+ let transportToken = 0;
3690
+ let imageTierBias = 0;
3691
+ const invalidateSameTargetRequest = (): void => { transportToken += 1; };
3692
+ type DispatchBinding =
3693
+ | { kind: "oauth"; selection: NonNullable<typeof oauthSelection>; snapshot: OAuthAccessSnapshot }
3694
+ | { kind: "api-key"; provider: OcxProviderConfig };
3695
+ const requestBindings = new WeakMap<AdapterRequest, DispatchBinding>();
3696
+ const adapterBindings = new WeakMap<ProviderAdapter, DispatchBinding>();
3697
+ const rawRunTurns = new WeakMap<ProviderAdapter, NonNullable<ProviderAdapter["runTurn"]>>();
3698
+ const commitResolvedOAuthSelection = async (
3699
+ candidate: OAuthAccessSnapshot,
3700
+ proactive = false,
3701
+ anthropicReason?: AnthropicAccountSelectionReason,
3702
+ ): Promise<OAuthAccessSnapshot | null> => {
3703
+ const maxSelectionAttempts = 3;
3704
+ for (let attempt = 0; attempt < maxSelectionAttempts; attempt++) {
3705
+ if (!oauthSelection) return null;
3706
+ const proactiveEnabled = route.providerName === "anthropic"
3707
+ ? isAnthropicAccountPoolEnabled(config)
3708
+ : (config.providers[route.providerName]?.oauthAccountFailover?.enabled
3709
+ ?? config.oauthAccountFailover?.enabled) === true;
3710
+ if (proactive && candidate.accountId !== oauthSelection.accountId && !proactiveEnabled) {
3711
+ oauthSelection = captureOAuthAccountSelection(route.providerName);
3712
+ if (!oauthSelection) return null;
3713
+ candidate = route.providerName === "anthropic"
3714
+ ? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
3715
+ : await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
3716
+ }
3717
+ const committed = await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
3718
+ expectedSelection: oauthSelection,
3719
+ expectedCredentialGeneration: candidate.generation,
3720
+ requireUsableAccount: true,
3721
+ });
3722
+ if (committed) {
3723
+ if (route.providerName === "anthropic" && !commitAnthropicSelectionRouting(
3724
+ candidate.accountId, oauthSelection, committed,
3725
+ { config, sessionKey: anthropicSessionKey, reason: anthropicReason, expectedCredentialGeneration: candidate.generation },
3726
+ )) return null;
3727
+ oauthSelection = committed;
3728
+ servingOAuthSnapshot = candidate;
3729
+ forgetGenericFailoverRoster(route.providerName);
3730
+ return candidate;
3731
+ }
3732
+ // A newer manual choice wins over this request's old proposal, including A→B→A.
3733
+ // Resolve that choice, not the rejected candidate, before trying admission again.
3734
+ oauthSelection = captureOAuthAccountSelection(route.providerName);
3735
+ if (!oauthSelection) return null;
3736
+ candidate = route.providerName === "anthropic"
3737
+ ? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
3738
+ : await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
3739
+ if (route.provider.googleMode === "cloud-code-assist" && !candidate.projectId) return null;
3740
+ }
3741
+ return null;
3742
+ };
3743
+ const refreshResolvedOAuthSelection = async (sent: OAuthAccessSnapshot): Promise<OAuthAccessSnapshot> => {
3744
+ const current = captureOAuthAccountSelection(route.providerName);
3745
+ const unchanged = current?.accountId === oauthSelection?.accountId
3746
+ && current?.revision === oauthSelection?.revision;
3747
+ const candidate = unchanged ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
3748
+ const admitted = await commitResolvedOAuthSelection(candidate);
3749
+ if (!admitted) throw new Error("OAuth selection changed during credential recovery");
3750
+ genericFailoverAccountId = admitted.accountId;
3751
+ stampOAuthAccountLabel(logCtx, route.providerName, route.provider, admitted.accountId);
3752
+ return admitted;
3753
+ };
3496
3754
  /**
3497
3755
  * Config generation captured where the serving credential is RESOLVED, not where the
3498
3756
  * quota is written. A streaming turn is a long await, so a generation captured at write
@@ -3521,11 +3779,14 @@ async function handleResponsesInner(
3521
3779
  * tolerates project discovery failing, so a stored account can legitimately have no project;
3522
3780
  * sending that account's bearer with the FAILED account's project is worse than not rotating.
3523
3781
  */
3524
- const applyFailoverSnapshot = (
3782
+ const applyFailoverSnapshot = async (
3525
3783
  snapshot: OAuthAccessSnapshot,
3526
3784
  retryParsed: OcxParsedRequest = parsed,
3527
- ): boolean => {
3785
+ ): Promise<boolean> => {
3528
3786
  if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
3787
+ const committed = await commitResolvedOAuthSelection(snapshot);
3788
+ if (!committed) return false;
3789
+ snapshot = committed;
3529
3790
  let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
3530
3791
  if (route.providerName === "github-copilot") {
3531
3792
  rotatedProvider = resolveProviderTransport(
@@ -3549,8 +3810,149 @@ async function handleResponsesInner(
3549
3810
  // served it. All three rotation sites funnel through here, so this is the only re-stamp
3550
3811
  // needed -- and putting it anywhere else would let one of the three drift.
3551
3812
  stampOAuthAccountLabel(logCtx, route.providerName, route.provider, snapshot.accountId);
3813
+ if (route.providerName === "anthropic") {
3814
+ anthropicPoolAccountId = snapshot.accountId;
3815
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", snapshot.accountId, config);
3816
+ } else {
3817
+ genericFailoverAccountId = snapshot.accountId;
3818
+ }
3819
+ sentOAuthSnapshot = snapshot;
3820
+ replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
3552
3821
  return true;
3553
3822
  };
3823
+ const selectionIsCurrent = (binding: DispatchBinding | undefined): boolean => {
3824
+ if (route.provider.authMode === "forward") return true;
3825
+ if (!binding) return false;
3826
+ if (binding.kind === "api-key") return providerApiKeySelectionIsCurrent(config, route.providerName, binding.provider);
3827
+ const selected = captureOAuthAccountSelection(route.providerName);
3828
+ const row = getAccountCredentialWithStatus(route.providerName, binding.snapshot.accountId);
3829
+ return selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision
3830
+ && !!row && !row.needsReauth && row.credential.expires > Date.now()
3831
+ && credentialGeneration(row.credential) === binding.snapshot.generation;
3832
+ };
3833
+ const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
3834
+ const resolved = resolveAdapter(provider, retention);
3835
+ if (route.provider.authMode === "forward") return resolved;
3836
+ const binding: DispatchBinding | undefined = route.provider.authMode === "oauth"
3837
+ ? oauthSelection && servingOAuthSnapshot
3838
+ ? { kind: "oauth", selection: { ...oauthSelection }, snapshot: servingOAuthSnapshot }
3839
+ : undefined
3840
+ : { kind: "api-key", provider: { ...route.provider } };
3841
+ if (binding) adapterBindings.set(resolved, binding);
3842
+ const build = resolved.buildRequest.bind(resolved);
3843
+ resolved.buildRequest = async (requestParsed, incoming) => {
3844
+ const request = await build(requestParsed, incoming);
3845
+ // Capture at adapter creation, never from mutable serving state after an await.
3846
+ if (binding) requestBindings.set(request, binding);
3847
+ return request;
3848
+ };
3849
+ if (resolved.runTurn) {
3850
+ rawRunTurns.set(resolved, resolved.runTurn.bind(resolved));
3851
+ resolved.runTurn = (requestParsed, incoming, emit) => runSelectedTurn(resolved, requestParsed, incoming, emit);
3852
+ }
3853
+ return resolved;
3854
+ };
3855
+ const refreshDispatchAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
3856
+ if (route.provider.authMode === "oauth") {
3857
+ if (!servingOAuthSnapshot || !await applyFailoverSnapshot(servingOAuthSnapshot, requestParsed)) {
3858
+ throw new Error("OAuth account selection changed before dispatch");
3859
+ }
3860
+ } else {
3861
+ const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
3862
+ if (!current) throw new Error("API key selection is unavailable before dispatch");
3863
+ route.provider = current;
3864
+ }
3865
+ adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
3866
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
3867
+ );
3868
+ invalidateSameTargetRequest();
3869
+ return adapter;
3870
+ };
3871
+ const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
3872
+ requestParsed._cursorIdentityScope = undefined;
3873
+ requestParsed._cursorConversationId = undefined;
3874
+ if (requestParsed._providerContinuation?.cursor) {
3875
+ const { cursor: _oldCursor, ...rest } = requestParsed._providerContinuation;
3876
+ requestParsed._providerContinuation = rest;
3877
+ }
3878
+ return refreshDispatchAdapter(requestParsed);
3879
+ };
3880
+ const runSelectedTurn = async (
3881
+ selectedAdapter: ProviderAdapter,
3882
+ ...[requestParsed, incoming, emit]: Parameters<NonNullable<ProviderAdapter["runTurn"]>>
3883
+ ): Promise<void> => {
3884
+ for (let attempt = 0; attempt < 3; attempt++) {
3885
+ if (!selectionIsCurrent(adapterBindings.get(selectedAdapter))) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
3886
+ const binding = adapterBindings.get(selectedAdapter);
3887
+ const run = rawRunTurns.get(selectedAdapter);
3888
+ if (!run) throw new Error("Selected provider no longer supports this turn transport");
3889
+ let sent = false;
3890
+ let refused = false;
3891
+ // Both main and image-loop callers already acquired the initial pacing slot.
3892
+ // Subsequent physical messages retain this adapter/credential and are paced normally.
3893
+ const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
3894
+ providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
3895
+ beforeDispatch: () => {
3896
+ if (sent) return;
3897
+ if (!selectionIsCurrent(binding)) {
3898
+ refused = true;
3899
+ throw new Error("Account selection changed before the first turn dispatch");
3900
+ }
3901
+ sent = true;
3902
+ },
3903
+ });
3904
+ try {
3905
+ await run(requestParsed, { ...incoming, providerFetch: fetch }, event => { if (!refused) emit(event); });
3906
+ } catch (error) {
3907
+ if (!refused) throw error;
3908
+ }
3909
+ if (!refused) return;
3910
+ // The adapter may map the guard's exception to an error event. Neither that
3911
+ // event nor a refused send may escape before retrying the newly selected account.
3912
+ selectedAdapter = await refreshRunTurnAdapter(requestParsed);
3913
+ }
3914
+ throw new Error("Account selection changed repeatedly before turn dispatch");
3915
+ };
3916
+ const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed): ProviderFetchOptions["dispatchOverride"] => {
3917
+ if (route.provider.authMode === "forward") return undefined;
3918
+ return async (input, init, execute) => {
3919
+ let destination = input;
3920
+ let dispatchInit = init;
3921
+ for (let attempt = 0; attempt < 3; attempt++) {
3922
+ if (selectionIsCurrent(requestBindings.get(wireRequest))) {
3923
+ const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
3924
+ return fetchImpl(destination, dispatchInit);
3925
+ }
3926
+ const nextAdapter = await refreshDispatchAdapter(requestParsed);
3927
+ const rebuilt = await nextAdapter.buildRequest(requestParsed, {
3928
+ headers: selectedForwardHeaders, translatorBudget,
3929
+ ...(imageTierBias > 0 ? { imageTierBias } : {}),
3930
+ });
3931
+ const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
3932
+ if (!bodySize.admitted) {
3933
+ rebuilt.releaseBodyObservation?.();
3934
+ return formatErrorResponse(413, "outbound_body_too_large", describeOutboundBodyRefusal(bodySize));
3935
+ }
3936
+ const headers = new Headers(dispatchInit.headers);
3937
+ for (const name of Object.keys(wireRequest.headers)) headers.delete(name);
3938
+ for (const [name, value] of Object.entries(rebuilt.headers)) headers.set(name, value);
3939
+ wireRequest.releaseBodyObservation?.();
3940
+ Object.assign(wireRequest, rebuilt);
3941
+ const binding = requestBindings.get(rebuilt);
3942
+ if (binding) requestBindings.set(wireRequest, binding);
3943
+ else requestBindings.delete(wireRequest);
3944
+ sameTargetRequest = wireRequest;
3945
+ sameTargetParsed = requestParsed;
3946
+ sameTargetToken = transportToken;
3947
+ destination = rebuilt.url;
3948
+ dispatchInit = { ...dispatchInit, method: rebuilt.method, headers, body: rebuilt.body };
3949
+ bindRouteReasoningReplayScope({ parsed: requestParsed, providerName: route.providerName, provider: route.provider,
3950
+ adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
3951
+ // The next iteration validates synchronously and calls fetch in that same turn.
3952
+ }
3953
+ throw new Error("OAuth account selection changed repeatedly before dispatch");
3954
+ };
3955
+ };
3554
3956
  const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
3555
3957
  ? anthropicSessionKeyFromParts({
3556
3958
  sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
@@ -3576,12 +3978,11 @@ async function handleResponsesInner(
3576
3978
  }
3577
3979
  return formatErrorResponse(401, "authentication_error", "No eligible Anthropic OAuth account available");
3578
3980
  }
3579
- const accessToken = await getAnthropicPoolAccessToken(selection.accountId);
3580
- anthropicPoolAccountId = selection.accountId;
3581
- bindAnthropicSessionAffinity(anthropicSessionKey, selection.accountId);
3582
- promoteAnthropicActiveAccount(selection.accountId);
3583
- route.provider = { ...route.provider, apiKey: accessToken };
3584
- logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
3981
+ const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(selection.accountId), true, selection.reason);
3982
+ if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
3983
+ anthropicPoolAccountId = admitted.accountId;
3984
+ route.provider = { ...route.provider, apiKey: admitted.accessToken };
3985
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
3585
3986
  } else {
3586
3987
  // Prefer the account with known headroom BEFORE the first attempt. Rotation alone
3587
3988
  // only reacts to a 429, so a turn could open on an account a previous probe already
@@ -3630,6 +4031,10 @@ async function handleResponsesInner(
3630
4031
  resolved = await getValidAccessTokenSnapshot(route.providerName);
3631
4032
  usedPreferredAccount = false;
3632
4033
  }
4034
+ const admitted = await commitResolvedOAuthSelection(resolved, true);
4035
+ if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
4036
+ if (admitted.accountId !== resolved.accountId) usedPreferredAccount = true;
4037
+ resolved = admitted;
3633
4038
  replayOAuthCredentialSnapshot = {
3634
4039
  accountId: resolved.accountId,
3635
4040
  generation: resolved.generation,
@@ -3662,22 +4067,11 @@ async function handleResponsesInner(
3662
4067
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
3663
4068
  parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
3664
4069
  }
3665
- // Antigravity (cloud-code-assist) needs the discovered Cloud Code Assist project id in the
3666
- // CCA envelope. Keep it paired with the token snapshot so an account rotation cannot mix
3667
- // a fresh token with project metadata re-read from a different credential generation.
4070
+ // Project identity belongs to the admitted account on EVERY request, including
4071
+ // the request after a pool transition made that account the persisted active one.
3668
4072
  if (route.provider.googleMode === "cloud-code-assist") {
3669
- // When pre-dispatch chose a DIFFERENT account, the configured project belongs to
3670
- // the account we did not use, and `!route.provider.project` would skip right past
3671
- // it — installing B's bearer alongside A's project. That is the #2841 pairing bug
3672
- // in its original shape, so the preferred-account path replaces the project
3673
- // unconditionally and refuses to dispatch at all if the chosen account has none.
3674
- // A project-less preferred account already fell back above, so by here the
3675
- // preferred path always has one.
3676
- if (usedPreferredAccount && resolved.projectId) {
3677
- route.provider = { ...route.provider, project: resolved.projectId };
3678
- } else if (!route.provider.project && resolved.projectId) {
3679
- route.provider = { ...route.provider, project: resolved.projectId };
3680
- }
4073
+ if (!resolved.projectId) return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist account project is unavailable")));
4074
+ route.provider = { ...route.provider, project: resolved.projectId };
3681
4075
  }
3682
4076
  }
3683
4077
  } catch (err) {
@@ -3719,7 +4113,7 @@ async function handleResponsesInner(
3719
4113
  logCtx.provider = route.providerName;
3720
4114
  delete logCtx.accountLogLabel;
3721
4115
  }
3722
- const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
4116
+ adapter = resolveSelectionAdapter(adapterProvider, config.cacheRetention);
3723
4117
  bindRouteReasoningReplayScope({
3724
4118
  parsed,
3725
4119
  providerName: route.providerName,
@@ -3747,7 +4141,8 @@ async function handleResponsesInner(
3747
4141
  (logCtx.attempts ??= []).push(attempt);
3748
4142
  }
3749
4143
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
3750
- let runTurnAdapter = adapter;
4144
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, adapterProvider, adapter.name);
4145
+ runTurnAdapter = adapter;
3751
4146
  if (adapter.runTurn) {
3752
4147
  recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
3753
4148
  }
@@ -3976,6 +4371,9 @@ async function handleResponsesInner(
3976
4371
  }
3977
4372
  throw error;
3978
4373
  }
4374
+ const functionRepairSchemas = isCanonicalOpenAiForwardProvider(route.provider)
4375
+ ? new Map()
4376
+ : collectFunctionCallRepairSchemas(clientToolAuthorizationBody);
3979
4377
  if (!isCanonicalOpenAiForwardProvider(route.provider)) {
3980
4378
  for (const name of request.convertedRoutedCustomToolNames ?? []) {
3981
4379
  if (
@@ -4041,6 +4439,14 @@ async function handleResponsesInner(
4041
4439
  );
4042
4440
  const restoreAuthorizedBareNamespaceToolCalls = (value: unknown): unknown =>
4043
4441
  restoreRoutedNamespaceCalls(value, authorizedBareNamespaceToolAliases).value;
4442
+ const normalizeFunctionCompletionJson = (text: string): string => {
4443
+ const snapshot = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)
4444
+ ? repairResponsesSnapshotJson(text, outboundRequestBody)
4445
+ : text;
4446
+ // Sparse gateways need completion status inferred before schema repair can
4447
+ // distinguish completed arguments from in-progress placeholders.
4448
+ return repairFunctionCallsInJson(backfillResponsesFieldsJson(snapshot), functionRepairSchemas);
4449
+ };
4044
4450
  let undeclaredToolGuardActive = false;
4045
4451
  const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
4046
4452
  outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
@@ -4153,12 +4559,15 @@ async function handleResponsesInner(
4153
4559
  const rememberPassthroughResponseChecked = rememberPassthroughResponse
4154
4560
  ? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
4155
4561
  if (inspectionSawUndeclaredTool) return;
4156
- const restoredResponse = restoreRoutedCustomCalls(
4157
- restoreAuthorizedBareNamespaceToolCalls(response),
4562
+ const restored = restoreRoutedCustomCalls(
4563
+ restoreAuthorizedBareNamespaceToolCalls(restoreRoutedNamespaceCalls(response, routedNamespaceToolAliases).value),
4158
4564
  routedCustomToolNames,
4159
4565
  routedCustomToolRepairNames,
4160
4566
  declaredWireToolNames,
4161
- ).value as { id?: unknown; output?: unknown; status?: unknown };
4567
+ ).value;
4568
+ const restoredResponse = (functionRepairSchemas.size > 0
4569
+ ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored)))
4570
+ : restored) as { id?: unknown; output?: unknown; status?: unknown };
4162
4571
  if (
4163
4572
  undeclaredToolGuardActive
4164
4573
  && undeclaredToolCallNameInResponse(
@@ -4334,6 +4743,7 @@ async function handleResponsesInner(
4334
4743
  body: request.body,
4335
4744
  }, recovery), upstream.signal, connectMs, parsed.stream,
4336
4745
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4746
+ dispatchOverride: oauthDispatch(request),
4337
4747
  providerName: route.providerName,
4338
4748
  modelId: route.modelId,
4339
4749
  onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
@@ -4364,7 +4774,7 @@ async function handleResponsesInner(
4364
4774
  const rebuildAndRefetch = async (
4365
4775
  recovery: AttemptRecoveryKind,
4366
4776
  ): Promise<Response | { failed: Response }> => {
4367
- const retryAdapter = resolveAdapter(
4777
+ const retryAdapter = resolveSelectionAdapter(
4368
4778
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4369
4779
  config.cacheRetention,
4370
4780
  );
@@ -4398,6 +4808,7 @@ async function handleResponsesInner(
4398
4808
  retryAdapter.name,
4399
4809
  logCtx.accountLogLabel,
4400
4810
  );
4811
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name);
4401
4812
  const rebuiltBodyRefusal = refuseOversizedOutboundBody(request);
4402
4813
  if (rebuiltBodyRefusal) return { failed: rebuiltBodyRefusal };
4403
4814
  try {
@@ -4410,6 +4821,7 @@ async function handleResponsesInner(
4410
4821
  body: request.body,
4411
4822
  }, innerRecovery), upstream.signal, connectMs, parsed.stream,
4412
4823
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4824
+ dispatchOverride: oauthDispatch(request),
4413
4825
  providerName: route.providerName,
4414
4826
  modelId: route.modelId,
4415
4827
  onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
@@ -4468,7 +4880,7 @@ async function handleResponsesInner(
4468
4880
  authCtx = replay.authCtx;
4469
4881
  route.provider = replay.provider;
4470
4882
  selectedForwardHeaders = replay.headers;
4471
- const replayAdapter = resolveAdapter(
4883
+ const replayAdapter = resolveSelectionAdapter(
4472
4884
  resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
4473
4885
  config.cacheRetention,
4474
4886
  );
@@ -4486,6 +4898,7 @@ async function handleResponsesInner(
4486
4898
  });
4487
4899
  logCtx.providerAdapter = replayAdapter.name;
4488
4900
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, replayAdapter.name, logCtx.accountLogLabel);
4901
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, replayAdapter.name);
4489
4902
  try {
4490
4903
  request = await replayAdapter.buildRequest(parsed, {
4491
4904
  headers: selectedForwardHeaders,
@@ -4514,6 +4927,7 @@ async function handleResponsesInner(
4514
4927
  // here on is a genuine transport attempt.
4515
4928
  storedPoolReplayDispatchNotifier(
4516
4929
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4930
+ dispatchOverride: oauthDispatch(request),
4517
4931
  providerName: route.providerName,
4518
4932
  modelId: route.modelId,
4519
4933
  onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
@@ -4551,12 +4965,17 @@ async function handleResponsesInner(
4551
4965
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4552
4966
  let refreshed: OAuthAccessSnapshot;
4553
4967
  try {
4554
- refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
4968
+ refreshed = await refreshResolvedOAuthSelection(sentOAuthSnapshot);
4555
4969
  } catch (err) {
4556
4970
  upstream.abort();
4557
4971
  releaseCodexAuthContextProbeLease(authCtx);
4558
4972
  return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
4559
4973
  }
4974
+ if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
4975
+ upstream.abort();
4976
+ releaseCodexAuthContextProbeLease(authCtx);
4977
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist project is required")));
4978
+ }
4560
4979
  sentOAuthSnapshot = refreshed;
4561
4980
  replayOAuthCredentialSnapshot = {
4562
4981
  accountId: refreshed.accountId,
@@ -4567,14 +4986,18 @@ async function handleResponsesInner(
4567
4986
  }
4568
4987
  const refreshedProvider = resolveProviderTransport(
4569
4988
  route.providerName,
4570
- { ...route.provider, apiKey: refreshed.accessToken },
4989
+ {
4990
+ ...route.provider,
4991
+ apiKey: refreshed.accessToken,
4992
+ ...(refreshed.projectId ? { project: refreshed.projectId } : {}),
4993
+ },
4571
4994
  parsed.options.promptCacheKey,
4572
4995
  route.providerName === "github-copilot"
4573
4996
  ? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
4574
4997
  : undefined,
4575
4998
  );
4576
4999
  route.provider = refreshedProvider;
4577
- const refreshedAdapter = resolveAdapter(
5000
+ const refreshedAdapter = resolveSelectionAdapter(
4578
5001
  resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
4579
5002
  config.cacheRetention,
4580
5003
  );
@@ -4596,6 +5019,7 @@ async function handleResponsesInner(
4596
5019
  refreshedAdapter.name,
4597
5020
  logCtx.accountLogLabel,
4598
5021
  );
5022
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, refreshedAdapter.name);
4599
5023
  try {
4600
5024
  request = await refreshedAdapter.buildRequest(parsed, {
4601
5025
  headers: selectedForwardHeaders,
@@ -4623,6 +5047,7 @@ async function handleResponsesInner(
4623
5047
  body: request.body,
4624
5048
  }, recovery), upstream.signal, connectMs, parsed.stream,
4625
5049
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
5050
+ dispatchOverride: oauthDispatch(request),
4626
5051
  providerName: route.providerName,
4627
5052
  modelId: route.modelId,
4628
5053
  onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
@@ -4661,13 +5086,10 @@ async function handleResponsesInner(
4661
5086
  try { snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); }
4662
5087
  catch { /* Keep the original 429 body readable when the next credential is unavailable. */ }
4663
5088
  }
4664
- if (snapshot && applyFailoverSnapshot(snapshot)) {
4665
- genericFailoverAccountId = snapshot.accountId;
5089
+ if (snapshot && await applyFailoverSnapshot(snapshot)) {
4666
5090
  genericFailovers += 1;
4667
- sentOAuthSnapshot = snapshot;
4668
- replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
4669
5091
  route.provider = resolveProviderTransport(
4670
- route.providerName, route.provider, parsed.options.promptCacheKey, snapshot.apiBaseUrl,
5092
+ route.providerName, route.provider, parsed.options.promptCacheKey, sentOAuthSnapshot?.apiBaseUrl,
4671
5093
  );
4672
5094
  bindRouteReasoningReplayScope({
4673
5095
  parsed, providerName: route.providerName, provider: route.provider,
@@ -4725,6 +5147,7 @@ async function handleResponsesInner(
4725
5147
  body: request.body,
4726
5148
  }, recovery), upstream.signal, connectMs, parsed.stream,
4727
5149
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
5150
+ dispatchOverride: oauthDispatch(request),
4728
5151
  providerName: route.providerName,
4729
5152
  modelId: route.modelId,
4730
5153
  onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
@@ -4851,6 +5274,48 @@ async function handleResponsesInner(
4851
5274
  upstreamResponse = opaqueBlobRecovery.response;
4852
5275
  continue passthroughRecovery;
4853
5276
  }
5277
+
5278
+ const recoveryContentType = upstreamResponse.headers.get("content-type")?.toLowerCase() ?? "";
5279
+ const streamedFunctionOutputCandidate = upstreamResponse.ok
5280
+ && !!upstreamResponse.body
5281
+ && (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && parsed.stream))
5282
+ && !opaqueBlobRecoveryGuard.attempted
5283
+ && outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body);
5284
+ if (streamedFunctionOutputCandidate) {
5285
+ const preflightLog: RequestLogContext = { model: logCtx.model, provider: logCtx.provider };
5286
+ const preflight = await preflightComboStreamResponse(upstreamResponse, preflightLog,
5287
+ payload => {
5288
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
5289
+ const type = (payload as { type?: unknown }).type;
5290
+ return (type === "error" || type === "response.failed" || type === "response.incomplete")
5291
+ && upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
5292
+ }, {
5293
+ allowMissingContentType: !recoveryContentType && parsed.stream,
5294
+ replayReadErrors: true,
5295
+ });
5296
+ if (options.abortSignal?.aborted) return transportFailureResponse(options.abortSignal.reason);
5297
+ upstreamResponse = preflight.response;
5298
+ if (preflight.kind === "failed") {
5299
+ const streamedOpaqueRecovery = await attemptOpaqueBlobRecovery({
5300
+ response: upstreamResponse,
5301
+ outboundBody: request.body,
5302
+ adapterName: adapter.name,
5303
+ parsed,
5304
+ guard: opaqueBlobRecoveryGuard,
5305
+ signal: upstream.signal,
5306
+ }, rebuildAndRefetch);
5307
+ if (streamedOpaqueRecovery.kind === "failed") return streamedOpaqueRecovery.response;
5308
+ if (streamedOpaqueRecovery.kind === "recovered") {
5309
+ resetStreamedOpaqueBlobLogContext(logCtx);
5310
+ upstreamResponse = streamedOpaqueRecovery.response;
5311
+ continue passthroughRecovery;
5312
+ }
5313
+ logCtx.upstreamError = preflightLog.upstreamError;
5314
+ logCtx.terminalHttpStatus = preflightLog.terminalHttpStatus;
5315
+ logCtx.terminalErrorCode = preflightLog.terminalErrorCode;
5316
+ logCtx.terminalIncompleteReason = preflightLog.terminalIncompleteReason;
5317
+ }
5318
+ }
4854
5319
  break;
4855
5320
  }
4856
5321
  const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
@@ -5004,6 +5469,12 @@ async function handleResponsesInner(
5004
5469
  )
5005
5470
  : upstreamResponse.body;
5006
5471
  const repairConfig = route.provider.responsesItemIdRepair;
5472
+ // Grok Build renders deltas live but reconstructs its durable assistant
5473
+ // turn from the completed response snapshot. Native Responses streams
5474
+ // may instead carry the complete items in output_item.done, so the
5475
+ // explicit Grok compatibility marker enables strict terminal-only repair.
5476
+ // The provider's broader snapshot/lifecycle repair remains opt-in.
5477
+ const grokClientSnapshotRepairEnabled = logCtx.surface === "grok";
5007
5478
  const snapshotRepairEnabled = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair);
5008
5479
  const githubCopilotRepairEnabled = route.providerName === "github-copilot";
5009
5480
  const responseModelRewrite = parsed._responseModelId !== undefined
@@ -5052,10 +5523,16 @@ async function handleResponsesInner(
5052
5523
  githubCopilotRepairEnabled
5053
5524
  ? createGithubCopilotResponsesBlockRewrite(translatorBudget)
5054
5525
  : undefined,
5526
+ grokClientSnapshotRepairEnabled
5527
+ ? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
5528
+ : undefined,
5055
5529
  snapshotRepairEnabled
5056
5530
  ? createResponsesSnapshotBlockRewrite(outboundRequestBody, translatorBudget)
5057
5531
  : undefined,
5058
5532
  createResponsesFieldBackfillBlockRewrite(),
5533
+ functionRepairSchemas.size > 0
5534
+ ? createResponsesFunctionToolRepairBlockRewrite(functionRepairSchemas, translatorBudget)
5535
+ : undefined,
5059
5536
  // Last: every rewrite above can still rename or reshape a call item, so the guard must
5060
5537
  // compare the names the client will actually receive against the declared catalog.
5061
5538
  undeclaredToolGuardActive
@@ -5130,11 +5607,14 @@ async function handleResponsesInner(
5130
5607
  ...(clientBlockRewrite
5131
5608
  ? { rewriteBlocks: clientBlockRewrite }
5132
5609
  : {}),
5133
- onSynthetic: kind => {
5610
+ onSynthetic: (kind, reason) => {
5134
5611
  if (!reportNativeTerminal) return;
5135
5612
  if (kind === "incomplete") {
5136
5613
  logCtx.terminalSource = "synthetic";
5137
5614
  reportNativeTerminal("incomplete");
5615
+ } else if (reason === "upstream_error") {
5616
+ logCtx.terminalSource = "synthetic";
5617
+ reportNativeTerminal("failed", logCtx.terminalHttpStatus ?? 502);
5138
5618
  } else {
5139
5619
  logCtx.transportPhase = "mid_stream";
5140
5620
  logCtx.terminalSource = "synthetic";
@@ -5147,6 +5627,7 @@ async function handleResponsesInner(
5147
5627
  }, {
5148
5628
  clientGoneSignal: options.abortSignal,
5149
5629
  ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
5630
+ ...(logCtx.upstreamError === undefined ? {} : { upstreamError: logCtx.upstreamError }),
5150
5631
  });
5151
5632
  // When selected, this relay closes response.completed even if upstream
5152
5633
  // keeps the connection alive. Marked Codex WS traffic, Windows
@@ -5229,7 +5710,12 @@ async function handleResponsesInner(
5229
5710
  const rewrittenBody = clientBlockRewrite !== undefined
5230
5711
  ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget)
5231
5712
  : nativeBody;
5232
- const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason));
5713
+ const clientBody = relaySseWithFailedTail(
5714
+ rewrittenBody,
5715
+ upstream,
5716
+ reason => clientGone.abort(reason),
5717
+ { upstreamError: logCtx.upstreamError },
5718
+ );
5233
5719
  return markNativePassthroughSseResponse(new Response(clientBody, {
5234
5720
  status: upstreamResponse.status,
5235
5721
  headers,
@@ -5273,12 +5759,10 @@ async function handleResponsesInner(
5273
5759
  restored,
5274
5760
  routedToolSearchNames,
5275
5761
  );
5276
- const repaired = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)
5277
- ? repairResponsesSnapshotJson(restoredToolSearch, outboundRequestBody)
5278
- : restoredToolSearch;
5762
+ const repaired = normalizeFunctionCompletionJson(restoredToolSearch);
5279
5763
  const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
5280
- ? rewriteResponsesModelJson(backfillResponsesFieldsJson(repaired), parsed._responseModelId)
5281
- : backfillResponsesFieldsJson(repaired);
5764
+ ? rewriteResponsesModelJson(repaired, parsed._responseModelId)
5765
+ : repaired;
5282
5766
  // The bounded-JSON answer bypasses the SSE payload rewrite, so content-
5283
5767
  // channel reasoning needs the same normalization here for the plain
5284
5768
  // JSON answer and every reframed-SSE variant built from clientJson.
@@ -5483,9 +5967,8 @@ async function handleResponsesInner(
5483
5967
  if (!nextAccountId) return null;
5484
5968
  try {
5485
5969
  const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
5486
- genericFailoverAccountId = nextAccountId;
5487
5970
  genericFailovers += 1;
5488
- if (!applyFailoverSnapshot(snapshot)) return null;
5971
+ if (!await applyFailoverSnapshot(snapshot)) return null;
5489
5972
  } catch {
5490
5973
  return null;
5491
5974
  }
@@ -5509,12 +5992,12 @@ async function handleResponsesInner(
5509
5992
  // carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
5510
5993
  // local-cli credential rule. Both existing Anthropic rotation sites apply the token the
5511
5994
  // same way.
5512
- const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
5513
- anthropicPoolAccountId = nextAccountId;
5995
+ const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
5996
+ if (!admitted) throw new Error("OAuth selection changed during recovery");
5997
+ anthropicPoolAccountId = admitted.accountId;
5514
5998
  anthropicPoolFailovers += 1;
5515
- route.provider = { ...route.provider, apiKey: accessToken };
5516
- promoteAnthropicActiveAccount(nextAccountId);
5517
- logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
5999
+ route.provider = { ...route.provider, apiKey: admitted.accessToken };
6000
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
5518
6001
  } catch {
5519
6002
  return null;
5520
6003
  }
@@ -5523,7 +6006,7 @@ async function handleResponsesInner(
5523
6006
  // credential. The 429 is terminal for this sidecar turn.
5524
6007
  return null;
5525
6008
  }
5526
- const rotatedAdapter = resolveAdapter(
6009
+ const rotatedAdapter = resolveSelectionAdapter(
5527
6010
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5528
6011
  config.cacheRetention,
5529
6012
  );
@@ -5607,6 +6090,13 @@ async function handleResponsesInner(
5607
6090
  stallTimeoutSec: config.stallTimeoutSec,
5608
6091
  waitForRequestSlot: imageProviderFetch.waitForPacing,
5609
6092
  fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
6093
+ fetchForRequest: (request, iterParsed) => {
6094
+ const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
6095
+ dispatchOverride: oauthDispatch(request, iterParsed),
6096
+ providerName: route.providerName, modelId: route.modelId,
6097
+ });
6098
+ return fetch.unpacedFetch ?? fetch;
6099
+ },
5610
6100
  onRequestBuilt: request => {
5611
6101
  recordAdapterReasoning(logCtx, request);
5612
6102
  recordAdapterTier(logCtx, request);
@@ -5667,6 +6157,10 @@ async function handleResponsesInner(
5667
6157
  })(input, init)) as typeof globalThis.fetch;
5668
6158
  const wsResponse = await runWithWebSearch({
5669
6159
  parsed, adapter,
6160
+ fetchForRequest: (request, iterParsed) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
6161
+ dispatchOverride: oauthDispatch(request, iterParsed),
6162
+ providerName: route.providerName, modelId: route.modelId,
6163
+ }),
5670
6164
  incomingMeta: {
5671
6165
  headers: selectedForwardHeaders,
5672
6166
  abortSignal: options.abortSignal,
@@ -5742,6 +6236,13 @@ async function handleResponsesInner(
5742
6236
  const queue = createAdapterEventQueue({
5743
6237
  onBacklogExceeded: () => runTurnAbort.abort(),
5744
6238
  });
6239
+ const refreshRunTurnSelection = async (): Promise<void> => {
6240
+ if (selectionIsCurrent(adapterBindings.get(runTurnAdapter))) return;
6241
+ await refreshRunTurnAdapter(parsed);
6242
+ bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: route.provider,
6243
+ adapterName: runTurnAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
6244
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, runTurnAdapter.name, logCtx.accountLogLabel);
6245
+ };
5745
6246
  // Initial admission must settle before the streaming Response commits HTTP 200.
5746
6247
  // Let the outer Responses facade preserve the local retryable-429 contract.
5747
6248
  try {
@@ -5765,6 +6266,7 @@ async function handleResponsesInner(
5765
6266
  if (!pacingSlotAcquired) {
5766
6267
  await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
5767
6268
  }
6269
+ await refreshRunTurnSelection();
5768
6270
  noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery);
5769
6271
  const runTurnProviderFetch = providerFetch(
5770
6272
  route.provider,
@@ -5830,9 +6332,8 @@ async function handleResponsesInner(
5830
6332
  if (!nextAccountId) return false;
5831
6333
  try {
5832
6334
  const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
5833
- genericFailoverAccountId = nextAccountId;
5834
6335
  genericFailovers += 1;
5835
- if (!applyFailoverSnapshot(snapshot)) return false;
6336
+ if (!await applyFailoverSnapshot(snapshot)) return false;
5836
6337
  // A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
5837
6338
  // client-visible bytes, so replay is safe, but carrying its account identity into the next
5838
6339
  // account would not be. Let the rotated adapter derive a fresh identity and conversation.
@@ -5848,7 +6349,7 @@ async function handleResponsesInner(
5848
6349
  route.provider,
5849
6350
  inboundWire,
5850
6351
  );
5851
- const rotatedAdapter = resolveAdapter(rotatedProvider, config.cacheRetention);
6352
+ const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
5852
6353
  if (!rotatedAdapter.runTurn) return false;
5853
6354
  runTurnAdapter = rotatedAdapter;
5854
6355
  bindRouteReasoningReplayScope({
@@ -5861,6 +6362,7 @@ async function handleResponsesInner(
5861
6362
  forwardHeaders: selectedForwardHeaders,
5862
6363
  });
5863
6364
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
6365
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name);
5864
6366
  return true;
5865
6367
  } catch {
5866
6368
  return false;
@@ -5897,7 +6399,7 @@ async function handleResponsesInner(
5897
6399
  if (parsed.stream) {
5898
6400
  void runTurn();
5899
6401
  let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
5900
- if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
6402
+ if (route.provider.authMode === "oauth" || (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
5901
6403
  // Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
5902
6404
  // replayed transparently; after any output reaches the bridge, a later error stays terminal.
5903
6405
  eventSource = await preflightRunTurnFailover(eventSource);
@@ -5980,7 +6482,7 @@ async function handleResponsesInner(
5980
6482
  await runTurn();
5981
6483
  const firstAttemptEvents = await queue.collect();
5982
6484
  let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
5983
- if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
6485
+ if (route.provider.authMode === "oauth" || (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
5984
6486
  runTurnEvents = [];
5985
6487
  for await (const event of await preflightRunTurnFailover(
5986
6488
  (async function* () { yield* firstAttemptEvents; })(),
@@ -6052,7 +6554,7 @@ async function handleResponsesInner(
6052
6554
  const stallTimeoutMs = typeof config.stallTimeoutSec === "number" && Number.isFinite(config.stallTimeoutSec) && config.stallTimeoutSec > 0
6053
6555
  ? Math.floor(config.stallTimeoutSec * 1000)
6054
6556
  : 300_000;
6055
- let activeAdapter = adapter;
6557
+ activeAdapter = adapter;
6056
6558
 
6057
6559
  // One immutable, body-safe outbound request per same-target sequence (URL, serialized body,
6058
6560
  // auth headers, generated compat headers). Same-target 429 replays reuse it verbatim; the
@@ -6151,16 +6653,15 @@ async function handleResponsesInner(
6151
6653
  // Capture it in a const so the fetch callbacks read a narrowed, immutable value
6152
6654
  // (TypeScript drops narrowing for a `let` captured by a nested function).
6153
6655
  const builtInitialRequest = initialRequest;
6154
- let sameTargetRequest: AdapterRequest | undefined = builtInitialRequest;
6155
- let sameTargetParsed: OcxParsedRequest | undefined = parsed;
6156
- let sameTargetToken = 0;
6157
- let transportToken = 0;
6656
+ sameTargetRequest = builtInitialRequest;
6657
+ sameTargetParsed = parsed;
6658
+ sameTargetToken = transportToken;
6158
6659
  /**
6159
6660
  * Invalidate the same-target request cache. Every credential/adapter/parsed mutation MUST
6160
6661
  * go through here: the cache keys on `parsed` REFERENCE identity, so an in-place mutation
6161
6662
  * is invisible to it and a missed bump would replay a request built with a stale key.
6162
6663
  */
6163
- const invalidateSameTargetRequest = (): void => { transportToken += 1; };
6664
+
6164
6665
  let upstreamResponse: Response;
6165
6666
  try {
6166
6667
  if (activeAdapter.fetchResponse) {
@@ -6171,6 +6672,7 @@ async function handleResponsesInner(
6171
6672
  timeoutMs: connectMs,
6172
6673
  stream: parsed.stream,
6173
6674
  executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
6675
+ dispatchOverride: oauthDispatch(builtInitialRequest),
6174
6676
  providerName: route.providerName,
6175
6677
  modelId: route.modelId,
6176
6678
  }),
@@ -6196,6 +6698,7 @@ async function handleResponsesInner(
6196
6698
  body: builtInitialRequest.body,
6197
6699
  }, recovery), upstream.signal, connectMs, parsed.stream,
6198
6700
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
6701
+ dispatchOverride: oauthDispatch(builtInitialRequest),
6199
6702
  providerName: route.providerName,
6200
6703
  modelId: route.modelId,
6201
6704
  }));
@@ -6227,7 +6730,6 @@ async function handleResponsesInner(
6227
6730
  let rateLimitRetries = 0;
6228
6731
  // Shared with the terminal-guard continuation below: an image-tier reduction that let the
6229
6732
  // main request clear a 413 must not be forgotten on the very next continuation build.
6230
- let imageTierBias = 0;
6231
6733
  if (!upstreamResponse.ok) {
6232
6734
  // Recovery loop: multi-key 429 failover + at most ONE opaque-state rebuild and ONE
6233
6735
  // anthropic 413 tightened retry
@@ -6280,6 +6782,7 @@ async function handleResponsesInner(
6280
6782
  if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
6281
6783
  logCtx.providerAdapter = activeAdapter.name;
6282
6784
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
6785
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
6283
6786
  noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
6284
6787
  try {
6285
6788
  try {
@@ -6290,6 +6793,7 @@ async function handleResponsesInner(
6290
6793
  timeoutMs: connectMs,
6291
6794
  stream: parsed.stream,
6292
6795
  executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
6796
+ dispatchOverride: oauthDispatch(retryRequest),
6293
6797
  providerName: route.providerName,
6294
6798
  modelId: route.modelId,
6295
6799
  }),
@@ -6311,6 +6815,7 @@ async function handleResponsesInner(
6311
6815
  method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
6312
6816
  }, recoveryKind), upstream.signal, connectMs, parsed.stream,
6313
6817
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
6818
+ dispatchOverride: oauthDispatch(retryRequest),
6314
6819
  providerName: route.providerName,
6315
6820
  modelId: route.modelId,
6316
6821
  })),
@@ -6350,11 +6855,15 @@ async function handleResponsesInner(
6350
6855
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
6351
6856
  let refreshed: OAuthAccessSnapshot;
6352
6857
  try {
6353
- refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
6858
+ refreshed = await refreshResolvedOAuthSelection(sentOAuthSnapshot);
6354
6859
  } catch (err) {
6355
6860
  cleanupUpstreamAbort();
6356
6861
  return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
6357
6862
  }
6863
+ if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
6864
+ cleanupUpstreamAbort();
6865
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist project is required")));
6866
+ }
6358
6867
  sentOAuthSnapshot = refreshed;
6359
6868
  replayOAuthCredentialSnapshot = {
6360
6869
  accountId: refreshed.accountId,
@@ -6365,7 +6874,11 @@ async function handleResponsesInner(
6365
6874
  }
6366
6875
  const refreshedProvider = resolveProviderTransport(
6367
6876
  route.providerName,
6368
- { ...route.provider, apiKey: refreshed.accessToken },
6877
+ {
6878
+ ...route.provider,
6879
+ apiKey: refreshed.accessToken,
6880
+ ...(refreshed.projectId ? { project: refreshed.projectId } : {}),
6881
+ },
6369
6882
  parsed.options.promptCacheKey,
6370
6883
  route.providerName === "github-copilot"
6371
6884
  ? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
@@ -6373,7 +6886,7 @@ async function handleResponsesInner(
6373
6886
  );
6374
6887
  route.provider = refreshedProvider;
6375
6888
  invalidateSameTargetRequest();
6376
- activeAdapter = resolveAdapter(
6889
+ activeAdapter = resolveSelectionAdapter(
6377
6890
  resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
6378
6891
  config.cacheRetention,
6379
6892
  );
@@ -6406,7 +6919,7 @@ async function handleResponsesInner(
6406
6919
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
6407
6920
  route.provider = rotated;
6408
6921
  invalidateSameTargetRequest();
6409
- activeAdapter = resolveAdapter(
6922
+ activeAdapter = resolveSelectionAdapter(
6410
6923
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6411
6924
  config.cacheRetention,
6412
6925
  );
@@ -6476,7 +6989,7 @@ async function handleResponsesInner(
6476
6989
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
6477
6990
  route.provider = rotated;
6478
6991
  invalidateSameTargetRequest();
6479
- activeAdapter = resolveAdapter(
6992
+ activeAdapter = resolveSelectionAdapter(
6480
6993
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6481
6994
  config.cacheRetention,
6482
6995
  );
@@ -6507,18 +7020,19 @@ async function handleResponsesInner(
6507
7020
  if (!nextAccountId) break;
6508
7021
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
6509
7022
  try {
6510
- const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
6511
- anthropicPoolAccountId = nextAccountId;
7023
+ const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
7024
+ if (!admitted) throw new Error("OAuth selection changed during recovery");
7025
+ anthropicPoolAccountId = admitted.accountId;
6512
7026
  anthropicPoolFailovers += 1;
6513
- route.provider = { ...route.provider, apiKey: accessToken };
7027
+ route.provider = { ...route.provider, apiKey: admitted.accessToken };
6514
7028
  invalidateSameTargetRequest();
6515
- promoteAnthropicActiveAccount(nextAccountId);
6516
- logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
6517
- activeAdapter = resolveAdapter(
7029
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
7030
+ activeAdapter = resolveSelectionAdapter(
6518
7031
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6519
7032
  config.cacheRetention,
6520
7033
  );
6521
7034
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
7035
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
6522
7036
  const result = await rebuildAndRefetch("anthropic-oauth-429");
6523
7037
  if ("failed" in result) return result.failed;
6524
7038
  upstreamResponse = result;
@@ -6553,15 +7067,15 @@ async function handleResponsesInner(
6553
7067
  // projectId with its token and Kiro carries routing metadata, so a token-only swap
6554
7068
  // would mix one account's credential with another's routing data.
6555
7069
  const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
6556
- genericFailoverAccountId = nextAccountId;
6557
- genericFailovers += 1;
6558
- if (!applyFailoverSnapshot(snapshot)) break;
7070
+ genericFailovers += 1;
7071
+ if (!await applyFailoverSnapshot(snapshot)) break;
6559
7072
  invalidateSameTargetRequest();
6560
- activeAdapter = resolveAdapter(
7073
+ activeAdapter = resolveSelectionAdapter(
6561
7074
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6562
7075
  config.cacheRetention,
6563
7076
  );
6564
7077
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
7078
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
6565
7079
  const result = await rebuildAndRefetch("oauth-account-429");
6566
7080
  if ("failed" in result) return result.failed;
6567
7081
  upstreamResponse = result;
@@ -6761,6 +7275,7 @@ async function handleResponsesInner(
6761
7275
  timeoutMs: connectMs,
6762
7276
  stream: nextParsed.stream,
6763
7277
  executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
7278
+ dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
6764
7279
  providerName: route.providerName,
6765
7280
  modelId: nextParsed.modelId,
6766
7281
  }),
@@ -6786,6 +7301,7 @@ async function handleResponsesInner(
6786
7301
  connectMs,
6787
7302
  nextParsed.stream,
6788
7303
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
7304
+ dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
6789
7305
  providerName: route.providerName,
6790
7306
  modelId: nextParsed.modelId,
6791
7307
  }),
@@ -6881,7 +7397,7 @@ async function handleResponsesInner(
6881
7397
  try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
6882
7398
  route.provider = rotated;
6883
7399
  invalidateSameTargetRequest();
6884
- activeAdapter = resolveAdapter(
7400
+ activeAdapter = resolveSelectionAdapter(
6885
7401
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6886
7402
  config.cacheRetention,
6887
7403
  );
@@ -6917,18 +7433,19 @@ async function handleResponsesInner(
6917
7433
  if (nextAccountId) {
6918
7434
  try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
6919
7435
  try {
6920
- const accessToken = await getAnthropicPoolAccessToken(nextAccountId);
6921
- anthropicPoolAccountId = nextAccountId;
7436
+ const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
7437
+ if (!admitted) throw new Error("OAuth selection changed during recovery");
7438
+ anthropicPoolAccountId = admitted.accountId;
6922
7439
  anthropicPoolFailovers += 1;
6923
- route.provider = { ...route.provider, apiKey: accessToken };
7440
+ route.provider = { ...route.provider, apiKey: admitted.accessToken };
6924
7441
  invalidateSameTargetRequest();
6925
- promoteAnthropicActiveAccount(nextAccountId);
6926
- logCtx.provider = formatAnthropicProviderForLog("anthropic", nextAccountId, config);
6927
- activeAdapter = resolveAdapter(
7442
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
7443
+ activeAdapter = resolveSelectionAdapter(
6928
7444
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6929
7445
  config.cacheRetention,
6930
7446
  );
6931
7447
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
7448
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
6932
7449
  nextContinuationRecoveryKind = "anthropic-oauth-429";
6933
7450
  continue;
6934
7451
  } catch {
@@ -6961,15 +7478,15 @@ async function handleResponsesInner(
6961
7478
  // metadata, so a token-only swap would mix one account's credential with another's
6962
7479
  // routing data.
6963
7480
  const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
6964
- genericFailoverAccountId = nextAccountId;
6965
- genericFailovers += 1;
6966
- if (applyFailoverSnapshot(snapshot, nextParsed)) {
7481
+ genericFailovers += 1;
7482
+ if (await applyFailoverSnapshot(snapshot, nextParsed)) {
6967
7483
  invalidateSameTargetRequest();
6968
- activeAdapter = resolveAdapter(
7484
+ activeAdapter = resolveSelectionAdapter(
6969
7485
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
6970
7486
  config.cacheRetention,
6971
7487
  );
6972
7488
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
7489
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, activeAdapter.name);
6973
7490
  nextContinuationRecoveryKind = "oauth-account-429";
6974
7491
  continue;
6975
7492
  }