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