@bitkyc08/opencodex 2.55.0 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,1476 @@
1
+ import type {
2
+ ResponsesRequestContext,
3
+ ResponsesAdmissionState,
4
+ PassthroughAdmissionState,
5
+ } from "./core-options";
6
+ import type { PreparedResponsesRequest } from "./request-prepare";
7
+ import type { ResponsesTransport } from "./request-transport";
8
+ import type { ResponsesEffects } from "./response-effects";
9
+ import type { ResponsesSendBudget } from "./request-send-budget";
10
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
11
+ import { codexSafetyBufferingFilterOptions, terminalStatusFromParsed } from "../relay";
12
+ import { imageGenToolCallAliases } from "../responses-image-gen-repair";
13
+ import { rememberResponseState } from "../../responses/state";
14
+ import {
15
+ currentTurnWireToolCatalogBody,
16
+ hasExplicitWireToolCatalog,
17
+ collectDeclaredWireToolNames,
18
+ collectDeclaredBareWireToolNames,
19
+ collectDeclaredNamelessClientCallTypes,
20
+ collectProviderExecutedCallTypes,
21
+ undeclaredToolCallName,
22
+ undeclaredToolCallNameInResponse,
23
+ normalizeDefaultNamespaceInResponse,
24
+ } from "../responses-undeclared-tool-guard";
25
+ import { collectSelfNamedNamespaceScrubAuthorization } from "../responses-self-named-namespace-scrub";
26
+ import type { ProviderExecutedCallType } from "../responses-undeclared-tool-guard";
27
+ import {
28
+ releaseCodexAuthContextProbeLease,
29
+ unwrapUpstreamRetryEvidenceError,
30
+ codexProbeLeaseId,
31
+ codexProbeQuotaScope,
32
+ createCodexReserveDispatchGuard,
33
+ } from "../../codex/auth-context";
34
+ import {
35
+ NamespaceToolCollisionError,
36
+ restoreRoutedNamespaceCalls,
37
+ } from "../../responses/namespace-tool-compat";
38
+ import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
39
+ import { formatErrorResponse } from "../../bridge";
40
+ import { redactSecretString } from "../../lib/redact";
41
+ import {
42
+ collectFunctionCallRepairSchemas,
43
+ repairFunctionCallsInJson,
44
+ } from "../../responses/function-call-compat";
45
+ import type { RoutedNamespaceToolAliases } from "../../responses/namespace-tool-compat";
46
+ import { hasResponsesSnapshotRepair, repairResponsesSnapshotJson } from "../responses-snapshot-repair";
47
+ import { backfillResponsesFieldsJson } from "./responses-field-backfill";
48
+ import type { AdapterRequest } from "../../adapters/base";
49
+ import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
50
+ import { CODE_MODE_EXEC_TOOL_NAME } from "../../types";
51
+ import type { ResponsesTerminalStatus } from "../../bridge";
52
+ import { hasPassiveAccountQuota, recordPassiveAccountQuota } from "../../providers/quota";
53
+ import {
54
+ isMuseSubscriptionUsagePayload,
55
+ parseMuseSubscriptionUsage,
56
+ } from "../../providers/muse-subscription-usage";
57
+ import { restoreMuseToolNames } from "../../responses/muse-tool-name-alias";
58
+ import { restoreRoutedCustomCalls } from "../../responses/custom-tool-compat";
59
+ import { restorePlaintextV2AgentMessageCalls } from "../../responses/plaintext-v2-agent-messages";
60
+ import {
61
+ recordAdapterReasoning,
62
+ recordAdapterTier,
63
+ noteAttemptSend,
64
+ sealRequestAttemptIdentity,
65
+ recordAttemptCredentialSource,
66
+ } from "../request-log";
67
+ import {
68
+ upstreamHostHealthKey,
69
+ normalizeUpstreamHostCircuitThreshold,
70
+ disableUpstreamHostCircuitForKey,
71
+ acquireUpstreamHostAdmission,
72
+ resetUpstreamHostHealth,
73
+ releaseUpstreamHostAdmission,
74
+ recordUpstreamHostFailure,
75
+ } from "../../codex/upstream-host-health";
76
+ import {
77
+ safeOriginLabel,
78
+ fetchWithHeaderTimeout,
79
+ providerFetch,
80
+ safeHostLabel,
81
+ storedPoolReplayDispatchNotifier,
82
+ } from "./fetch-helpers";
83
+ import { clientCancelledResponse } from "./core-errors";
84
+ import {
85
+ upstreamHostCircuitOpenResponse,
86
+ usesCodexForwardPoolAuth,
87
+ codexWsQuotaObserver,
88
+ isFixedCodexAccount,
89
+ shouldRetryCodexPoolAccountModel400,
90
+ shouldRetryCodexPoolAccountQuota,
91
+ shouldRetryCodexPoolAccountTransient,
92
+ retryCodexPoolOnAlternateAccount,
93
+ } from "./core-codex-account";
94
+ import { readCodexWsStage } from "./codex-ws-wire";
95
+ import { linkAbortSignal } from "./core-lifetime";
96
+ import type { CodexAuthContext } from "../../codex/auth-context";
97
+ import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
98
+ import { streamingContextOverflowResponse } from "./context-overflow";
99
+ import {
100
+ SendBudgetExhaustedError,
101
+ fetchWithTransientRetry,
102
+ applyUpstreamRecoveryInit,
103
+ TRANSIENT_RETRY_MAX_ATTEMPTS,
104
+ prepareSameTarget429Wait,
105
+ sleepWithAbort,
106
+ } from "../../lib/upstream-retry";
107
+ import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
108
+ import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
109
+ import { recordCodexUpstreamOutcome } from "../../codex/routing";
110
+ import { describeUpstreamConnectFailure } from "./upstream-error";
111
+ import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
112
+ import { rateLimitRetryPolicyFor, rateLimitRetryDelayMs } from "../../providers/key-failover";
113
+ import type { AttemptRecoveryKind } from "../../usage/log";
114
+ import { resolveWireProtocolOverride } from "../adapter-resolve";
115
+ import { refreshPoolForwardAuth, refreshNativeMainForwardAuth, withClaudeNativeSession } from "./core-auth";
116
+ import { bindRouteReasoningReplayScope } from "./core-replay";
117
+ import type { OAuthAccessSnapshot } from "../../oauth";
118
+ import { publicOAuthAuthenticationErrorMessage } from "../../oauth";
119
+ import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
120
+ import {
121
+ GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
122
+ isGenericOAuthFailoverEnabled,
123
+ rotateGenericOAuthAccountOn429,
124
+ failoverAccountSnapshot,
125
+ } from "../../oauth/generic-account-failover";
126
+ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
127
+ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
128
+ import {
129
+ attemptOpaqueBlobRecovery,
130
+ outboundResponsesBodyCarriesEncryptedFunctionOutput,
131
+ resetStreamedOpaqueBlobLogContext,
132
+ consoleGoUploadRejectionBody,
133
+ CONSOLE_GO_UPLOAD_RETRY_DELAY_MS,
134
+ reasoningEffortRejectionText,
135
+ } from "./core-opaque-recovery";
136
+ import type { RequestLogContext } from "../request-log";
137
+ import { preflightComboStreamResponse } from "./combo-stream-preflight";
138
+ import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION } from "../../lib/errors";
139
+ import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit";
140
+ import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
141
+
142
+ /** Prepares and recovers one native Responses exchange before client commitment. */
143
+ export async function preparePassthroughExchange(
144
+ requestContext: Pick<ResponsesRequestContext, "config" | "logCtx" | "options" | "req">,
145
+ admissionState: ResponsesAdmissionState,
146
+ nativeHostState: PassthroughAdmissionState,
147
+ requestState: Pick<
148
+ PreparedResponsesRequest,
149
+ | "route"
150
+ | "toolBridgeMaps"
151
+ | "parsed"
152
+ | "translatorBudget"
153
+ | "responseStateOptions"
154
+ | "selectedForwardHeaders"
155
+ | "clientRequestedStream"
156
+ | "inboundWire"
157
+ | "substituteMainCredential"
158
+ | "callerAuthHeaders"
159
+ | "subagentFallbackAccountId"
160
+ >,
161
+ transportState: Pick<
162
+ ResponsesTransport,
163
+ | "adapter"
164
+ | "genericFailoverAccountId"
165
+ | "passiveQuotaWriterGeneration"
166
+ | "oauthDispatch"
167
+ | "resolveSelectionAdapter"
168
+ | "isOAuth401ReplayProvider"
169
+ | "sentOAuthSnapshot"
170
+ | "refreshResolvedOAuthSelection"
171
+ | "replayOAuthCredentialSnapshot"
172
+ | "genericFailovers"
173
+ | "applyFailoverSnapshot"
174
+ >,
175
+ responseEffects: Pick<
176
+ ResponsesEffects,
177
+ | "refreshRequestToolAliases"
178
+ | "routedMuseToolNameAliases"
179
+ | "plaintextV2AgentMessageToolNames"
180
+ | "routedNamespaceToolAliases"
181
+ | "plaintextV2AgentMessageAliasedToolNames"
182
+ | "notifyResponseComplete"
183
+ >,
184
+ sendBudgetState: Pick<
185
+ ResponsesSendBudget,
186
+ | "remainingTransientSendBudget"
187
+ | "noteTransientSends"
188
+ | "recoverySendAllowance"
189
+ | "recoveryClassFor"
190
+ | "sendBudgetExhausted"
191
+ | "reserveCredentialHop"
192
+ | "pendingHopPermit"
193
+ | "workflowRootId"
194
+ >,
195
+ ) {
196
+ const { config, logCtx, options, req } = requestContext;
197
+ const {
198
+ route,
199
+ toolBridgeMaps,
200
+ parsed,
201
+ translatorBudget,
202
+ responseStateOptions,
203
+ clientRequestedStream,
204
+ inboundWire,
205
+ substituteMainCredential,
206
+ callerAuthHeaders,
207
+ } = requestState;
208
+ const {
209
+ passiveQuotaWriterGeneration,
210
+ oauthDispatch,
211
+ resolveSelectionAdapter,
212
+ isOAuth401ReplayProvider,
213
+ refreshResolvedOAuthSelection,
214
+ applyFailoverSnapshot,
215
+ } = transportState;
216
+ const { refreshRequestToolAliases, notifyResponseComplete } = responseEffects;
217
+ const {
218
+ remainingTransientSendBudget,
219
+ noteTransientSends,
220
+ recoverySendAllowance,
221
+ recoveryClassFor,
222
+ sendBudgetExhausted,
223
+ reserveCredentialHop,
224
+ workflowRootId,
225
+ } = sendBudgetState;
226
+
227
+ const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider)
228
+ ? codexSafetyBufferingFilterOptions(config)
229
+ : undefined;
230
+ const imageGenCallAliases = route.provider.authMode === "forward"
231
+ ? new Map<string, { namespace: string; name: string }>()
232
+ : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
233
+ const routedCustomToolNames = new Set<string>();
234
+ const routedCustomToolRepairNames = new Set<string>();
235
+ const routedToolSearchNames = new Set<string>();
236
+ // Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
237
+ // previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
238
+ // REST backend rejects the parameter — the adapter strips it in forward mode, so the ONLY
239
+ // way a chained turn keeps its earlier context is the local replay expansion. Record
240
+ // completed passthrough responses (force bypasses Codex's blanket store:false) so the next
241
+ // turn's expansion hits. Never record a body whose own previous_response_id failed to
242
+ // expand: its input is a delta, and storing it would replay a truncated conversation.
243
+ // Compaction turns are excluded: _rawBody still carries the full pre-compaction history and
244
+ // recording it would let a later expansion rehydrate the chain Codex just replaced.
245
+ const passthroughRecordEligible = parsed._compactionRequest !== true
246
+ && (!parsed.previousResponseId || parsed._previousResponseInputExpanded === true);
247
+ const rememberPassthroughResponse = passthroughRecordEligible
248
+ ? (response: { id?: unknown; output?: unknown; status?: unknown }) =>
249
+ rememberResponseState(parsed._rawBody, response, undefined, responseStateOptions(true))
250
+ : undefined;
251
+ if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
252
+ console.warn(
253
+ `[responses] previous_response_id ${parsed.previousResponseId} not found in local replay state `
254
+ + `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
255
+ );
256
+ }
257
+ // Preserve the caller's readable catalog boundary before provider-specific normalization can
258
+ // remove an unsupported final entry (for example xAI cached-only web search).
259
+ const replayedInputPrefixLength = parsed._replayPrefixLen ?? 0;
260
+ const clientToolAuthorizationBody = currentTurnWireToolCatalogBody(
261
+ parsed._rawBody,
262
+ replayedInputPrefixLength,
263
+ );
264
+ const selfNamedNamespaceScrubAuthorization = collectSelfNamedNamespaceScrubAuthorization(
265
+ clientToolAuthorizationBody,
266
+ toolBridgeMaps.bareCustomToolNames,
267
+ toolBridgeMaps.bareFunctionToolNames,
268
+ );
269
+ const clientExplicitWireToolCatalog = hasExplicitWireToolCatalog(clientToolAuthorizationBody);
270
+ const clientDeclaredWireToolNames = collectDeclaredWireToolNames(clientToolAuthorizationBody);
271
+ const clientDeclaredBareWireToolNames = collectDeclaredBareWireToolNames(clientToolAuthorizationBody);
272
+ const clientDeclaredNamelessCallTypes = collectDeclaredNamelessClientCallTypes(
273
+ clientToolAuthorizationBody,
274
+ );
275
+ // Hosted calls the PROVIDER runs itself. Gated on the destination actually being xAI, so a
276
+ // declaration alone cannot buy the exemption on some other upstream that never serves it.
277
+ // Provider-executed declarations are authorized from the actual outbound body, after the
278
+ // adapter has applied destination-specific injection and normalization. Client-executed tool
279
+ // authority remains bounded to the caller-owned catalog above.
280
+ const providerExecutedCallTypes = new Set<ProviderExecutedCallType>();
281
+ let request: Awaited<ReturnType<typeof transportState.adapter.buildRequest>>;
282
+ try {
283
+ request = await transportState.adapter.buildRequest(parsed, { headers: requestState.selectedForwardHeaders, translatorBudget });
284
+ } catch (error) {
285
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
286
+ // A tool catalog this proxy cannot lower onto one wire namespace is a client input error, and
287
+ // the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
288
+ // it here escaped every catch up to the Bun handler, so the same request produced an
289
+ // unstructured 500 — and no request log — depending only on whether a rotation ran first.
290
+ // Same shape for a tool_choice this proxy cannot honor: the destination rejects a schema the
291
+ // catalog had to drop, so the selector naming it is a client input error, not a 500.
292
+ if (error instanceof NamespaceToolCollisionError || error instanceof XaiToolSchemaCompatibilityError) {
293
+ return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
294
+ }
295
+ throw error;
296
+ }
297
+ const functionRepairSchemas = isCanonicalOpenAiForwardProvider(route.provider)
298
+ ? new Map()
299
+ : collectFunctionCallRepairSchemas(clientToolAuthorizationBody);
300
+ if (!isCanonicalOpenAiForwardProvider(route.provider)) {
301
+ for (const name of request.convertedRoutedCustomToolNames ?? []) {
302
+ if (
303
+ toolBridgeMaps.freeformToolNames.has(name)
304
+ || toolBridgeMaps.toolNsMap.get(name)?.freeform === true
305
+ ) routedCustomToolNames.add(name);
306
+ }
307
+ for (const name of request.routedCustomToolRepairNames ?? []) {
308
+ if (
309
+ toolBridgeMaps.freeformToolNames.has(name)
310
+ || toolBridgeMaps.toolNsMap.get(name)?.freeform === true
311
+ ) routedCustomToolRepairNames.add(name);
312
+ }
313
+ }
314
+ for (const name of request.convertedRoutedToolSearchNames ?? []) {
315
+ // The adapter already keeps this set empty when tool_choice forbids the private search.
316
+ // Its wire name may be collision-aliased, so comparing it to the caller-facing name here
317
+ // would incorrectly disable restoration for the exact ambiguous-name case the alias fixes.
318
+ routedToolSearchNames.add(name);
319
+ }
320
+ refreshRequestToolAliases(request);
321
+ // #1700: the bridged paths refuse a call to a tool the request never declared
322
+ // (`declaredToolNames`, src/bridge.ts). The passthrough had no equivalent, so a routed
323
+ // provider's top-level `apply_patch` — which under Codex code mode exists only as a nested
324
+ // `tools.apply_patch(...)` helper inside `exec`, never as a wire tool — reached Codex as a
325
+ // call it cannot execute, and the turn showed a bare `aborted` with the file untouched.
326
+ // Forward auth is the canonical ChatGPT backend speaking Codex's own protocol rather than a
327
+ // routed provider, so it keeps passing through unguarded, as it does for the rewrites above.
328
+ // The guard needs a catalog to compare against, so it stands down when the request omits one.
329
+ // An explicit empty catalog is still authoritative: it declares that no client tools may be
330
+ // called. A passthrough request can legitimately omit `tools` entirely and still receive a call
331
+ // the client understands — `tests/providers/github-copilot/github-copilot-stream-contract.test.ts` sends
332
+ // `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
333
+ // `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
334
+ // too because the proxy cannot establish the caller's declared authorization boundary.
335
+ const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
336
+ try {
337
+ const body = JSON.parse(bodyText) as unknown;
338
+ return body && typeof body === "object" && !Array.isArray(body)
339
+ ? body as Record<string, unknown>
340
+ : undefined;
341
+ } catch {
342
+ return undefined;
343
+ }
344
+ };
345
+ let outboundRequestBody: Record<string, unknown> | undefined;
346
+ const declaredWireToolNames = new Set<string>();
347
+ const declaredBareWireToolNames = new Set<string>();
348
+ const declaredNamelessClientCallTypes = new Set<string>();
349
+ // `buildToolBridgeMaps` creates a bare alias only when the caller selected exactly one
350
+ // namespaced tool through a bare tool_choice. Restore that request-bounded identity before
351
+ // authorization checks instead of admitting the bare name into the declared set: for `exec`,
352
+ // the latter would also authorize the unrelated code-mode helper names.
353
+ const authorizedBareNamespaceToolAliases: RoutedNamespaceToolAliases = new Map(
354
+ [...toolBridgeMaps.toolNsMap].flatMap(([alias, identity]) =>
355
+ alias === identity.name
356
+ ? [[alias, {
357
+ namespace: identity.namespace,
358
+ name: identity.name,
359
+ kind: identity.freeform ? "custom" as const : "function" as const,
360
+ }] as const]
361
+ : []
362
+ ),
363
+ );
364
+ const restoreAuthorizedBareNamespaceToolCalls = (value: unknown): unknown =>
365
+ restoreRoutedNamespaceCalls(value, authorizedBareNamespaceToolAliases).value;
366
+ const normalizeFunctionCompletionJson = (text: string): string => {
367
+ const snapshot = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)
368
+ ? repairResponsesSnapshotJson(text, outboundRequestBody)
369
+ : text;
370
+ // Sparse gateways need completion status inferred before schema repair can
371
+ // distinguish completed arguments from in-progress placeholders.
372
+ return repairFunctionCallsInJson(backfillResponsesFieldsJson(snapshot), functionRepairSchemas);
373
+ };
374
+ let undeclaredToolGuardActive = false;
375
+ const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
376
+ outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
377
+ providerExecutedCallTypes.clear();
378
+ if (isXaiResponsesDestination(route.provider)) {
379
+ // Preserve the caller-declared authorization recognized by the original classifier, then
380
+ // add adapter-injected declarations from the actual current-turn outbound catalog.
381
+ for (const callType of collectProviderExecutedCallTypes(clientToolAuthorizationBody)) {
382
+ providerExecutedCallTypes.add(callType);
383
+ }
384
+ const currentOutboundCatalog = currentTurnWireToolCatalogBody(
385
+ outboundRequestBody,
386
+ replayedInputPrefixLength,
387
+ );
388
+ for (const callType of collectProviderExecutedCallTypes(currentOutboundCatalog)) {
389
+ providerExecutedCallTypes.add(callType);
390
+ }
391
+ }
392
+ declaredWireToolNames.clear();
393
+ // With no replay prefix the full outbound body belongs to this turn and its normalized
394
+ // aliases are authoritative. A continuation's outbound body still contains historical
395
+ // catalogs (and may promote historical tool-search definitions), so it can never widen the
396
+ // current caller snapshot captured above.
397
+ declaredBareWireToolNames.clear();
398
+ if (replayedInputPrefixLength === 0) {
399
+ for (const name of collectDeclaredWireToolNames(outboundRequestBody)) {
400
+ declaredWireToolNames.add(name);
401
+ }
402
+ for (const name of collectDeclaredBareWireToolNames(outboundRequestBody)) {
403
+ declaredBareWireToolNames.add(name);
404
+ }
405
+ }
406
+ for (const name of clientDeclaredWireToolNames) declaredWireToolNames.add(name);
407
+ for (const name of clientDeclaredBareWireToolNames) declaredBareWireToolNames.add(name);
408
+ declaredNamelessClientCallTypes.clear();
409
+ if (replayedInputPrefixLength === 0) {
410
+ for (const callType of collectDeclaredNamelessClientCallTypes(outboundRequestBody)) {
411
+ declaredNamelessClientCallTypes.add(callType);
412
+ }
413
+ }
414
+ for (const callType of clientDeclaredNamelessCallTypes) {
415
+ declaredNamelessClientCallTypes.add(callType);
416
+ }
417
+ // On an ordinary request these maps capture caller-catalog identities that normalization may
418
+ // replace on the outbound wire (for example a client image tool becoming hosted). On replay,
419
+ // however, the parsed maps also contain historical catalog entries, so only the bounded
420
+ // current-turn wire snapshot above may authorize a call.
421
+ if (replayedInputPrefixLength === 0) {
422
+ for (const name of toolBridgeMaps.declaredToolNames) {
423
+ // `buildToolBridgeMaps` also aliases a namespaced tool under its bare name when the
424
+ // caller's `tool_choice` selected it unambiguously, which the bridge needs to route the
425
+ // call back. For `exec` alone that alias would also switch on nested-helper
426
+ // normalization and re-authorize `exec_command`/`shell_command`/`apply_patch`/`view_image`, so it is
427
+ // admitted here only when the caller's own catalog declared a bare `exec`. Selecting an
428
+ // MCP `exec` is not a declaration of the code-mode shell tool.
429
+ if (
430
+ name === CODE_MODE_EXEC_TOOL_NAME
431
+ && !clientDeclaredWireToolNames.has(CODE_MODE_EXEC_TOOL_NAME)
432
+ ) continue;
433
+ declaredWireToolNames.add(name);
434
+ }
435
+ }
436
+ undeclaredToolGuardActive = (
437
+ declaredWireToolNames.size > 0
438
+ || clientDeclaredNamelessCallTypes.size > 0
439
+ || clientExplicitWireToolCatalog
440
+ ) && route.provider.authMode !== "forward";
441
+ };
442
+ refreshUndeclaredToolGuard(request);
443
+ // A refused turn must not seed `previous_response_id` replay. The inspection branch reads the
444
+ // untouched upstream stream, so it can still observe a `response.completed` the client never
445
+ // received; checking the payload itself rather than a flag shared with the client relay keeps
446
+ // this free of tee ordering races.
447
+ //
448
+ // Checking only the terminal snapshot is not enough. An upstream can announce the undeclared
449
+ // call in `response.output_item.added`, which trips the client guard, and then close with a
450
+ // `response.completed` whose `output` is empty. The client gets `response.failed`, the terminal
451
+ // check sees nothing undeclared, and the refused turn enters continuation state anyway. So the
452
+ // rejection is sticky for the whole turn, set from every parsed payload on the inspection side.
453
+ let inspectionSawUndeclaredTool = false;
454
+ let inspectedTerminal: ResponsesTerminalStatus | null = null;
455
+ let inspectedCompletionSeen = false;
456
+ let firstTerminalAllowsRecall = false;
457
+ const passiveQuotaObserved = hasPassiveAccountQuota(route.providerName)
458
+ && route.provider.authMode === "oauth";
459
+ const noteInspectedPayload = (payload: unknown) => {
460
+ // First terminal stays authoritative even in metadata-only inspection, which
461
+ // intentionally continues parsing after a failed/incomplete terminal.
462
+ const terminal = terminalStatusFromParsed(payload);
463
+ if (inspectedTerminal === null && terminal !== null) {
464
+ inspectedTerminal = terminal;
465
+ // The client boundary accepts a terminal by event type, even without a
466
+ // response object. Such a terminal must permanently decline recall.
467
+ if (terminal === "completed" && payload && typeof payload === "object"
468
+ && "response" in payload && payload.response && typeof payload.response === "object"
469
+ && !Array.isArray(payload.response) && "model" in payload.response) {
470
+ firstTerminalAllowsRecall = typeof payload.response.model === "string"
471
+ && payload.response.model.trim().length > 0;
472
+ }
473
+ }
474
+ // Meta reports subscription usage ONLY as an in-stream event; there is no endpoint
475
+ // to poll (003 §E probed 17 paths, all 404). Observed here rather than behind a
476
+ // dedicated inspector handler because onParsedPayload already reaches every
477
+ // passthrough shape -- eager relay and both tee consumers -- through this one
478
+ // function.
479
+ //
480
+ // Placed BEFORE the undeclared-tool early return below, which is load-bearing: that
481
+ // guard latches for the rest of the turn once it fires, and a turn that tripped it
482
+ // still legitimately reports usage.
483
+ if (passiveQuotaObserved && isMuseSubscriptionUsagePayload(payload)) {
484
+ const quota = parseMuseSubscriptionUsage(payload);
485
+ // Read at EVENT time, not at handler construction: failover rebinds this, and the
486
+ // quota belongs to the account that actually served the turn.
487
+ const servingAccountId = transportState.genericFailoverAccountId;
488
+ if (quota && servingAccountId) {
489
+ recordPassiveAccountQuota(route.providerName, servingAccountId, quota, passiveQuotaWriterGeneration);
490
+ }
491
+ }
492
+ // Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth
493
+ // provider) every name looks undeclared, and flipping this would stop recording continuation
494
+ // state for exactly the passthrough traffic the guard deliberately stands down for.
495
+ if (undeclaredToolGuardActive && !inspectionSawUndeclaredTool && undeclaredToolCallName(
496
+ restoreAuthorizedBareNamespaceToolCalls(
497
+ restoreMuseToolNames(payload, responseEffects.routedMuseToolNameAliases).value,
498
+ ),
499
+ declaredWireToolNames,
500
+ declaredNamelessClientCallTypes,
501
+ providerExecutedCallTypes,
502
+ declaredBareWireToolNames,
503
+ ) !== undefined) {
504
+ inspectionSawUndeclaredTool = true;
505
+ }
506
+ // The snapshot callback opts the inspector into output reconstruction. Compaction
507
+ // has no continuation cache, so use the parsed terminal here without adding retention.
508
+ if (responseEffects.plaintextV2AgentMessageToolNames.size === 0 && !rememberPassthroughResponse && payload && typeof payload === "object"
509
+ && "type" in payload && payload.type === "response.completed"
510
+ && "response" in payload && payload.response && typeof payload.response === "object"
511
+ && !Array.isArray(payload.response)) {
512
+ rememberPassthroughResponseChecked(payload.response as Record<string, unknown>);
513
+ }
514
+ };
515
+ const rememberPassthroughResponseChecked = (
516
+ response: { id?: unknown; output?: unknown; status?: unknown; model?: unknown },
517
+ ) => {
518
+ if (inspectionSawUndeclaredTool) return;
519
+ const restored = restoreRoutedCustomCalls(
520
+ restoreAuthorizedBareNamespaceToolCalls(
521
+ restoreRoutedNamespaceCalls(
522
+ restoreMuseToolNames(response, responseEffects.routedMuseToolNameAliases).value,
523
+ responseEffects.routedNamespaceToolAliases,
524
+ ).value,
525
+ ),
526
+ routedCustomToolNames,
527
+ routedCustomToolRepairNames,
528
+ declaredWireToolNames,
529
+ ).value;
530
+ const normalizedResponse = (functionRepairSchemas.size > 0
531
+ ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored)))
532
+ : restored) as { id?: unknown; output?: unknown; status?: unknown };
533
+ const plaintextRestore = restorePlaintextV2AgentMessageCalls(
534
+ normalizedResponse, responseEffects.plaintextV2AgentMessageToolNames, responseEffects.plaintextV2AgentMessageAliasedToolNames,
535
+ );
536
+ if (plaintextRestore.overflowed) return;
537
+ const restoredResponse = plaintextRestore.value as typeof normalizedResponse;
538
+ // Replay overlap compares the items the client echoes, including visible reasoning shape.
539
+ const replayResponse = restoredResponse;
540
+ if (
541
+ undeclaredToolGuardActive
542
+ && undeclaredToolCallNameInResponse(
543
+ restoredResponse,
544
+ declaredWireToolNames,
545
+ declaredNamelessClientCallTypes,
546
+ providerExecutedCallTypes,
547
+ declaredBareWireToolNames,
548
+ ) !== undefined
549
+ ) {
550
+ return;
551
+ }
552
+ const normalizedReplayResponse = (undeclaredToolGuardActive
553
+ ? normalizeDefaultNamespaceInResponse(
554
+ replayResponse,
555
+ declaredWireToolNames,
556
+ declaredBareWireToolNames,
557
+ ).value
558
+ : replayResponse) as typeof replayResponse;
559
+ rememberPassthroughResponse?.(normalizedReplayResponse);
560
+ const firstCompletion = !inspectedCompletionSeen;
561
+ inspectedCompletionSeen = true;
562
+ if (firstCompletion && (inspectedTerminal === null || firstTerminalAllowsRecall)) {
563
+ // A model-less first completion permanently declines recall; later terminal
564
+ // frames are hidden by the client boundary and cannot supply its identity.
565
+ // Native inspection sees the pre-rewrite model. Only an actual terminal
566
+ // model can seed recall; an absent model never falls back to the pick.
567
+ if (typeof response.model === "string" && response.model.trim()) {
568
+ notifyResponseComplete({
569
+ status: response.status,
570
+ model: parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
571
+ ? parsed._responseModelId : response.model,
572
+ });
573
+ }
574
+ }
575
+ };
576
+ recordAdapterReasoning(logCtx, request);
577
+ recordAdapterTier(logCtx, request);
578
+ const actualHostKey = upstreamHostHealthKey(
579
+ route.providerName,
580
+ safeOriginLabel(request.url),
581
+ );
582
+ const hostKey = route.provider.authMode === "forward"
583
+ ? actualHostKey
584
+ : null;
585
+ const hostCircuitEnabled = hostKey !== null
586
+ && normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0;
587
+ if (hostKey !== null && !hostCircuitEnabled) {
588
+ disableUpstreamHostCircuitForKey(actualHostKey);
589
+ }
590
+ if (nativeHostState.lease && nativeHostState.lease.key !== hostKey) {
591
+ return formatErrorResponse(502, "upstream_error", "Provider host changed after circuit admission");
592
+ }
593
+ if (options.abortSignal?.aborted) {
594
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
595
+ return clientCancelledResponse();
596
+ }
597
+ if (!nativeHostState.lease && hostCircuitEnabled) {
598
+ const admission = acquireUpstreamHostAdmission(
599
+ hostKey!,
600
+ config.upstreamHostCircuitThreshold,
601
+ );
602
+ if (admission.kind === "blocked") {
603
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
604
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
605
+ }
606
+ nativeHostState.lease = admission.lease;
607
+ }
608
+ const settleObservedHostResponse = (): void => {
609
+ if (hostCircuitEnabled) {
610
+ resetUpstreamHostHealth(actualHostKey, nativeHostState.lease);
611
+ } else {
612
+ resetUpstreamHostHealth(actualHostKey);
613
+ }
614
+ nativeHostState.lease = null;
615
+ };
616
+ /**
617
+ * #4191: a Codex WS exchange pins its content-free stage record on the
618
+ * Response it resolves (markCodexWsStage). Adopting the record here, at
619
+ * the single funnel every physical upstream response passes through,
620
+ * binds it to the attempt that actually served it — including the 502/504
621
+ * pre-response JSON settles that never reach the SSE relay.
622
+ */
623
+ const adoptCodexWsStage = (response: Response): void => {
624
+ const stage = readCodexWsStage(response);
625
+ if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
626
+ };
627
+ const adoptObservedResponse = <T extends Response>(response: T): T => {
628
+ settleObservedHostResponse();
629
+ adoptCodexWsStage(response);
630
+ return response;
631
+ };
632
+ let passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
633
+ ? request.usageLog.inputTokens
634
+ : undefined;
635
+ if (passthroughEstimate !== undefined) {
636
+ logCtx.usageLogInputTokens = passthroughEstimate;
637
+ }
638
+ // Abort the upstream if the client disconnects. A directly-relayed body does not propagate the
639
+ // consumer's cancel to a signalled fetch, so we pass the signal and relay through relayWithAbort,
640
+ // whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
641
+ const upstream = new AbortController();
642
+ linkAbortSignal(upstream, options.abortSignal);
643
+ const connectMs = config.connectTimeoutMs ?? 200_000;
644
+ let upstreamResponse: Response;
645
+ /**
646
+ * Refuse a built body that exceeds the operator's configured ceiling, before it is sent.
647
+ *
648
+ * Unconfigured this measures nothing and returns undefined, so an unset proxy behaves
649
+ * exactly as it does today. Runs at every point a body is built or rebuilt, because a
650
+ * rebuild can produce a payload the initial check never saw.
651
+ */
652
+ const refuseOversizedOutboundBody = (
653
+ builtRequest: AdapterRequest,
654
+ refusalAuthCtx: CodexAuthContext = admissionState.authCtx,
655
+ ): Response | undefined => {
656
+ const result = checkOutboundBodySize(builtRequest.body, config.maxUpstreamBodyBytes);
657
+ if (result.admitted) return undefined;
658
+
659
+ // This returns before the surrounding fetch/finally owns the observation, so release
660
+ // it here or one refused body holds translator budget for the process lifetime.
661
+ builtRequest.releaseBodyObservation?.();
662
+ upstream.abort();
663
+ releaseUpstreamHostAdmission(nativeHostState.lease);
664
+ nativeHostState.lease = null;
665
+ releaseCodexAuthContextProbeLease(refusalAuthCtx);
666
+ logCtx.errorCode = "outbound_body_too_large";
667
+ console.warn(
668
+ `[responses] refused an oversized outbound body: bytes=${result.bytes} limit=${result.limit} `
669
+ + `input_images=${result.imageCount} image_bytes=${result.imageBytes} `
670
+ + `model=${JSON.stringify(parsed.modelId)}`,
671
+ );
672
+ // A streaming client treats HTTP 413 as a retryable transport error and resends the same
673
+ // oversized body — the reconnect loop #3177 exists to stop. Terminal overflow is the
674
+ // honest shape, and it is what the upstream-413 path already returns.
675
+ if (clientRequestedStream) {
676
+ return streamingContextOverflowResponse(
677
+ parsed._responseModelId ?? parsed.modelId,
678
+ translatorBudget,
679
+ );
680
+ }
681
+ return formatErrorResponse(
682
+ 413,
683
+ "outbound_body_too_large",
684
+ describeOutboundBodyRefusal(result),
685
+ );
686
+ };
687
+ const transportFailureResponse = (err: unknown): Response => {
688
+ upstream.abort();
689
+ if (options.abortSignal?.aborted) {
690
+ releaseUpstreamHostAdmission(nativeHostState.lease);
691
+ nativeHostState.lease = null;
692
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
693
+ return clientCancelledResponse();
694
+ }
695
+ // A budget refusal is a proxy decision, not an upstream fault. Reporting it as
696
+ // 502 upstream_error would blame the provider for a limit this process applied, and
697
+ // would record a fake reachability failure against the account's health.
698
+ if (err instanceof SendBudgetExhaustedError) {
699
+ releaseUpstreamHostAdmission(nativeHostState.lease);
700
+ nativeHostState.lease = null;
701
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
702
+ return formatErrorResponse(429, "request_send_budget_exhausted", err.message);
703
+ }
704
+ const localRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
705
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
706
+ });
707
+ if (localRefusal) {
708
+ releaseUpstreamHostAdmission(nativeHostState.lease);
709
+ nativeHostState.lease = null;
710
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
711
+ return localRefusal;
712
+ }
713
+ const outcome = classifyTransportFailureKind(err);
714
+ // Host-level evidence stands regardless of pool membership: a direct
715
+ // forward send has no pool accounting, but the reachability failure is
716
+ // still host-wide, not account evidence (#914 review).
717
+ if (outcome === "connect_neutral") {
718
+ if (hostCircuitEnabled) {
719
+ recordUpstreamHostFailure(actualHostKey, {
720
+ code: transportErrorCode(err),
721
+ threshold: config.upstreamHostCircuitThreshold,
722
+ lease: nativeHostState.lease,
723
+ });
724
+ } else {
725
+ recordUpstreamHostFailure(actualHostKey, { code: transportErrorCode(err) });
726
+ }
727
+ nativeHostState.lease = null;
728
+ } else {
729
+ releaseUpstreamHostAdmission(nativeHostState.lease);
730
+ nativeHostState.lease = null;
731
+ }
732
+ if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
733
+ recordCodexUpstreamOutcome(config, admissionState.authCtx.accountId, outcome, {
734
+ threadId: admissionState.authCtx.affinityKey,
735
+ fixedAccount: admissionState.authCtx.fixedAccount,
736
+ modelId: route.modelId,
737
+ probeLeaseId: codexProbeLeaseId(admissionState.authCtx),
738
+ probeQuotaScope: codexProbeQuotaScope(admissionState.authCtx),
739
+ writerGeneration: admissionState.authCtx.writerGeneration,
740
+ });
741
+ }
742
+ const msg = outcome === "timeout"
743
+ ? `Provider connect timeout after ${connectMs}ms`
744
+ : describeUpstreamConnectFailure(err, connectMs);
745
+ return formatErrorResponse(502, "upstream_error", msg);
746
+ };
747
+ const initialBodyRefusal = refuseOversizedOutboundBody(request);
748
+ if (initialBodyRefusal) return initialBodyRefusal;
749
+ try {
750
+ // Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
751
+ // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
752
+ // Body is a replayable string; nothing has streamed to the client yet.
753
+ upstreamResponse = await fetchWithTransientRetry(
754
+ recovery => {
755
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery);
756
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
757
+ method: request.method,
758
+ headers: request.headers,
759
+ body: request.body,
760
+ }, recovery), upstream.signal, connectMs, parsed.stream,
761
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
762
+ dispatchOverride: oauthDispatch(request),
763
+ providerName: route.providerName,
764
+ modelId: route.modelId,
765
+ onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
766
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
767
+ ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
768
+ }),
769
+ route.provider.authMode === "forward")
770
+ // Every real attempt response — including an intermediate 5xx the
771
+ // retry wrapper replaces — proves the host was reached (#914 review).
772
+ .then(adoptObservedResponse);
773
+ },
774
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(TRANSIENT_RETRY_MAX_ATTEMPTS), onSendsConsumed: noteTransientSends },
775
+ );
776
+ } catch (err) {
777
+ return transportFailureResponse(err);
778
+ } finally {
779
+ request.releaseBodyObservation?.();
780
+ }
781
+
782
+ const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
783
+ // At most one reasoning-effort downgrade per request.
784
+ const reasoningEffortDowngradeGuard: { attempted: boolean } = { attempted: false };
785
+ let oauth401ReplayAttempted = false;
786
+ let codex401ReplayKind: "main" | "stored" | null = null;
787
+ // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
788
+ // moments later; at most one byte-identical replay is allowed per request.
789
+ const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
790
+ const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
791
+ let rateLimitRetries = 0;
792
+ const rebuildAndRefetch = async (
793
+ recovery: AttemptRecoveryKind,
794
+ ): Promise<Response | { failed: Response }> => {
795
+ const retryAdapter = resolveSelectionAdapter(
796
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
797
+ config.cacheRetention,
798
+ );
799
+ if (!("passthrough" in retryAdapter) || !retryAdapter.passthrough) {
800
+ upstream.abort();
801
+ return { failed: formatErrorResponse(502, "upstream_error", "Recovery changed the provider wire unexpectedly") };
802
+ }
803
+ try {
804
+ if (recovery !== "console-go-upload-retry") {
805
+ request = await retryAdapter.buildRequest(parsed, {
806
+ headers: requestState.selectedForwardHeaders,
807
+ translatorBudget,
808
+ });
809
+ }
810
+ refreshRequestToolAliases(request);
811
+ recordAdapterReasoning(logCtx, request);
812
+ recordAdapterTier(logCtx, request);
813
+ } catch (err) {
814
+ upstream.abort();
815
+ if (options.abortSignal?.aborted) return { failed: clientCancelledResponse() };
816
+ const msg = err instanceof Error ? err.message : String(err);
817
+ return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(msg)) };
818
+ }
819
+ passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
820
+ ? request.usageLog.inputTokens
821
+ : undefined;
822
+ if (passthroughEstimate !== undefined) logCtx.usageLogInputTokens = passthroughEstimate;
823
+ refreshUndeclaredToolGuard(request);
824
+ logCtx.providerAdapter = retryAdapter.name;
825
+ sealRequestAttemptIdentity(
826
+ logCtx.activeAttempt,
827
+ logCtx.provider,
828
+ retryAdapter.name,
829
+ logCtx.accountLogLabel,
830
+ );
831
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name);
832
+ const rebuiltBodyRefusal = refuseOversizedOutboundBody(request);
833
+ if (rebuiltBodyRefusal) return { failed: rebuiltBodyRefusal };
834
+ // The base allowance is spent first; once it is gone this leg may still draw the one
835
+ // shared final-recovery reserve, which is what keeps a validated sanitized rebuild
836
+ // after a 5xx streak alive at four total sends instead of dying at three. Reserved
837
+ // outside the try so the finally can hand it back if the leg never reached its send.
838
+ const allowance = recoverySendAllowance(
839
+ TRANSIENT_RETRY_MAX_ATTEMPTS,
840
+ recoveryClassFor(recovery),
841
+ `${route.providerName}|${route.modelId}|${recovery}`,
842
+ );
843
+ try {
844
+ return await fetchWithTransientRetry(
845
+ innerRecovery => {
846
+ // Gated on the return, not fire-and-forget: a consumed permit means this leg
847
+ // already sent once, and letting the second call through would be a free send.
848
+ if (allowance.permit && !allowance.permit.use()) {
849
+ throw new SendBudgetExhaustedError(safeHostLabel(request.url));
850
+ }
851
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, innerRecovery ?? recovery);
852
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
853
+ method: request.method,
854
+ headers: request.headers,
855
+ body: request.body,
856
+ }, innerRecovery), upstream.signal, connectMs, parsed.stream,
857
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
858
+ dispatchOverride: oauthDispatch(request),
859
+ providerName: route.providerName,
860
+ modelId: route.modelId,
861
+ onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
862
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
863
+ ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
864
+ }),
865
+ route.provider.authMode === "forward")
866
+ .then(adoptObservedResponse);
867
+ },
868
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: allowance.attempts, onSendsConsumed: noteTransientSends },
869
+ );
870
+ } catch (err) {
871
+ return { failed: transportFailureResponse(err) };
872
+ } finally {
873
+ // A no-op once the permit was used or once onSendsConsumed settled it; it only refunds
874
+ // a reservation whose send never happened.
875
+ allowance.permit?.release();
876
+ request.releaseBodyObservation?.();
877
+ }
878
+ };
879
+
880
+ // Keep recovery kinds in sync with the generic `recovery:` loop below.
881
+ passthroughRecovery: for (;;) {
882
+
883
+ if (
884
+ upstreamResponse.status === 401
885
+ && (admissionState.authCtx.kind === "main-pool" || admissionState.authCtx.kind === "pool")
886
+ && usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)
887
+ && codex401ReplayKind === null
888
+ ) {
889
+ codex401ReplayKind = admissionState.authCtx.kind === "pool" ? "stored" : "main";
890
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
891
+ const poolAuthCtx = admissionState.authCtx.kind === "pool" ? admissionState.authCtx : undefined;
892
+ const poolReplay = poolAuthCtx
893
+ ? await refreshPoolForwardAuth({ req, config, route, authCtx: poolAuthCtx, substituteMainCredential, options, logCtx })
894
+ : undefined;
895
+ const replay = poolReplay
896
+ ?? await refreshNativeMainForwardAuth({ req, config, route, authCtx: admissionState.authCtx, substituteMainCredential, options });
897
+ if (!replay.ok) {
898
+ // Compact already records this; core historically returned without recording,
899
+ // so a dead grant stayed selectable and every request repeated the same doomed
900
+ // refresh. Fenced by the generation the 401 belongs to (#2887).
901
+ if (poolAuthCtx && poolReplay && !poolReplay.ok && poolReplay.quarantine) {
902
+ recordCodexUpstreamOutcome(config, poolAuthCtx.accountId, 401, {
903
+ threadId: poolAuthCtx.affinityKey,
904
+ fixedAccount: poolAuthCtx.fixedAccount,
905
+ modelId: route.modelId,
906
+ writerGeneration: poolAuthCtx.writerGeneration,
907
+ credentialGeneration: poolReplay.quarantineGeneration ?? poolAuthCtx.generation,
908
+ });
909
+ }
910
+ upstream.abort();
911
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
912
+ return replay.response;
913
+ }
914
+ admissionState.authCtx = replay.authCtx;
915
+ route.provider = replay.provider;
916
+ requestState.selectedForwardHeaders = withClaudeNativeSession(replay.headers, replay.provider, options.claudeNativeSessionId);
917
+ const replayAdapter = resolveSelectionAdapter(
918
+ resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
919
+ config.cacheRetention,
920
+ );
921
+ if (!("passthrough" in replayAdapter) || !replayAdapter.passthrough) {
922
+ upstream.abort();
923
+ return formatErrorResponse(502, "upstream_error", "Native main refresh changed the provider wire unexpectedly");
924
+ }
925
+ bindRouteReasoningReplayScope({
926
+ parsed,
927
+ providerName: route.providerName,
928
+ provider: replay.provider,
929
+ adapterName: replayAdapter.name,
930
+ codexAuthContext: admissionState.authCtx,
931
+ forwardHeaders: requestState.selectedForwardHeaders,
932
+ });
933
+ logCtx.providerAdapter = replayAdapter.name;
934
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, replayAdapter.name, logCtx.accountLogLabel);
935
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, replayAdapter.name);
936
+ try {
937
+ request = await replayAdapter.buildRequest(parsed, {
938
+ headers: requestState.selectedForwardHeaders,
939
+ translatorBudget,
940
+ });
941
+ refreshRequestToolAliases(request);
942
+ recordAdapterReasoning(logCtx, request);
943
+ recordAdapterTier(logCtx, request);
944
+ refreshUndeclaredToolGuard(request);
945
+ // The 401 replay rebuilds the body before sending, so it needs the same ceiling as
946
+ // every other build site; a replay is exactly when a grown payload reappears.
947
+ const replayBodyRefusal = refuseOversizedOutboundBody(request);
948
+ if (replayBodyRefusal) return replayBodyRefusal;
949
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, "oauth-401");
950
+ upstreamResponse = await fetchWithHeaderTimeout(
951
+ request.url,
952
+ { method: request.method, headers: request.headers, body: request.body },
953
+ upstream.signal,
954
+ connectMs,
955
+ parsed.stream,
956
+ // The replay-dispatched signal is what bounds the rest of this logical request, so it
957
+ // has to describe a send that actually happened. fetchWithHeaderTimeout awaits pacing
958
+ // admission BEFORE calling the executor, so signalling at the call site would spend the
959
+ // budget even when a rejected pacing wait means nothing reaches the network. Wrapping
960
+ // the executor moves the signal to the last moment before the send, where a throw from
961
+ // here on is a genuine transport attempt.
962
+ storedPoolReplayDispatchNotifier(
963
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
964
+ dispatchOverride: oauthDispatch(request),
965
+ providerName: route.providerName,
966
+ modelId: route.modelId,
967
+ onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
968
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
969
+ ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
970
+ }),
971
+ codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
972
+ ),
973
+ route.provider.authMode === "forward",
974
+ ).then(adoptObservedResponse);
975
+ } catch (err) {
976
+ return transportFailureResponse(err);
977
+ } finally {
978
+ request.releaseBodyObservation?.();
979
+ }
980
+ continue passthroughRecovery;
981
+ }
982
+
983
+ if (codex401ReplayKind !== null && upstreamResponse.status === 401) break;
984
+
985
+ // Native Responses providers return before the generic adapter recovery loop below. Keep
986
+ // their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
987
+ // rebuilt replay. xAI's current subscription models use this branch now that their official
988
+ // Grok CLI catalog declares the Responses backend.
989
+ if (
990
+ upstreamResponse.status === 401
991
+ && isOAuth401ReplayProvider
992
+ && transportState.sentOAuthSnapshot
993
+ && !oauth401ReplayAttempted
994
+ // Refused here, before the 401 body is cancelled: once it is gone the request can only
995
+ // answer with a synthetic 502, which would report a proxy budget decision as an upstream
996
+ // fault and throw away the credential evidence the client needs.
997
+ && !sendBudgetExhausted()
998
+ ) {
999
+ oauth401ReplayAttempted = true;
1000
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1001
+ let refreshed: OAuthAccessSnapshot;
1002
+ try {
1003
+ refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
1004
+ } catch (err) {
1005
+ upstream.abort();
1006
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
1007
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
1008
+ }
1009
+ if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
1010
+ upstream.abort();
1011
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
1012
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist project is required")));
1013
+ }
1014
+ transportState.sentOAuthSnapshot = refreshed;
1015
+ transportState.replayOAuthCredentialSnapshot = {
1016
+ accountId: refreshed.accountId,
1017
+ generation: refreshed.generation,
1018
+ };
1019
+ if (route.providerName === "kiro") {
1020
+ parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
1021
+ }
1022
+ const refreshedProvider = resolveProviderTransport(
1023
+ route.providerName,
1024
+ {
1025
+ ...route.provider,
1026
+ apiKey: refreshed.accessToken,
1027
+ ...(refreshed.projectId ? { project: refreshed.projectId } : {}),
1028
+ },
1029
+ parsed.options.promptCacheKey,
1030
+ route.providerName === "github-copilot"
1031
+ ? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
1032
+ : undefined,
1033
+ );
1034
+ route.provider = refreshedProvider;
1035
+ const refreshedAdapter = resolveSelectionAdapter(
1036
+ resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
1037
+ config.cacheRetention,
1038
+ );
1039
+ if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
1040
+ upstream.abort();
1041
+ return formatErrorResponse(502, "upstream_error", "OAuth refresh changed the provider wire unexpectedly");
1042
+ }
1043
+ bindRouteReasoningReplayScope({
1044
+ parsed,
1045
+ providerName: route.providerName,
1046
+ provider: refreshedProvider,
1047
+ adapterName: refreshedAdapter.name,
1048
+ oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1049
+ });
1050
+ logCtx.providerAdapter = refreshedAdapter.name;
1051
+ sealRequestAttemptIdentity(
1052
+ logCtx.activeAttempt,
1053
+ logCtx.provider,
1054
+ refreshedAdapter.name,
1055
+ logCtx.accountLogLabel,
1056
+ );
1057
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, refreshedAdapter.name);
1058
+ try {
1059
+ request = await refreshedAdapter.buildRequest(parsed, {
1060
+ headers: requestState.selectedForwardHeaders,
1061
+ translatorBudget,
1062
+ });
1063
+ refreshRequestToolAliases(request);
1064
+ recordAdapterReasoning(logCtx, request);
1065
+ recordAdapterTier(logCtx, request);
1066
+ } catch (err) {
1067
+ upstream.abort();
1068
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
1069
+ const msg = err instanceof Error ? err.message : String(err);
1070
+ return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
1071
+ }
1072
+ refreshUndeclaredToolGuard(request);
1073
+ const refreshedBodyRefusal = refuseOversizedOutboundBody(request);
1074
+ if (refreshedBodyRefusal) return refreshedBodyRefusal;
1075
+ try {
1076
+ upstreamResponse = await fetchWithTransientRetry(
1077
+ recovery => {
1078
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "oauth-401");
1079
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
1080
+ method: request.method,
1081
+ headers: request.headers,
1082
+ body: request.body,
1083
+ }, recovery), upstream.signal, connectMs, parsed.stream,
1084
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1085
+ dispatchOverride: oauthDispatch(request),
1086
+ providerName: route.providerName,
1087
+ modelId: route.modelId,
1088
+ onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1089
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1090
+ ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1091
+ }),
1092
+ route.provider.authMode === "forward")
1093
+ .then(adoptObservedResponse);
1094
+ },
1095
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(TRANSIENT_RETRY_MAX_ATTEMPTS), onSendsConsumed: noteTransientSends },
1096
+ );
1097
+ } catch (err) {
1098
+ return transportFailureResponse(err);
1099
+ } finally {
1100
+ request.releaseBodyObservation?.();
1101
+ }
1102
+ }
1103
+
1104
+ // Native Responses returns before the generic adapter's OAuth rotation loop. Keep
1105
+ // the same quorum, cooldown and request budget here, before any client bytes flow.
1106
+ if (
1107
+ upstreamResponse.status === 429
1108
+ && transportState.genericFailoverAccountId
1109
+ && transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
1110
+ && isGenericOAuthFailoverEnabled(config, route.providerName)
1111
+ ) {
1112
+ // The roster cap above is one half of the bound; the request's shared budget is the
1113
+ // other. A refused hop leaves the real 429 -- body, Retry-After and any quota evidence
1114
+ // -- exactly as upstream sent it.
1115
+ const hop = reserveCredentialHop(
1116
+ "auth-recovery",
1117
+ `${route.providerName}|${route.modelId}|oauth-account-429`,
1118
+ true,
1119
+ );
1120
+ if (hop.allowed) {
1121
+ const nextAccountId = rotateGenericOAuthAccountOn429(
1122
+ config, route.providerName, transportState.genericFailoverAccountId,
1123
+ upstreamResponse.headers.get("retry-after"),
1124
+ );
1125
+ let snapshot: OAuthAccessSnapshot | undefined;
1126
+ if (nextAccountId) {
1127
+ try { snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); }
1128
+ catch { /* Keep the original 429 body readable when the next credential is unavailable. */ }
1129
+ }
1130
+ if (snapshot && await applyFailoverSnapshot(snapshot)) {
1131
+ transportState.genericFailovers += 1;
1132
+ route.provider = resolveProviderTransport(
1133
+ route.providerName, route.provider, parsed.options.promptCacheKey, transportState.sentOAuthSnapshot?.apiBaseUrl,
1134
+ );
1135
+ bindRouteReasoningReplayScope({
1136
+ parsed, providerName: route.providerName, provider: route.provider,
1137
+ adapterName: "openai-responses", oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1138
+ });
1139
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
1140
+ // The replay IS this hop's send, so the rebuild spends the reservation instead of
1141
+ // asking for one of its own.
1142
+ sendBudgetState.pendingHopPermit = hop.permit;
1143
+ const result = await rebuildAndRefetch("oauth-account-429");
1144
+ sendBudgetState.pendingHopPermit = undefined;
1145
+ if ("failed" in result) return result.failed;
1146
+ upstreamResponse = result;
1147
+ continue passthroughRecovery;
1148
+ }
1149
+ // No credential moved, so the reservation costs nothing.
1150
+ hop.permit?.release();
1151
+ }
1152
+ }
1153
+
1154
+ // Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
1155
+ // passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
1156
+ // key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
1157
+ // immediately with no same-key replay. Pre-stream only — nothing has been relayed yet, so
1158
+ // the replay is lossless (same invariant as the recovery loop). Forward/OAuth providers
1159
+ // keep their pool logic below (rateLimitRetryPolicyFor returns null for them).
1160
+ while (
1161
+ upstreamResponse.status === 429
1162
+ && rateLimitPolicy !== null
1163
+ && rateLimitRetries < rateLimitPolicy.attempts
1164
+ // Checked here rather than inside the helper: prepareSameTarget429Wait releases the 429
1165
+ // body, so a refusal discovered after the wait can no longer return the real rate-limit
1166
+ // answer and would surface a synthetic 502 instead.
1167
+ && !sendBudgetExhausted()
1168
+ ) {
1169
+ rateLimitRetries += 1;
1170
+ // Release unread body + deliberate wait via the shared same-target helper.
1171
+ const retryAfterHeader = upstreamResponse.headers.get("retry-after");
1172
+ try {
1173
+ for await (const _ of prepareSameTarget429Wait({
1174
+ body: upstreamResponse.body,
1175
+ signal: options.abortSignal,
1176
+ delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
1177
+ })) {
1178
+ // pre-stream: no stall watchdog to feed
1179
+ }
1180
+ } catch {
1181
+ upstream.abort();
1182
+ return clientCancelledResponse();
1183
+ }
1184
+ // Client cancellation wins over any stale timer edge: re-check before dispatching the
1185
+ // replay so the wire never starts work for a request the client already abandoned.
1186
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
1187
+ upstream.abort();
1188
+ return clientCancelledResponse();
1189
+ }
1190
+ try {
1191
+ upstreamResponse = await fetchWithTransientRetry(
1192
+ recovery => {
1193
+ // The first send of every replay is itself a rate-limit retry; inner transient-5xx
1194
+ // recoveries keep their own label (recovery is provided for those).
1195
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "rate-limit-429");
1196
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
1197
+ method: request.method,
1198
+ headers: request.headers,
1199
+ body: request.body,
1200
+ }, recovery), upstream.signal, connectMs, parsed.stream,
1201
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1202
+ dispatchOverride: oauthDispatch(request),
1203
+ providerName: route.providerName,
1204
+ modelId: route.modelId,
1205
+ onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1206
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1207
+ ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1208
+ }),
1209
+ route.provider.authMode === "forward")
1210
+ .then(adoptObservedResponse);
1211
+ },
1212
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(TRANSIENT_RETRY_MAX_ATTEMPTS), onSendsConsumed: noteTransientSends },
1213
+ );
1214
+ } catch (err) {
1215
+ return transportFailureResponse(err);
1216
+ }
1217
+ }
1218
+
1219
+ const captureAffinityResponse = (
1220
+ response: Response,
1221
+ captureAuthCtx: CodexAuthContext = admissionState.authCtx,
1222
+ captureRequest: Awaited<ReturnType<typeof transportState.adapter.buildRequest>> = request,
1223
+ credentialSubstituted = substituteMainCredential
1224
+ || captureAuthCtx.kind === "pool"
1225
+ || captureAuthCtx.kind === "main-pool",
1226
+ ): void => {
1227
+ if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
1228
+ captureCodexAffinityDiagnostic({
1229
+ inboundHeaders: req.headers,
1230
+ outboundHeaders: captureRequest.headers,
1231
+ authKind: captureAuthCtx.kind,
1232
+ accountMode: route.codexAccountMode,
1233
+ fixedAccount: isFixedCodexAccount(captureAuthCtx),
1234
+ credentialSubstituted,
1235
+ accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
1236
+ wireModelNormalized: parsed.modelId !== route.modelId,
1237
+ status: response.status,
1238
+ });
1239
+ };
1240
+ captureAffinityResponse(upstreamResponse);
1241
+
1242
+ if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
1243
+ let poolRetryOutcome: number | undefined;
1244
+ if (await shouldRetryCodexPoolAccountModel400(
1245
+ upstreamResponse,
1246
+ route.modelId,
1247
+ options.abortSignal,
1248
+ )) {
1249
+ poolRetryOutcome = 400;
1250
+ } else if (!admissionState.authCtx.fixedAccount && await shouldRetryCodexPoolAccountQuota(
1251
+ upstreamResponse,
1252
+ options.abortSignal,
1253
+ )) {
1254
+ // Pre-stream only: once SSE has begun, mid-stream quota stays terminal.
1255
+ // ChatGPT sometimes wraps quota exhaustion in a generic 5xx. Normalize only
1256
+ // body-confirmed cases to quota evidence so cooldown and rotation both apply.
1257
+ poolRetryOutcome = upstreamResponse.status >= 500 ? 429 : upstreamResponse.status;
1258
+ } else if (!admissionState.authCtx.fixedAccount && shouldRetryCodexPoolAccountTransient(upstreamResponse)) {
1259
+ // A plain transient 5xx the same-account retry layer could not absorb. Keep the real
1260
+ // status so it records as transient rather than quota.
1261
+ poolRetryOutcome = upstreamResponse.status;
1262
+ }
1263
+
1264
+ if (poolRetryOutcome !== undefined) {
1265
+ // A stored Pool 401 spent this request's account budget on its own refresh and replay, so
1266
+ // nothing afterwards may be paid for out of a DIFFERENT account. One flag carries that,
1267
+ // rather than a status check here as well: a quota failure has no same-account move, so
1268
+ // `sameAccountOnly` makes it terminal by refusing the alternate; the gated-model 400
1269
+ // ladder does have one — retrying the account the refreshed roster still grants — and
1270
+ // keeps it. An earlier revision also broke here on a non-400 outcome, which no test could
1271
+ // justify because this flag already produced the identical result.
1272
+ const storedReplaySpent = codex401ReplayKind === "stored";
1273
+ const retry = await retryCodexPoolOnAlternateAccount({
1274
+ callerAuthHeaders,
1275
+ config,
1276
+ route,
1277
+ parsed,
1278
+ logCtx,
1279
+ options: { ...options, workflowRootId },
1280
+ firstAuthCtx: admissionState.authCtx,
1281
+ firstResponse: upstreamResponse,
1282
+ outcomeStatus: poolRetryOutcome,
1283
+ sameAccountOnly: storedReplaySpent,
1284
+ upstream,
1285
+ connectMs,
1286
+ passthroughEstimate,
1287
+ stream: parsed.stream,
1288
+ onResponse: (response, retryAuthCtx, retryRequest) => {
1289
+ adoptCodexWsStage(response);
1290
+ captureAffinityResponse(
1291
+ response,
1292
+ retryAuthCtx,
1293
+ retryRequest,
1294
+ retryAuthCtx.kind !== "main",
1295
+ );
1296
+ },
1297
+ });
1298
+ if (retry.kind === "transport") {
1299
+ admissionState.authCtx = retry.authCtx;
1300
+ return transportFailureResponse(retry.error);
1301
+ }
1302
+ if (retry.kind === "retried") {
1303
+ admissionState.authCtx = retry.authCtx;
1304
+ request = retry.request;
1305
+ refreshRequestToolAliases(request);
1306
+ refreshUndeclaredToolGuard(request);
1307
+ upstreamResponse = retry.upstreamResponse;
1308
+ requestState.selectedForwardHeaders = retry.selectedForwardHeaders;
1309
+ // Keep subagent quota-failure health keyed to the account that actually served.
1310
+ requestState.subagentFallbackAccountId = retry.authCtx.accountId;
1311
+ }
1312
+ }
1313
+ }
1314
+ // The deterministic route record cannot classify history it never observed (restart, expiry,
1315
+ // eviction, or an older transcript). Inspect only a bounded clone of a 4xx whose exact outbound
1316
+ // Responses body still carries opaque state, then rebuild once through the ordinary adapter
1317
+ // sanitation path. A second rejection falls through unchanged because the guard stays armed.
1318
+ const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
1319
+ response: upstreamResponse,
1320
+ outboundBody: request.body,
1321
+ adapterName: transportState.adapter.name,
1322
+ parsed,
1323
+ guard: opaqueBlobRecoveryGuard,
1324
+ signal: upstream.signal,
1325
+ }, rebuildAndRefetch);
1326
+ if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
1327
+ if (opaqueBlobRecovery.kind === "recovered") {
1328
+ upstreamResponse = opaqueBlobRecovery.response;
1329
+ continue passthroughRecovery;
1330
+ }
1331
+
1332
+ const recoveryContentType = upstreamResponse.headers.get("content-type")?.toLowerCase() ?? "";
1333
+ const streamedFunctionOutputCandidate = upstreamResponse.ok
1334
+ && !!upstreamResponse.body
1335
+ && (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && parsed.stream))
1336
+ && !opaqueBlobRecoveryGuard.attempted
1337
+ && outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body);
1338
+ if (streamedFunctionOutputCandidate) {
1339
+ const preflightLog: RequestLogContext = { model: logCtx.model, provider: logCtx.provider };
1340
+ const preflight = await preflightComboStreamResponse(upstreamResponse, preflightLog,
1341
+ payload => {
1342
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
1343
+ const type = (payload as { type?: unknown }).type;
1344
+ return (type === "error" || type === "response.failed" || type === "response.incomplete")
1345
+ && upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
1346
+ }, {
1347
+ allowMissingContentType: !recoveryContentType && parsed.stream,
1348
+ replayReadErrors: true,
1349
+ });
1350
+ if (options.abortSignal?.aborted) return transportFailureResponse(options.abortSignal.reason);
1351
+ upstreamResponse = preflight.response;
1352
+ if (preflight.kind === "failed") {
1353
+ const streamedOpaqueRecovery = await attemptOpaqueBlobRecovery({
1354
+ response: upstreamResponse,
1355
+ outboundBody: request.body,
1356
+ adapterName: transportState.adapter.name,
1357
+ parsed,
1358
+ guard: opaqueBlobRecoveryGuard,
1359
+ signal: upstream.signal,
1360
+ }, rebuildAndRefetch);
1361
+ if (streamedOpaqueRecovery.kind === "failed") return streamedOpaqueRecovery.response;
1362
+ if (streamedOpaqueRecovery.kind === "recovered") {
1363
+ resetStreamedOpaqueBlobLogContext(logCtx);
1364
+ upstreamResponse = streamedOpaqueRecovery.response;
1365
+ continue passthroughRecovery;
1366
+ }
1367
+ logCtx.upstreamError = preflightLog.upstreamError;
1368
+ logCtx.terminalHttpStatus = preflightLog.terminalHttpStatus;
1369
+ logCtx.terminalErrorCode = preflightLog.terminalErrorCode;
1370
+ logCtx.terminalIncompleteReason = preflightLog.terminalIncompleteReason;
1371
+ }
1372
+ }
1373
+ // Console Go (opencode-zen / opencode-go) intermittently rejects a body it accepts seconds
1374
+ // later with 400 invalid_request_error / "Invalid upload request." Replay the byte-identical
1375
+ // request once after the exact gateway rejection. Single-shot guard.
1376
+ // This recovery reuses the captured request; other recovery kinds still rebuild.
1377
+ if (!consoleGoUploadRetryGuard.attempted) {
1378
+ const uploadRejectionBody = await consoleGoUploadRejectionBody(
1379
+ upstreamResponse,
1380
+ consoleGoUploadRetryGuard.attempted,
1381
+ upstream.signal,
1382
+ );
1383
+ if (uploadRejectionBody !== undefined
1384
+ && isTransientConsoleGoUploadRejection({
1385
+ status: upstreamResponse.status,
1386
+ errorBody: uploadRejectionBody,
1387
+ outboundUrl: request.url,
1388
+ })) {
1389
+ consoleGoUploadRetryGuard.attempted = true;
1390
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1391
+ if (!upstream.signal.aborted) {
1392
+ try {
1393
+ await sleepWithAbort(CONSOLE_GO_UPLOAD_RETRY_DELAY_MS, upstream.signal);
1394
+ } catch { return clientCancelledResponse(); }
1395
+ }
1396
+ if (upstream.signal.aborted) return clientCancelledResponse();
1397
+ const result = await rebuildAndRefetch("console-go-upload-retry");
1398
+ if ("failed" in result) return result.failed;
1399
+ upstreamResponse = result;
1400
+ continue passthroughRecovery;
1401
+ }
1402
+ }
1403
+ // Reasoning-effort downgrade: a rung the catalog still advertises can be refused upstream --
1404
+ // the metadata records the model's ladder, not this account's entitlement (a Muse Code
1405
+ // subscription gates max on muse-spark-1.3-contributor, for example). Learn the refusal so
1406
+ // later turns clamp before dispatch, then replay once at the next lower published rung
1407
+ // instead of failing the turn; requestedEffort/effectiveEffort keep both values in usage.
1408
+ if (!reasoningEffortDowngradeGuard.attempted) {
1409
+ const rejectionText = await reasoningEffortRejectionText(
1410
+ upstreamResponse,
1411
+ reasoningEffortDowngradeGuard.attempted,
1412
+ upstream.signal,
1413
+ );
1414
+ const downgrade = rejectionText === undefined
1415
+ ? undefined
1416
+ : planReasoningEffortDowngrade({
1417
+ provider: route.provider,
1418
+ modelId: parsed.modelId,
1419
+ requested: parsed.options.reasoning,
1420
+ rejectionText,
1421
+ });
1422
+ if (downgrade) {
1423
+ reasoningEffortDowngradeGuard.attempted = true;
1424
+ parsed.options.reasoning = downgrade.effort;
1425
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1426
+ const result = await rebuildAndRefetch("reasoning-effort-downgrade");
1427
+ if ("failed" in result) return result.failed;
1428
+ upstreamResponse = result;
1429
+ continue passthroughRecovery;
1430
+ }
1431
+ }
1432
+ break;
1433
+ }
1434
+
1435
+ return {
1436
+ codexSafetyBufferingOptions,
1437
+ imageGenCallAliases,
1438
+ routedCustomToolNames,
1439
+ routedCustomToolRepairNames,
1440
+ routedToolSearchNames,
1441
+ rememberPassthroughResponse,
1442
+ selfNamedNamespaceScrubAuthorization,
1443
+ providerExecutedCallTypes,
1444
+ get request(): Awaited<ReturnType<typeof transportState.adapter.buildRequest>> {
1445
+ return request;
1446
+ },
1447
+ set request(value: Awaited<ReturnType<typeof transportState.adapter.buildRequest>>) {
1448
+ request = value;
1449
+ },
1450
+ functionRepairSchemas,
1451
+ get outboundRequestBody(): Record<string, unknown> | undefined {
1452
+ return outboundRequestBody;
1453
+ },
1454
+ set outboundRequestBody(value: Record<string, unknown> | undefined) {
1455
+ outboundRequestBody = value;
1456
+ },
1457
+ declaredWireToolNames,
1458
+ declaredBareWireToolNames,
1459
+ declaredNamelessClientCallTypes,
1460
+ authorizedBareNamespaceToolAliases,
1461
+ normalizeFunctionCompletionJson,
1462
+ get undeclaredToolGuardActive(): typeof undeclaredToolGuardActive {
1463
+ return undeclaredToolGuardActive;
1464
+ },
1465
+ set undeclaredToolGuardActive(value: typeof undeclaredToolGuardActive) {
1466
+ undeclaredToolGuardActive = value;
1467
+ },
1468
+ noteInspectedPayload,
1469
+ rememberPassthroughResponseChecked,
1470
+ upstream,
1471
+ connectMs,
1472
+ upstreamResponse,
1473
+ };
1474
+ }
1475
+
1476
+ export type PassthroughExchange = Exclude<Awaited<ReturnType<typeof preparePassthroughExchange>>, Response>;