@bitkyc08/opencodex 2.55.0 → 2.57.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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,803 @@
1
+ import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
2
+ import type { PreparedResponsesRequest } from "./request-prepare";
3
+ import type { OAuthAccessSnapshot } from "../../oauth";
4
+ import {
5
+ captureOAuthAccountSelection,
6
+ commitOAuthAccountSelection,
7
+ getAccountCredentialWithStatus,
8
+ credentialGeneration,
9
+ } from "../../oauth/store";
10
+ import type { ProviderAdapter, AdapterRequest } from "../../adapters/base";
11
+ import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../../types";
12
+ import type { AnthropicAccountSelectionReason } from "../../oauth/anthropic-routing";
13
+ import {
14
+ isAnthropicAccountPoolEnabled,
15
+ getAnthropicPoolAccessSnapshot,
16
+ commitAnthropicSelectionRouting,
17
+ formatAnthropicProviderForLog,
18
+ anthropicSessionKeyFromParts,
19
+ resolveAnthropicAccountForSession,
20
+ getAnthropicPoolRetryAfterSeconds,
21
+ hasAnthropicFailoverQuorum,
22
+ } from "../../oauth/anthropic-routing";
23
+ import {
24
+ getValidAccessSnapshotForAccount,
25
+ forceRefreshOAuthAccessSnapshot,
26
+ getValidAccessTokenSnapshot,
27
+ publicOAuthAuthenticationErrorMessage,
28
+ UnsupportedOAuthProviderError,
29
+ } from "../../oauth";
30
+ import {
31
+ forgetGenericFailoverRoster,
32
+ isGenericFailoverProvider,
33
+ preferredInitialAccount,
34
+ noteGenericPoolSelection,
35
+ } from "../../oauth/generic-account-failover";
36
+ import { stampOAuthAccountLabel, usesApiKeyAccount } from "../../providers/label";
37
+ import { resolveProviderTransport } from "../../providers/xai-transport";
38
+ import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
39
+ import {
40
+ providerApiKeySelectionIsCurrent,
41
+ resolveCurrentProviderApiKeyTransport,
42
+ } from "../../providers/api-key-selection";
43
+ import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
44
+ import { providerFetch } from "./fetch-helpers";
45
+ import type { ProviderFetchOptions } from "./fetch-helpers";
46
+ import { captureConfigGeneration } from "../../lib/state-store-sweeper";
47
+ import { recordAnthropicAccountQuotaFromHeaders, hasPassiveAccountQuota } from "../../providers/quota";
48
+ import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
49
+ import { formatErrorResponse } from "../../bridge";
50
+ import { bindRouteReasoningReplayScope } from "./core-replay";
51
+ import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
52
+ import { redactSecretString } from "../../lib/redact";
53
+ import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
54
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
55
+ import { providerConsumesCallerAuthorization } from "../../providers/caller-authorization";
56
+ import { releaseCodexAuthContextProbeLease, stripCodexRuntimeProviderFields } from "../../codex/auth-context";
57
+ import {
58
+ beginRequestAttempt,
59
+ sealRequestAttemptIdentity,
60
+ recordAttemptCredentialSource,
61
+ recordAdapterTierMetadata,
62
+ noteProviderAttemptSend,
63
+ recordKeyAttemptFailure,
64
+ recordKeyAttemptUsage,
65
+ } from "../request-log";
66
+ import type { AttemptRecoveryKind } from "../../usage/log";
67
+ import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
68
+
69
+ /** Owns live credential selection and adapter bindings for one request. */
70
+ export async function prepareResponsesTransport(
71
+ requestContext: Pick<ResponsesRequestContext, "config" | "logCtx" | "options" | "req">,
72
+ admissionState: ResponsesAdmissionState,
73
+ requestState: Pick<
74
+ PreparedResponsesRequest,
75
+ | "route"
76
+ | "parsed"
77
+ | "inboundWire"
78
+ | "selectedForwardHeaders"
79
+ | "translatorBudget"
80
+ >,
81
+ ) {
82
+ const { config, logCtx, options, req } = requestContext;
83
+ const { route, parsed, inboundWire, translatorBudget } = requestState;
84
+
85
+
86
+ // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
87
+ // existing openai-chat / anthropic adapters authenticate with no change.
88
+ const isOAuth401ReplayProvider = (
89
+ route.providerName === "xai"
90
+ || route.providerName === "github-copilot"
91
+ || route.providerName === "kiro"
92
+ || route.providerName === "google-antigravity"
93
+ || route.providerName === "orcarouter-oauth"
94
+ ) && route.provider.authMode === "oauth";
95
+ let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
96
+ let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
97
+ let anthropicPoolAccountId: string | null = null;
98
+ let anthropicPoolFailovers = 0;
99
+ // Generic OAuth rotation (#2568) for providers with no pool of their own. Bound to the account
100
+ // the request actually used, so a concurrent rotation cannot cool an innocent replacement.
101
+ let genericFailoverAccountId: string | null = null;
102
+ let genericFailovers = 0;
103
+ let oauthSelection = route.provider.authMode === "oauth"
104
+ ? captureOAuthAccountSelection(route.providerName) : null;
105
+ let servingOAuthSnapshot: OAuthAccessSnapshot | undefined;
106
+ // These owners also serve early passthrough and sidecar sends. A dispatch-time
107
+ // rebuild must update every later builder, without entering a later block's TDZ.
108
+ let adapter: ProviderAdapter;
109
+ let activeAdapter: ProviderAdapter;
110
+ let runTurnAdapter: ProviderAdapter;
111
+ let sameTargetRequest: AdapterRequest | undefined;
112
+ let sameTargetParsed: OcxParsedRequest | undefined;
113
+ let sameTargetToken = 0;
114
+ let transportToken = 0;
115
+ let imageTierBias = 0;
116
+ const invalidateSameTargetRequest = (): void => { transportToken += 1; };
117
+ type DispatchBinding =
118
+ | { kind: "oauth"; selection: NonNullable<typeof oauthSelection>; snapshot: OAuthAccessSnapshot }
119
+ | { kind: "api-key"; provider: OcxProviderConfig };
120
+ const requestBindings = new WeakMap<AdapterRequest, DispatchBinding>();
121
+ const adapterBindings = new WeakMap<ProviderAdapter, DispatchBinding>();
122
+ const rawRunTurns = new WeakMap<ProviderAdapter, NonNullable<ProviderAdapter["runTurn"]>>();
123
+ const commitResolvedOAuthSelection = async (
124
+ candidate: OAuthAccessSnapshot,
125
+ proactive = false,
126
+ anthropicReason?: AnthropicAccountSelectionReason,
127
+ ): Promise<OAuthAccessSnapshot | null> => {
128
+ const maxSelectionAttempts = 3;
129
+ for (let attempt = 0; attempt < maxSelectionAttempts; attempt++) {
130
+ if (!oauthSelection) return null;
131
+ const proactiveEnabled = route.providerName === "anthropic"
132
+ ? isAnthropicAccountPoolEnabled(config)
133
+ : (config.providers[route.providerName]?.oauthAccountFailover?.enabled
134
+ ?? config.oauthAccountFailover?.enabled) === true;
135
+ if (proactive && candidate.accountId !== oauthSelection.accountId && !proactiveEnabled) {
136
+ oauthSelection = captureOAuthAccountSelection(route.providerName);
137
+ if (!oauthSelection) return null;
138
+ candidate = route.providerName === "anthropic"
139
+ ? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
140
+ : await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
141
+ }
142
+ const committed = await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
143
+ expectedSelection: oauthSelection,
144
+ expectedCredentialGeneration: candidate.generation,
145
+ requireUsableAccount: true,
146
+ });
147
+ if (committed) {
148
+ if (route.providerName === "anthropic" && !commitAnthropicSelectionRouting(
149
+ candidate.accountId, oauthSelection, committed,
150
+ { config, sessionKey: anthropicSessionKey, reason: anthropicReason, expectedCredentialGeneration: candidate.generation },
151
+ )) return null;
152
+ oauthSelection = committed;
153
+ servingOAuthSnapshot = candidate;
154
+ forgetGenericFailoverRoster(route.providerName);
155
+ return candidate;
156
+ }
157
+ // A newer manual choice wins over this request's old proposal, including A→B→A.
158
+ // Resolve that choice, not the rejected candidate, before trying admission again.
159
+ oauthSelection = captureOAuthAccountSelection(route.providerName);
160
+ if (!oauthSelection) return null;
161
+ candidate = route.providerName === "anthropic"
162
+ ? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
163
+ : await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
164
+ if (route.provider.googleMode === "cloud-code-assist" && !candidate.projectId) return null;
165
+ }
166
+ return null;
167
+ };
168
+ const refreshResolvedOAuthSelection = async (sent: OAuthAccessSnapshot): Promise<OAuthAccessSnapshot> => {
169
+ const current = captureOAuthAccountSelection(route.providerName);
170
+ const unchanged = current?.accountId === oauthSelection?.accountId
171
+ && current?.revision === oauthSelection?.revision;
172
+ const candidate = unchanged ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
173
+ const admitted = await commitResolvedOAuthSelection(candidate);
174
+ if (!admitted) throw new Error("OAuth selection changed during credential recovery");
175
+ genericFailoverAccountId = admitted.accountId;
176
+ stampOAuthAccountLabel(logCtx, route.providerName, route.provider, admitted.accountId);
177
+ return admitted;
178
+ };
179
+ /**
180
+ * Config generation captured where the serving credential is RESOLVED, not where the
181
+ * quota is written. A streaming turn is a long await, so a generation captured at write
182
+ * time cannot see a config or account change that happened earlier in the same turn —
183
+ * the case the fence exists for. Stays 0 for every provider without a passive quota.
184
+ */
185
+ let passiveQuotaWriterGeneration = 0;
186
+ /**
187
+ * Apply a rotated account's FULL credential snapshot to the live route (#2568d).
188
+ *
189
+ * One helper for all three rotation sites on purpose. Each site used to inline the same four
190
+ * lines, and the divergence that produced was the bug: `apiKey` was swapped while the routing
191
+ * metadata paired with it stayed behind.
192
+ *
193
+ * Returns false when the snapshot cannot be used safely, and the caller must then abandon the
194
+ * rotation rather than send a half-applied identity:
195
+ *
196
+ * - Copilot pins its bearer to an account-scoped regional origin, so transport is re-resolved
197
+ * with the new account's `apiBaseUrl` instead of inheriting the previous account's host. The
198
+ * snapshot value is RESOLVED first: `rotatedProvider` is a clone of the FAILED account's
199
+ * provider, so passing a bare `undefined` origin let the transport resolver fall through its
200
+ * own `?? validateCopilotApiBaseUrl(provider.baseUrl)` step to the previous account's host —
201
+ * pairing B's bearer with A's accepted origin. Login and refresh always persist a resolved
202
+ * origin, so this fallback protects malformed or manually seeded credentials.
203
+ * - A Cloud Code Assist provider needs an account-matched project. Antigravity's refresh path
204
+ * tolerates project discovery failing, so a stored account can legitimately have no project;
205
+ * sending that account's bearer with the FAILED account's project is worse than not rotating.
206
+ */
207
+ const applyFailoverSnapshot = async (
208
+ snapshot: OAuthAccessSnapshot,
209
+ retryParsed: OcxParsedRequest = parsed,
210
+ ): Promise<boolean> => {
211
+ if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
212
+ const committed = await commitResolvedOAuthSelection(snapshot);
213
+ if (!committed) return false;
214
+ snapshot = committed;
215
+ let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
216
+ if (route.providerName === "github-copilot") {
217
+ rotatedProvider = resolveProviderTransport(
218
+ route.providerName,
219
+ rotatedProvider,
220
+ parsed.options.promptCacheKey,
221
+ resolveCopilotApiBaseUrl(snapshot.apiBaseUrl),
222
+ ) as OcxProviderConfig;
223
+ }
224
+ if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
225
+ route.provider = rotatedProvider;
226
+ if (route.providerName === "kiro") {
227
+ const kiroContext = { ...(snapshot.kiro ?? {}) };
228
+ // Terminal-guard continuations are rebuilt from a shallow clone. Updating only the
229
+ // outer request pairs the new bearer with the failed account's region/profile on
230
+ // the retry. Keep both owners synchronized; for ordinary paths they are identical.
231
+ parsed._kiroAuthContext = kiroContext;
232
+ if (retryParsed !== parsed) retryParsed._kiroAuthContext = { ...kiroContext };
233
+ }
234
+ // Re-stamp: a request that rotated accounts must be attributed to the account that actually
235
+ // served it. All three rotation sites funnel through here, so this is the only re-stamp
236
+ // needed -- and putting it anywhere else would let one of the three drift.
237
+ stampOAuthAccountLabel(logCtx, route.providerName, route.provider, snapshot.accountId);
238
+ if (route.providerName === "anthropic") {
239
+ anthropicPoolAccountId = snapshot.accountId;
240
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", snapshot.accountId, config);
241
+ } else {
242
+ genericFailoverAccountId = snapshot.accountId;
243
+ }
244
+ sentOAuthSnapshot = snapshot;
245
+ replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
246
+ return true;
247
+ };
248
+ // Key sends may be rebuilt while queued. Keep metadata pending until the guarded
249
+ // physical dispatch binds it to the selection that actually reaches the upstream.
250
+ let pendingKeySend: { estimate: number | undefined; recovery?: AttemptRecoveryKind } | undefined;
251
+ const noteRoutedAttemptSend = (estimate: number | undefined, recovery?: AttemptRecoveryKind): void => {
252
+ if (usesApiKeyAccount(route.provider)) pendingKeySend = { estimate, recovery };
253
+ else noteProviderAttemptSend(logCtx, route.providerName, route.provider, estimate, recovery);
254
+ };
255
+ const commitKeyAttemptSend = (): void => {
256
+ if (!usesApiKeyAccount(route.provider)) return;
257
+ noteProviderAttemptSend(logCtx, route.providerName, route.provider,
258
+ pendingKeySend?.estimate ?? logCtx.usageLogInputTokens, pendingKeySend?.recovery);
259
+ pendingKeySend = undefined;
260
+ };
261
+ const bindKeyUsageFromBridge = (usage: OcxUsage | undefined): void => {
262
+ logCtx.usageFromBridge = true;
263
+ if (usesApiKeyAccount(route.provider)) {
264
+ logCtx.usage = logCtx.activeAttempt?.usage;
265
+ return;
266
+ }
267
+ if (usage) {
268
+ logCtx.usage = usage;
269
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
270
+ }
271
+ };
272
+ const selectionIsCurrent = (binding: DispatchBinding | undefined): boolean => {
273
+ if (route.provider.authMode === "forward") return true;
274
+ if (!binding) return false;
275
+ if (binding.kind === "api-key") return providerApiKeySelectionIsCurrent(config, route.providerName, binding.provider);
276
+ const selected = captureOAuthAccountSelection(route.providerName);
277
+ const row = getAccountCredentialWithStatus(route.providerName, binding.snapshot.accountId);
278
+ return selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision
279
+ && !!row && !row.needsReauth && row.credential.expires > Date.now()
280
+ && credentialGeneration(row.credential) === binding.snapshot.generation;
281
+ };
282
+ const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
283
+ const resolved = resolveAdapter(provider, retention, route.providerName);
284
+ if (route.provider.authMode === "forward") return resolved;
285
+ const binding: DispatchBinding | undefined = route.provider.authMode === "oauth"
286
+ ? oauthSelection && servingOAuthSnapshot
287
+ ? { kind: "oauth", selection: { ...oauthSelection }, snapshot: servingOAuthSnapshot }
288
+ : undefined
289
+ : { kind: "api-key", provider: { ...route.provider } };
290
+ if (binding) adapterBindings.set(resolved, binding);
291
+ // Observe terminals before search/image loops or continuation guards hide earlier rounds.
292
+ // Each adapter parser is called once per physical response; bridge totals are client-only.
293
+ const observedResponses = new WeakSet<object>();
294
+ const observeUsage = (event: AdapterEvent, response: object): void => {
295
+ if (usesApiKeyAccount(provider) && "usage" in event && event.usage && !observedResponses.has(response)) {
296
+ observedResponses.add(response);
297
+ recordKeyAttemptUsage(logCtx, event.usage);
298
+ }
299
+ };
300
+ const parseStream = resolved.parseStream.bind(resolved);
301
+ resolved.parseStream = async function* (...args) {
302
+ for await (const event of parseStream(...args)) { observeUsage(event, args[0]); yield event; }
303
+ };
304
+ if (resolved.parseResponse) {
305
+ const parseResponse = resolved.parseResponse.bind(resolved);
306
+ resolved.parseResponse = async (...args) => {
307
+ const events = await parseResponse(...args);
308
+ events.forEach(event => observeUsage(event, args[0]));
309
+ return events;
310
+ };
311
+ }
312
+ const build = resolved.buildRequest.bind(resolved);
313
+ resolved.buildRequest = async (requestParsed, incoming) => {
314
+ const request = await build(requestParsed, incoming);
315
+ // Capture at adapter creation, never from mutable serving state after an await.
316
+ if (binding) requestBindings.set(request, binding);
317
+ return request;
318
+ };
319
+ if (resolved.runTurn) {
320
+ const runTurn = resolved.runTurn.bind(resolved);
321
+ rawRunTurns.set(resolved, (requestParsed, incoming, emit) => {
322
+ const response = {};
323
+ return runTurn(requestParsed, incoming, event => { observeUsage(event, response); emit(event); });
324
+ });
325
+ resolved.runTurn = (requestParsed, incoming, emit) => runSelectedTurn(resolved, requestParsed, incoming, emit);
326
+ }
327
+ return resolved;
328
+ };
329
+ const refreshDispatchAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
330
+ if (route.provider.authMode === "oauth") {
331
+ if (!servingOAuthSnapshot || !await applyFailoverSnapshot(servingOAuthSnapshot, requestParsed)) {
332
+ throw new Error("OAuth account selection changed before dispatch");
333
+ }
334
+ } else {
335
+ const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
336
+ if (!current) throw new Error("API key selection is unavailable before dispatch");
337
+ route.provider = current;
338
+ }
339
+ adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
340
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
341
+ );
342
+ invalidateSameTargetRequest();
343
+ return adapter;
344
+ };
345
+ const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
346
+ requestParsed._cursorIdentityScope = undefined;
347
+ requestParsed._cursorConversationId = undefined;
348
+ if (requestParsed._providerContinuation?.cursor) {
349
+ const { cursor: _oldCursor, ...rest } = requestParsed._providerContinuation;
350
+ requestParsed._providerContinuation = rest;
351
+ }
352
+ return refreshDispatchAdapter(requestParsed);
353
+ };
354
+ const runSelectedTurn = async (
355
+ selectedAdapter: ProviderAdapter,
356
+ ...[requestParsed, incoming, emit]: Parameters<NonNullable<ProviderAdapter["runTurn"]>>
357
+ ): Promise<void> => {
358
+ for (let attempt = 0; attempt < 3; attempt++) {
359
+ if (!selectionIsCurrent(adapterBindings.get(selectedAdapter))) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
360
+ const binding = adapterBindings.get(selectedAdapter);
361
+ const run = rawRunTurns.get(selectedAdapter);
362
+ if (!run) throw new Error("Selected provider no longer supports this turn transport");
363
+ let sent = false;
364
+ let refused = false;
365
+ // Both main and image-loop callers already acquired the initial pacing slot.
366
+ // Subsequent physical messages retain this adapter/credential and are paced normally.
367
+ const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
368
+ providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
369
+ beforeDispatch: () => {
370
+ if (sent) return;
371
+ if (!selectionIsCurrent(binding)) {
372
+ refused = true;
373
+ throw new Error("Account selection changed before the first turn dispatch");
374
+ }
375
+ commitKeyAttemptSend();
376
+ sent = true;
377
+ },
378
+ });
379
+ try {
380
+ await run(requestParsed, { ...incoming, providerFetch: fetch }, event => { if (!refused) emit(event); });
381
+ } catch (error) {
382
+ if (!refused) throw error;
383
+ }
384
+ if (!refused) return;
385
+ // The adapter may map the guard's exception to an error event. Neither that
386
+ // event nor a refused send may escape before retrying the newly selected account.
387
+ selectedAdapter = await refreshRunTurnAdapter(requestParsed);
388
+ }
389
+ throw new Error("Account selection changed repeatedly before turn dispatch");
390
+ };
391
+ const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed): ProviderFetchOptions["dispatchOverride"] => {
392
+ if (route.provider.authMode === "forward") return undefined;
393
+ return async (input, init, execute) => {
394
+ let destination = input;
395
+ let dispatchInit = init;
396
+ for (let attempt = 0; attempt < 3; attempt++) {
397
+ if (selectionIsCurrent(requestBindings.get(wireRequest))) {
398
+ const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
399
+ const binding = requestBindings.get(wireRequest);
400
+ const snapshot = route.providerName === "anthropic" && anthropicPoolAccountId && binding?.kind === "oauth"
401
+ ? binding.snapshot : undefined;
402
+ const writerGeneration = snapshot ? captureConfigGeneration() : 0;
403
+ const sentHeaders = snapshot ? new Headers(dispatchInit.headers) : undefined;
404
+ const ownsBearer = snapshot !== undefined
405
+ && sentHeaders?.get("authorization") === `Bearer ${snapshot.accessToken}`
406
+ && !sentHeaders?.has("x-api-key");
407
+ // Reselection can choose a provider override instead of the supplied executor.
408
+ commitKeyAttemptSend();
409
+ const response = await fetchImpl(destination, { ...dispatchInit, redirect: "manual" });
410
+ if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
411
+ // Observe each physical response before retries replace it. The binding belongs to
412
+ // this dispatch, so a manual switch cannot file A's headers against B. Header
413
+ // overrides and credential replacement make ownership unprovable: skip those writes.
414
+ if (ownsBearer && snapshot) {
415
+ try {
416
+ const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
417
+ if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation) {
418
+ recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, response.headers, writerGeneration);
419
+ }
420
+ } catch { /* best-effort observation cannot fail the response */ }
421
+ }
422
+ return response;
423
+ }
424
+ const nextAdapter = await refreshDispatchAdapter(requestParsed);
425
+ const rebuilt = await nextAdapter.buildRequest(requestParsed, {
426
+ headers: requestState.selectedForwardHeaders, translatorBudget,
427
+ ...(imageTierBias > 0 ? { imageTierBias } : {}),
428
+ });
429
+ const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
430
+ if (!bodySize.admitted) {
431
+ rebuilt.releaseBodyObservation?.();
432
+ return formatErrorResponse(413, "outbound_body_too_large", describeOutboundBodyRefusal(bodySize));
433
+ }
434
+ const headers = new Headers(dispatchInit.headers);
435
+ for (const name of Object.keys(wireRequest.headers)) headers.delete(name);
436
+ for (const [name, value] of Object.entries(rebuilt.headers)) headers.set(name, value);
437
+ wireRequest.releaseBodyObservation?.();
438
+ Object.assign(wireRequest, rebuilt);
439
+ const binding = requestBindings.get(rebuilt);
440
+ if (binding) requestBindings.set(wireRequest, binding);
441
+ else requestBindings.delete(wireRequest);
442
+ sameTargetRequest = wireRequest;
443
+ sameTargetParsed = requestParsed;
444
+ sameTargetToken = transportToken;
445
+ destination = rebuilt.url;
446
+ dispatchInit = { ...dispatchInit, method: rebuilt.method, headers, body: rebuilt.body };
447
+ bindRouteReasoningReplayScope({ parsed: requestParsed, providerName: route.providerName, provider: route.provider,
448
+ adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
449
+ // The next iteration validates synchronously and calls fetch in that same turn.
450
+ }
451
+ throw new Error("OAuth account selection changed repeatedly before dispatch");
452
+ };
453
+ };
454
+ const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
455
+ ? anthropicSessionKeyFromParts({
456
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
457
+ threadIdHeader: req.headers.get("thread-id"),
458
+ promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
459
+ clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
460
+ promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
461
+ })
462
+ : null;
463
+ if (route.provider.authMode === "oauth") {
464
+ try {
465
+ if (route.providerName === "anthropic" && isAnthropicAccountPoolEnabled(config)) {
466
+ const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config);
467
+ if (!selection.accountId) {
468
+ if (selection.reason === "all-cooled") {
469
+ const retryAfterSec = getAnthropicPoolRetryAfterSeconds();
470
+ return formatErrorResponse(
471
+ 429,
472
+ "rate_limit_error",
473
+ "All Anthropic OAuth accounts are temporarily rate-limited",
474
+ retryAfterSec !== null ? { retryAfter: String(retryAfterSec) } : undefined,
475
+ );
476
+ }
477
+ return formatErrorResponse(401, "authentication_error", "No eligible Anthropic OAuth account available");
478
+ }
479
+ const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(selection.accountId), true, selection.reason);
480
+ if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
481
+ anthropicPoolAccountId = admitted.accountId;
482
+ route.provider = { ...route.provider, apiKey: admitted.accessToken };
483
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
484
+ } else {
485
+ // Prefer the account with known headroom BEFORE the first attempt. Rotation alone
486
+ // only reacts to a 429, so a turn could open on an account a previous probe already
487
+ // measured as spent. A null answer means "use the active account", so every provider
488
+ // without quota evidence keeps the resolution it has today.
489
+ const preferredAccountId = isGenericFailoverProvider(route.providerName, route.provider)
490
+ ? preferredInitialAccount(config, route.providerName)
491
+ : null;
492
+ // Resolved account-scoped, NOT through failoverAccountSnapshot: that helper marks a
493
+ // rotation site, and rotation sites must apply their credential through
494
+ // applyFailoverSnapshot's pairing rules. This is initial resolution — the code below
495
+ // already pairs the snapshot's Kiro metadata, Copilot origin and Antigravity project
496
+ // with this same bearer, exactly as it does for the active account.
497
+ let usedPreferredAccount = preferredAccountId !== null;
498
+ let resolved: OAuthAccessSnapshot;
499
+ if (preferredAccountId) {
500
+ try {
501
+ // `requireUsableAccount` makes a removed OR reauth-flagged account throw from
502
+ // inside the resolver's own store read. Without it a revoked account resolves
503
+ // successfully — its credential is still readable — and the request would
504
+ // dispatch on an account already known to need a fresh login.
505
+ resolved = await getValidAccessSnapshotForAccount(
506
+ route.providerName,
507
+ preferredAccountId,
508
+ { requireUsableAccount: true },
509
+ );
510
+ } catch {
511
+ // The roster is read behind a short TTL, so a preferred account can be removed
512
+ // or flagged for reauth in the window after it was cached. Resolving it then
513
+ // throws, and a PREFERENCE that turns a healthy request into a 401 is worse
514
+ // than no preference at all — the active account is still perfectly usable.
515
+ // Drop the stale roster so the next request re-reads it, and carry on.
516
+ forgetGenericFailoverRoster(route.providerName);
517
+ usedPreferredAccount = false;
518
+ resolved = await getValidAccessTokenSnapshot(route.providerName);
519
+ }
520
+ } else {
521
+ resolved = await getValidAccessTokenSnapshot(route.providerName);
522
+ }
523
+ // A Cloud Code Assist account needs its own project. Antigravity's refresh path
524
+ // tolerates project discovery failing, so a stored account can legitimately have
525
+ // none — and a PREFERENCE must never turn a working request into an error. Fall
526
+ // back to the ordinary active-account resolution instead, which is exactly what
527
+ // would have happened had the preference never existed.
528
+ if (usedPreferredAccount && route.provider.googleMode === "cloud-code-assist" && !resolved.projectId) {
529
+ resolved = await getValidAccessTokenSnapshot(route.providerName);
530
+ usedPreferredAccount = false;
531
+ }
532
+ const admitted = await commitResolvedOAuthSelection(resolved, true);
533
+ if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
534
+ if (admitted.accountId !== resolved.accountId) usedPreferredAccount = true;
535
+ resolved = admitted;
536
+ replayOAuthCredentialSnapshot = {
537
+ accountId: resolved.accountId,
538
+ generation: resolved.generation,
539
+ };
540
+ if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
541
+ route.provider = { ...route.provider, apiKey: resolved.accessToken };
542
+ // Attribution is independent of failover (#2699): stamped from the resolved snapshot
543
+ // itself, not from inside the `isGenericFailoverProvider` branch below, so a future
544
+ // narrowing of that predicate cannot silently switch attribution off.
545
+ stampOAuthAccountLabel(logCtx, route.providerName, route.provider, resolved.accountId);
546
+ // Remember which account actually served this request so a 429 cools THAT one, not
547
+ // whichever account is active by the time the response comes back (#2568).
548
+ if (isGenericFailoverProvider(route.providerName, route.provider)) {
549
+ genericFailoverAccountId = resolved.accountId;
550
+ // Advance the pool cursor only now that this account is actually admitted. The
551
+ // helper returns immediately unless the kernel is on AND the strategy is
552
+ // round-robin, so quota and fill-first pools reach it without being touched.
553
+ noteGenericPoolSelection(config, route.providerName, resolved.accountId);
554
+ }
555
+ // Anthropic is excluded from isGenericFailoverProvider -- its own pool owns affinity and
556
+ // a fail-closed local-cli credential rule -- so without this stamp its identity is
557
+ // dropped whenever the pool flag is off, and a later 429 has no account to cool. Reactive
558
+ // failover needs only the id: no affinity bind, no promotion, no quota-ranked pick. Those
559
+ // are proactive and stay behind anthropicAccountPool.enabled.
560
+ if (route.providerName === "anthropic" && hasAnthropicFailoverQuorum()) {
561
+ anthropicPoolAccountId = resolved.accountId;
562
+ }
563
+ // Captured beside the account it fences, so the two can never disagree.
564
+ if (hasPassiveAccountQuota(route.providerName)) {
565
+ passiveQuotaWriterGeneration = captureConfigGeneration();
566
+ }
567
+ if (route.providerName === "kiro") {
568
+ // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
569
+ // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
570
+ parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
571
+ }
572
+ // Project identity belongs to the admitted account on EVERY request, including
573
+ // the request after a pool transition made that account the persisted active one.
574
+ if (route.provider.googleMode === "cloud-code-assist") {
575
+ if (!resolved.projectId) return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist account project is unavailable")));
576
+ route.provider = { ...route.provider, project: resolved.projectId };
577
+ }
578
+ }
579
+ } catch (err) {
580
+ if (err instanceof UnsupportedOAuthProviderError) {
581
+ const safeProviderName = redactSecretString(route.providerName);
582
+ return formatErrorResponse(
583
+ 400,
584
+ "invalid_request_error",
585
+ `${redactSecretString(err.message)}. Remove or reconfigure provider '${safeProviderName}' in the OpenCodex configuration.`,
586
+ );
587
+ }
588
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
589
+ }
590
+ }
591
+ // Key-auth twin of the OAuth preference above: pick a warm key BEFORE the first attempt when
592
+ // the committed one is already cooling, instead of spending the request earning a 429 the
593
+ // runtime could already predict. The picker refuses to override a healthy committed key and
594
+ // returns null without a configured strategy, so an ordinary install evaluates one predicate.
595
+ //
596
+ // It RETURNS a rebuilt route rather than mutating one, and the assignment has to land here --
597
+ // ahead of the transport pin below, the adapterProvider copy that follows it, and the request
598
+ // the HTTP path bakes later. The image bridge and web search read route.provider directly and
599
+ // have no stale-selection re-read to save them, so ordering is the whole correctness argument.
600
+ //
601
+ // The Transport variant, not the bare picker: the picker answers with the PERSISTED row, and
602
+ // a built-in provider stored in its valid minimal form would lose the adapter id, base URL
603
+ // and static headers registry backfill supplies, throwing `Unknown adapter: undefined`.
604
+ const proactiveKeyProvider = selectProactiveApiKeyTransport(
605
+ config,
606
+ route.providerName,
607
+ route.provider,
608
+ parsed.options.promptCacheKey,
609
+ );
610
+ if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
611
+ route.provider = resolveProviderTransport(
612
+ route.providerName,
613
+ route.provider,
614
+ parsed.options.promptCacheKey,
615
+ route.providerName === "github-copilot" && route.provider.authMode === "oauth"
616
+ ? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
617
+ : undefined,
618
+ );
619
+ let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
620
+ const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
621
+ && !isCanonicalOpenAiForwardProvider(adapterProvider)
622
+ && ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
623
+ || providerConsumesCallerAuthorization(adapterProvider));
624
+ if (stripClaudeMainAuth) {
625
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
626
+ admissionState.authCtx = { kind: "main", accountId: null };
627
+ route.provider = stripCodexRuntimeProviderFields(route.provider);
628
+ adapterProvider = stripCodexRuntimeProviderFields(adapterProvider);
629
+ requestState.selectedForwardHeaders = new Headers(requestState.selectedForwardHeaders);
630
+ requestState.selectedForwardHeaders.delete("authorization");
631
+ requestState.selectedForwardHeaders.delete("chatgpt-account-id");
632
+ delete route.codexAccountMode;
633
+ delete route.codexAccountId;
634
+ delete route.codexAccountNamespace;
635
+ logCtx.provider = route.providerName;
636
+ delete logCtx.accountLogLabel;
637
+ }
638
+ adapter = resolveSelectionAdapter(adapterProvider, config.cacheRetention);
639
+ bindRouteReasoningReplayScope({
640
+ parsed,
641
+ providerName: route.providerName,
642
+ provider: adapterProvider,
643
+ adapterName: adapter.name,
644
+ oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
645
+ codexAuthContext: admissionState.authCtx,
646
+ forwardHeaders: requestState.selectedForwardHeaders,
647
+ });
648
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
649
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
650
+ }
651
+ logCtx.providerAdapter = adapter.name;
652
+ // Ordinary requests receive one durable attempt only after their final initial
653
+ // adapter is resolved. Combo children own their attempt and retries keep it.
654
+ if (!options.comboAttempt && !logCtx.activeAttempt) {
655
+ const attempt = beginRequestAttempt(
656
+ (logCtx.attempts?.length ?? 0) + 1,
657
+ logCtx.provider,
658
+ route.modelId,
659
+ adapter.name,
660
+ );
661
+ logCtx.activeAttempt = attempt;
662
+ logCtx.activeAttemptStartedAt = Date.now();
663
+ (logCtx.attempts ??= []).push(attempt);
664
+ }
665
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
666
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, adapterProvider, adapter.name);
667
+ runTurnAdapter = adapter;
668
+ if (adapter.runTurn) {
669
+ recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
670
+ }
671
+ // Optional route-identity linkage for attempt correlation (CL-09 consumes it). The slot
672
+ // resolves to null unless an opt-in subsystem registered a linker, so an install without
673
+ // routing profiles does no work here and loads no additional module. The non-throwing
674
+ // guarantee lives in the slot helper.
675
+ if (logCtx.activeAttempt && !logCtx.activeAttempt.labRouteSubjectId) {
676
+ const passiveSubjectId = resolvePassiveRouteSubjectId(
677
+ config,
678
+ route.providerName,
679
+ route.modelId,
680
+ route.provider,
681
+ inboundWire,
682
+ );
683
+ if (passiveSubjectId) logCtx.activeAttempt.labRouteSubjectId = passiveSubjectId;
684
+ }
685
+ const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
686
+
687
+ const rawInput = (parsed._rawBody as { input?: unknown }).input;
688
+ if (!isPassthrough && Array.isArray(rawInput) && rawInput.some(
689
+ item => item !== null && typeof item === "object" && item.type === "computer_call_output",
690
+ )) {
691
+ return formatErrorResponse(
692
+ 400,
693
+ "invalid_request_error",
694
+ "computer_call_output requires a Responses passthrough route; send screenshots as user input_image content on translated routes.",
695
+ );
696
+ }
697
+
698
+ return {
699
+ isOAuth401ReplayProvider,
700
+ get sentOAuthSnapshot(): OAuthAccessSnapshot | undefined {
701
+ return sentOAuthSnapshot;
702
+ },
703
+ set sentOAuthSnapshot(value: OAuthAccessSnapshot | undefined) {
704
+ sentOAuthSnapshot = value;
705
+ },
706
+ get replayOAuthCredentialSnapshot(): Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined {
707
+ return replayOAuthCredentialSnapshot;
708
+ },
709
+ set replayOAuthCredentialSnapshot(value: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined) {
710
+ replayOAuthCredentialSnapshot = value;
711
+ },
712
+ get anthropicPoolAccountId(): string | null {
713
+ return anthropicPoolAccountId;
714
+ },
715
+ set anthropicPoolAccountId(value: string | null) {
716
+ anthropicPoolAccountId = value;
717
+ },
718
+ get anthropicPoolFailovers(): typeof anthropicPoolFailovers {
719
+ return anthropicPoolFailovers;
720
+ },
721
+ set anthropicPoolFailovers(value: typeof anthropicPoolFailovers) {
722
+ anthropicPoolFailovers = value;
723
+ },
724
+ get genericFailoverAccountId(): string | null {
725
+ return genericFailoverAccountId;
726
+ },
727
+ set genericFailoverAccountId(value: string | null) {
728
+ genericFailoverAccountId = value;
729
+ },
730
+ get genericFailovers(): typeof genericFailovers {
731
+ return genericFailovers;
732
+ },
733
+ set genericFailovers(value: typeof genericFailovers) {
734
+ genericFailovers = value;
735
+ },
736
+ get adapter(): ProviderAdapter {
737
+ return adapter;
738
+ },
739
+ set adapter(value: ProviderAdapter) {
740
+ adapter = value;
741
+ },
742
+ get activeAdapter(): ProviderAdapter {
743
+ return activeAdapter;
744
+ },
745
+ set activeAdapter(value: ProviderAdapter) {
746
+ activeAdapter = value;
747
+ },
748
+ get runTurnAdapter(): ProviderAdapter {
749
+ return runTurnAdapter;
750
+ },
751
+ set runTurnAdapter(value: ProviderAdapter) {
752
+ runTurnAdapter = value;
753
+ },
754
+ get sameTargetRequest(): AdapterRequest | undefined {
755
+ return sameTargetRequest;
756
+ },
757
+ set sameTargetRequest(value: AdapterRequest | undefined) {
758
+ sameTargetRequest = value;
759
+ },
760
+ get sameTargetParsed(): OcxParsedRequest | undefined {
761
+ return sameTargetParsed;
762
+ },
763
+ set sameTargetParsed(value: OcxParsedRequest | undefined) {
764
+ sameTargetParsed = value;
765
+ },
766
+ get sameTargetToken(): typeof sameTargetToken {
767
+ return sameTargetToken;
768
+ },
769
+ set sameTargetToken(value: typeof sameTargetToken) {
770
+ sameTargetToken = value;
771
+ },
772
+ get transportToken(): typeof transportToken {
773
+ return transportToken;
774
+ },
775
+ set transportToken(value: typeof transportToken) {
776
+ transportToken = value;
777
+ },
778
+ get imageTierBias(): typeof imageTierBias {
779
+ return imageTierBias;
780
+ },
781
+ set imageTierBias(value: typeof imageTierBias) {
782
+ imageTierBias = value;
783
+ },
784
+ invalidateSameTargetRequest,
785
+ requestBindings,
786
+ adapterBindings,
787
+ commitResolvedOAuthSelection,
788
+ refreshResolvedOAuthSelection,
789
+ passiveQuotaWriterGeneration,
790
+ applyFailoverSnapshot,
791
+ selectionIsCurrent,
792
+ resolveSelectionAdapter,
793
+ refreshRunTurnAdapter,
794
+ oauthDispatch,
795
+ noteRoutedAttemptSend,
796
+ commitKeyAttemptSend,
797
+ bindKeyUsageFromBridge,
798
+ anthropicSessionKey,
799
+ isPassthrough,
800
+ };
801
+ }
802
+
803
+ export type ResponsesTransport = Exclude<Awaited<ReturnType<typeof prepareResponsesTransport>>, Response>;