@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.
- package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
2
|
+
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
|
+
import type { ResponsesTransport } from "./request-transport";
|
|
4
|
+
import type { ResponsesEffects } from "./response-effects";
|
|
5
|
+
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
6
|
+
import { linkAbortSignal } from "./core-lifetime";
|
|
7
|
+
import type { AdapterRequest } from "../../adapters/base";
|
|
8
|
+
import type { AdapterEvent } from "../../types";
|
|
9
|
+
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse } from "../../bridge";
|
|
10
|
+
import { trackStreamLifetime } from "../lifecycle";
|
|
11
|
+
import {
|
|
12
|
+
recordAdapterReasoning,
|
|
13
|
+
recordAdapterTier,
|
|
14
|
+
noteAttemptSend,
|
|
15
|
+
sealRequestAttemptIdentity,
|
|
16
|
+
recordAttemptCredentialSource,
|
|
17
|
+
} from "../request-log";
|
|
18
|
+
import { clientCancelledResponse, readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
|
|
19
|
+
import { redactSecretString } from "../../lib/redact";
|
|
20
|
+
import { waitForProviderRequestSlot } from "../../providers/request-pacing";
|
|
21
|
+
import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
|
|
22
|
+
import {
|
|
23
|
+
transientRetryPolicyFor,
|
|
24
|
+
rateLimitRetryPolicyFor,
|
|
25
|
+
hasKeyPoolFailover,
|
|
26
|
+
rotateProviderTransportOn401,
|
|
27
|
+
rateLimitRetryDelayMs,
|
|
28
|
+
rotateProviderTransportOn429,
|
|
29
|
+
} from "../../providers/key-failover";
|
|
30
|
+
import {
|
|
31
|
+
fetchWithTransientRetry,
|
|
32
|
+
fetchWithResetRetry,
|
|
33
|
+
applyUpstreamRecoveryInit,
|
|
34
|
+
SendBudgetExhaustedError,
|
|
35
|
+
prepareSameTarget429Wait,
|
|
36
|
+
sleepWithAbort,
|
|
37
|
+
} from "../../lib/upstream-retry";
|
|
38
|
+
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
39
|
+
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
40
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
41
|
+
import type { OAuthAccessSnapshot } from "../../oauth";
|
|
42
|
+
import { publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
43
|
+
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
44
|
+
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
45
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
46
|
+
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
47
|
+
import {
|
|
48
|
+
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
49
|
+
rotateAnthropicAccountOn429,
|
|
50
|
+
getAnthropicPoolAccessSnapshot,
|
|
51
|
+
formatAnthropicProviderForLog,
|
|
52
|
+
} from "../../oauth/anthropic-routing";
|
|
53
|
+
import {
|
|
54
|
+
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
55
|
+
isGenericOAuthFailoverEnabled,
|
|
56
|
+
rotateGenericOAuthAccountOn429,
|
|
57
|
+
failoverAccountSnapshot,
|
|
58
|
+
} from "../../oauth/generic-account-failover";
|
|
59
|
+
import {
|
|
60
|
+
attemptOpaqueBlobRecovery,
|
|
61
|
+
consoleGoUploadRejectionBody,
|
|
62
|
+
CONSOLE_GO_UPLOAD_RETRY_DELAY_MS,
|
|
63
|
+
reasoningEffortRejectionText,
|
|
64
|
+
} from "./core-opaque-recovery";
|
|
65
|
+
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
66
|
+
import {
|
|
67
|
+
isTransientConsoleGoUploadRejection,
|
|
68
|
+
enrichOpenCodeZenUpstreamMessage,
|
|
69
|
+
} from "../../providers/opencode-zen-rate-limit";
|
|
70
|
+
import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
|
|
71
|
+
import { consumeComboFailure } from "./core-combo-failure";
|
|
72
|
+
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
73
|
+
import { isFixedCodexAccount } from "./core-codex-account";
|
|
74
|
+
import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-model-fallback";
|
|
75
|
+
import { isCyberPolicyCode, CYBER_POLICY_FALLBACK_MESSAGE, CYBER_POLICY_ERROR_CODE } from "../../lib/errors";
|
|
76
|
+
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
77
|
+
import { cancelBodyOnAbort } from "../../lib/abort";
|
|
78
|
+
|
|
79
|
+
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
80
|
+
export async function prepareAdapterExchange(
|
|
81
|
+
requestContext: Pick<ResponsesRequestContext, "options" | "config" | "logCtx" | "req">,
|
|
82
|
+
admissionState: ResponsesAdmissionState,
|
|
83
|
+
requestState: Pick<
|
|
84
|
+
PreparedResponsesRequest,
|
|
85
|
+
| "parsed"
|
|
86
|
+
| "toolBridgeMaps"
|
|
87
|
+
| "translatorBudget"
|
|
88
|
+
| "selectedForwardHeaders"
|
|
89
|
+
| "route"
|
|
90
|
+
| "inboundWire"
|
|
91
|
+
| "clientRequestedStream"
|
|
92
|
+
| "subagentQuotaFailureModel"
|
|
93
|
+
| "subagentFallbackAccountId"
|
|
94
|
+
>,
|
|
95
|
+
transportState: Pick<
|
|
96
|
+
ResponsesTransport,
|
|
97
|
+
| "activeAdapter"
|
|
98
|
+
| "adapter"
|
|
99
|
+
| "sameTargetRequest"
|
|
100
|
+
| "sameTargetParsed"
|
|
101
|
+
| "sameTargetToken"
|
|
102
|
+
| "transportToken"
|
|
103
|
+
| "oauthDispatch"
|
|
104
|
+
| "imageTierBias"
|
|
105
|
+
| "isOAuth401ReplayProvider"
|
|
106
|
+
| "sentOAuthSnapshot"
|
|
107
|
+
| "refreshResolvedOAuthSelection"
|
|
108
|
+
| "replayOAuthCredentialSnapshot"
|
|
109
|
+
| "invalidateSameTargetRequest"
|
|
110
|
+
| "resolveSelectionAdapter"
|
|
111
|
+
| "anthropicPoolAccountId"
|
|
112
|
+
| "anthropicPoolFailovers"
|
|
113
|
+
| "anthropicSessionKey"
|
|
114
|
+
| "commitResolvedOAuthSelection"
|
|
115
|
+
| "genericFailoverAccountId"
|
|
116
|
+
| "genericFailovers"
|
|
117
|
+
| "applyFailoverSnapshot"
|
|
118
|
+
>,
|
|
119
|
+
responseEffects: Pick<ResponsesEffects, "cancelResponseCompletion" | "notifyResponseComplete" | "refreshRequestToolAliases">,
|
|
120
|
+
sendBudgetState: Pick<
|
|
121
|
+
ResponsesSendBudget,
|
|
122
|
+
| "adapterSendBudget"
|
|
123
|
+
| "noteAdapterPhysicalSend"
|
|
124
|
+
| "remainingTransientSendBudget"
|
|
125
|
+
| "noteTransientSends"
|
|
126
|
+
| "recoverySendAllowance"
|
|
127
|
+
| "recoveryClassFor"
|
|
128
|
+
| "sendBudgetExhausted"
|
|
129
|
+
| "reserveCredentialHop"
|
|
130
|
+
>,
|
|
131
|
+
) {
|
|
132
|
+
const { options, config, logCtx, req } = requestContext;
|
|
133
|
+
const {
|
|
134
|
+
oauthDispatch,
|
|
135
|
+
isOAuth401ReplayProvider,
|
|
136
|
+
refreshResolvedOAuthSelection,
|
|
137
|
+
invalidateSameTargetRequest,
|
|
138
|
+
resolveSelectionAdapter,
|
|
139
|
+
anthropicSessionKey,
|
|
140
|
+
commitResolvedOAuthSelection,
|
|
141
|
+
applyFailoverSnapshot,
|
|
142
|
+
} = transportState;
|
|
143
|
+
const {
|
|
144
|
+
parsed,
|
|
145
|
+
toolBridgeMaps,
|
|
146
|
+
translatorBudget,
|
|
147
|
+
route,
|
|
148
|
+
inboundWire,
|
|
149
|
+
clientRequestedStream,
|
|
150
|
+
subagentQuotaFailureModel,
|
|
151
|
+
} = requestState;
|
|
152
|
+
const { cancelResponseCompletion, notifyResponseComplete, refreshRequestToolAliases } = responseEffects;
|
|
153
|
+
const {
|
|
154
|
+
adapterSendBudget,
|
|
155
|
+
noteAdapterPhysicalSend,
|
|
156
|
+
remainingTransientSendBudget,
|
|
157
|
+
noteTransientSends,
|
|
158
|
+
recoverySendAllowance,
|
|
159
|
+
recoveryClassFor,
|
|
160
|
+
sendBudgetExhausted,
|
|
161
|
+
reserveCredentialHop,
|
|
162
|
+
} = sendBudgetState;
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
const upstream = new AbortController();
|
|
166
|
+
const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
|
|
167
|
+
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
168
|
+
// Bridge stall budget (seconds of silence before upstream_stall_timeout); the retry backoff
|
|
169
|
+
// heartbeat interval is derived from it so the watchdog is always fed during deliberate waits.
|
|
170
|
+
const stallTimeoutMs = typeof config.stallTimeoutSec === "number" && Number.isFinite(config.stallTimeoutSec) && config.stallTimeoutSec > 0
|
|
171
|
+
? Math.floor(config.stallTimeoutSec * 1000)
|
|
172
|
+
: 300_000;
|
|
173
|
+
transportState.activeAdapter = transportState.adapter;
|
|
174
|
+
|
|
175
|
+
// One immutable, body-safe outbound request per same-target sequence (URL, serialized body,
|
|
176
|
+
// auth headers, generated compat headers). Same-target 429 replays reuse it verbatim; the
|
|
177
|
+
// builder runs again only after a key/account/adapter rotation, an oauth refresh, or an
|
|
178
|
+
// image-tier bias change (transportToken bump). `body` is always a serialized string, so
|
|
179
|
+
// reuse is safe, and releaseBodyObservation is idempotent per build.
|
|
180
|
+
let initialRequest: AdapterRequest | undefined;
|
|
181
|
+
let inputTokenEstimate: number | undefined;
|
|
182
|
+
// An adapter may know the turn needs no inference at all — Kiro's replayed history ending in a
|
|
183
|
+
// delivered final answer. Answer it locally: no build (so no token estimate), no send (so
|
|
184
|
+
// sendCount stays 0), and crucially no empty-completion guard, which treats an outputless
|
|
185
|
+
// terminal as a failed turn and re-invokes the identical request. Routing this through the
|
|
186
|
+
// ordinary event path would therefore reinstate the loop it exists to end.
|
|
187
|
+
const localTerminal = transportState.activeAdapter.localTerminal?.(parsed);
|
|
188
|
+
if (localTerminal) {
|
|
189
|
+
logCtx.localTerminalReason = localTerminal.reason;
|
|
190
|
+
// Mark the physical attempt too, not just the parent row. `finishRequestAttempt` finalizes the
|
|
191
|
+
// attempt through the same estimated-provider path, so without this the row reads exact while
|
|
192
|
+
// its own attempt still claims an estimate — the detailed accounting a maintainer actually
|
|
193
|
+
// reads for a zero-send turn.
|
|
194
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.locallyAnswered = true;
|
|
195
|
+
cleanupUpstreamAbort();
|
|
196
|
+
upstream.abort();
|
|
197
|
+
const terminalEvents: AdapterEvent[] = [{
|
|
198
|
+
type: "done",
|
|
199
|
+
endTurn: true,
|
|
200
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
201
|
+
}];
|
|
202
|
+
if (parsed.stream) {
|
|
203
|
+
const localSse = bridgeToResponsesSSE(
|
|
204
|
+
(async function* () { yield* terminalEvents; })(),
|
|
205
|
+
parsed._responseModelId ?? parsed.modelId,
|
|
206
|
+
toolBridgeMaps.toolNsMap,
|
|
207
|
+
toolBridgeMaps.freeformToolNames,
|
|
208
|
+
toolBridgeMaps.toolSearchToolNames,
|
|
209
|
+
cancelResponseCompletion,
|
|
210
|
+
2_000,
|
|
211
|
+
{
|
|
212
|
+
translatorBudget,
|
|
213
|
+
onCompletedResponse: notifyResponseComplete,
|
|
214
|
+
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
215
|
+
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
// Same lifetime tracking as every other streaming return in this function: the turn
|
|
219
|
+
// admission lease is released when the body finishes or the client disconnects. Returning
|
|
220
|
+
// the raw stream would hold a lease for a turn that already has all of its output.
|
|
221
|
+
const localTurnAc = new AbortController();
|
|
222
|
+
return new Response(
|
|
223
|
+
trackStreamLifetime(localSse, localTurnAc, undefined, options.turnAdmissionLease),
|
|
224
|
+
{
|
|
225
|
+
headers: {
|
|
226
|
+
"Content-Type": "text/event-stream",
|
|
227
|
+
"Cache-Control": "no-cache",
|
|
228
|
+
"Connection": "keep-alive",
|
|
229
|
+
"X-Accel-Buffering": "no",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const json = buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, { translatorBudget });
|
|
235
|
+
notifyResponseComplete(json);
|
|
236
|
+
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
initialRequest = await transportState.activeAdapter.buildRequest(parsed, { headers: requestState.selectedForwardHeaders, translatorBudget });
|
|
240
|
+
refreshRequestToolAliases(initialRequest);
|
|
241
|
+
recordAdapterReasoning(logCtx, initialRequest);
|
|
242
|
+
recordAdapterTier(logCtx, initialRequest);
|
|
243
|
+
inputTokenEstimate = typeof initialRequest.usageLog?.inputTokens === "number"
|
|
244
|
+
? initialRequest.usageLog.inputTokens
|
|
245
|
+
: undefined;
|
|
246
|
+
if (inputTokenEstimate !== undefined) logCtx.usageLogInputTokens = inputTokenEstimate;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
// A throwing buildRequest never returned a request; if a post-build step threw, release
|
|
249
|
+
// the serialized-body observation (idempotent) so the translator budget is not leaked.
|
|
250
|
+
// The build runs after linkAbortSignal, so a failure must also tear the link down and
|
|
251
|
+
// abort the upstream controller instead of escaping handleResponses unmapped.
|
|
252
|
+
initialRequest?.releaseBodyObservation?.();
|
|
253
|
+
cleanupUpstreamAbort();
|
|
254
|
+
upstream.abort();
|
|
255
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
256
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
257
|
+
return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
|
|
258
|
+
}
|
|
259
|
+
// The catch path above always returns, so the request is definitely assigned here.
|
|
260
|
+
// Capture it in a const so the fetch callbacks read a narrowed, immutable value
|
|
261
|
+
// (TypeScript drops narrowing for a `let` captured by a nested function).
|
|
262
|
+
const builtInitialRequest = initialRequest;
|
|
263
|
+
transportState.sameTargetRequest = builtInitialRequest;
|
|
264
|
+
transportState.sameTargetParsed = parsed;
|
|
265
|
+
transportState.sameTargetToken = transportState.transportToken;
|
|
266
|
+
/**
|
|
267
|
+
* Invalidate the same-target request cache. Every credential/adapter/parsed mutation MUST
|
|
268
|
+
* go through here: the cache keys on `parsed` REFERENCE identity, so an in-place mutation
|
|
269
|
+
* is invisible to it and a missed bump would replay a request built with a stale key.
|
|
270
|
+
*/
|
|
271
|
+
|
|
272
|
+
let upstreamResponse: Response;
|
|
273
|
+
try {
|
|
274
|
+
if (transportState.activeAdapter.fetchResponse) {
|
|
275
|
+
noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
|
|
276
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal);
|
|
277
|
+
upstreamResponse = await transportState.activeAdapter.fetchResponse(builtInitialRequest, {
|
|
278
|
+
abortSignal: upstream.signal,
|
|
279
|
+
timeoutMs: connectMs,
|
|
280
|
+
sendBudget: adapterSendBudget,
|
|
281
|
+
onPhysicalSend: send => noteAdapterPhysicalSend(inputTokenEstimate, send),
|
|
282
|
+
stream: parsed.stream,
|
|
283
|
+
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
284
|
+
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
285
|
+
providerName: route.providerName,
|
|
286
|
+
modelId: route.modelId,
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
// #1851 scope guard: transient-5xx retry on this generic adapter path is opt-in for
|
|
291
|
+
// direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other
|
|
292
|
+
// adapters keep reset-only retry so combo failover still hops on the first 5xx
|
|
293
|
+
// instead of burning ~1.2s of same-target retries per hop.
|
|
294
|
+
// #2643: an opted-in key-auth openai-chat provider also gets transient-5xx retry. The
|
|
295
|
+
// legacy direct-Google exception is preserved exactly; every other adapter still keeps
|
|
296
|
+
// reset-only semantics so combo failover hops on the first 5xx.
|
|
297
|
+
const transientPolicy = transientRetryPolicyFor(route.provider);
|
|
298
|
+
const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
|
|
299
|
+
? fetchWithTransientRetry
|
|
300
|
+
: fetchWithResetRetry;
|
|
301
|
+
upstreamResponse = await fetchWithRetryPolicy(
|
|
302
|
+
recovery => {
|
|
303
|
+
noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery);
|
|
304
|
+
return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
|
|
305
|
+
method: builtInitialRequest.method,
|
|
306
|
+
headers: builtInitialRequest.headers,
|
|
307
|
+
body: builtInitialRequest.body,
|
|
308
|
+
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
309
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
310
|
+
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
311
|
+
providerName: route.providerName,
|
|
312
|
+
modelId: route.modelId,
|
|
313
|
+
}));
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
abortSignal: upstream.signal,
|
|
317
|
+
label: safeHostLabel(builtInitialRequest.url),
|
|
318
|
+
...(transientPolicy
|
|
319
|
+
// Draws the remainder, not the raw policy. A combo child inherits the parent's
|
|
320
|
+
// holder but used to take a fresh full allowance on its own first send, so the
|
|
321
|
+
// shared counter was inherited without ever being read as a limit.
|
|
322
|
+
? {
|
|
323
|
+
attempts: remainingTransientSendBudget(transientPolicy.attempts),
|
|
324
|
+
onSendsConsumed: noteTransientSends,
|
|
325
|
+
}
|
|
326
|
+
: {}),
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
} catch (err) {
|
|
331
|
+
cleanupUpstreamAbort();
|
|
332
|
+
upstream.abort();
|
|
333
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
334
|
+
const msg = describeUpstreamConnectFailure(err, connectMs);
|
|
335
|
+
return formatErrorResponse(502, "upstream_error", msg);
|
|
336
|
+
} finally {
|
|
337
|
+
builtInitialRequest.releaseBodyObservation?.();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Same-target 429 retry budget is per REQUEST: it lives OUTSIDE the recovery loop (so a 413/401
|
|
341
|
+
// replay that comes back 429 cannot silently re-arm a fresh budget) and is SHARED with the
|
|
342
|
+
// terminal-guard continuation below, so the main loop + one continuation can never exceed
|
|
343
|
+
// `attempts` same-key replays in total (bounded per request).
|
|
344
|
+
const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
|
|
345
|
+
let rateLimitRetries = 0;
|
|
346
|
+
// Shared with the terminal-guard continuation below: an image-tier reduction that let the
|
|
347
|
+
// main request clear a 413 must not be forgotten on the very next continuation build.
|
|
348
|
+
if (!upstreamResponse.ok) {
|
|
349
|
+
// Recovery loop: multi-key 429 failover + at most ONE opaque-state rebuild and ONE
|
|
350
|
+
// anthropic 413 tightened retry
|
|
351
|
+
// (devlog/260714_image_normalization_pipeline/030). One mutable activeAdapter serves
|
|
352
|
+
// both paths so a 429→413 sequence never rebuilds against a stale pre-rotation
|
|
353
|
+
// adapter, and imageTierBias — once armed — rides EVERY subsequent rebuild so a
|
|
354
|
+
// 413→429 rotation cannot silently undo the tightening.
|
|
355
|
+
let imageRetryAttempted = false;
|
|
356
|
+
const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
|
|
357
|
+
// Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
|
|
358
|
+
// moments later; at most one byte-identical replay is allowed per request.
|
|
359
|
+
const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
|
|
360
|
+
let oauth401ReplayAttempted = false;
|
|
361
|
+
// At most one reasoning-effort downgrade per request. This sits outside the recovery loop
|
|
362
|
+
// below for the same reason the two guards above do: a guard declared inside it is reset by
|
|
363
|
+
// every `continue recovery`, which would let one turn walk the whole ladder down.
|
|
364
|
+
const reasoningEffortDowngradeGuard: { attempted: boolean } = { attempted: false };
|
|
365
|
+
/**
|
|
366
|
+
* Rebuild the request from the current parsed input (and any image-tier bias) and refetch
|
|
367
|
+
* it once, tagging the attempt with the given recovery kind. Rebuilds are deterministic
|
|
368
|
+
* for the same parsed request, so same-target replays stay byte-identical.
|
|
369
|
+
*/
|
|
370
|
+
const rebuildAndRefetch = async (
|
|
371
|
+
recovery: AttemptRecoveryKind,
|
|
372
|
+
/**
|
|
373
|
+
* Called at the dispatch boundary — after the request is rebuilt and shaped, immediately
|
|
374
|
+
* before the send. A caller holding a reserved hop confirms it here rather than before the
|
|
375
|
+
* rebuild, because a build failure returns `{ failed }` without ever reaching the wire and
|
|
376
|
+
* a permit confirmed earlier would keep the charge for a send that never happened.
|
|
377
|
+
*/
|
|
378
|
+
onDispatch?: () => void,
|
|
379
|
+
): Promise<Response | { failed: Response }> => {
|
|
380
|
+
let retryRequest: AdapterRequest;
|
|
381
|
+
if (transportState.sameTargetRequest !== undefined && transportState.sameTargetParsed === parsed && transportState.sameTargetToken === transportState.transportToken) {
|
|
382
|
+
// Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
|
|
383
|
+
retryRequest = transportState.sameTargetRequest;
|
|
384
|
+
} else {
|
|
385
|
+
try {
|
|
386
|
+
retryRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
387
|
+
headers: requestState.selectedForwardHeaders,
|
|
388
|
+
translatorBudget,
|
|
389
|
+
...(transportState.imageTierBias > 0 ? { imageTierBias: transportState.imageTierBias } : {}),
|
|
390
|
+
});
|
|
391
|
+
recordAdapterReasoning(logCtx, retryRequest);
|
|
392
|
+
recordAdapterTier(logCtx, retryRequest);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
// A rotated/rebuilt adapter build failure is a request-shaping error, not an
|
|
395
|
+
// upstream connect failure: tear the abort link down and map it as 400 (no 413
|
|
396
|
+
// translator-budget mapping here — that stays with parseRequest/buildToolBridgeMaps).
|
|
397
|
+
cleanupUpstreamAbort();
|
|
398
|
+
upstream.abort();
|
|
399
|
+
if (options.abortSignal?.aborted) return { failed: clientCancelledResponse() };
|
|
400
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
401
|
+
return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(msg)) };
|
|
402
|
+
}
|
|
403
|
+
transportState.sameTargetRequest = retryRequest;
|
|
404
|
+
transportState.sameTargetParsed = parsed;
|
|
405
|
+
transportState.sameTargetToken = transportState.transportToken;
|
|
406
|
+
}
|
|
407
|
+
refreshRequestToolAliases(retryRequest);
|
|
408
|
+
const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
|
|
409
|
+
? retryRequest.usageLog.inputTokens
|
|
410
|
+
: undefined;
|
|
411
|
+
if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
|
|
412
|
+
logCtx.providerAdapter = transportState.activeAdapter.name;
|
|
413
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
414
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
415
|
+
noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
|
|
416
|
+
try {
|
|
417
|
+
try {
|
|
418
|
+
if (transportState.activeAdapter.fetchResponse) {
|
|
419
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal);
|
|
420
|
+
// The dispatch boundary is HERE, not before the pacing wait: that wait can reject for
|
|
421
|
+
// an abort, a saturated queue, an expired slot or a removed provider, and none of
|
|
422
|
+
// those reach the wire. Confirming earlier would hold the charge for a send that the
|
|
423
|
+
// pacer refused.
|
|
424
|
+
onDispatch?.();
|
|
425
|
+
return await transportState.activeAdapter.fetchResponse(retryRequest, {
|
|
426
|
+
abortSignal: upstream.signal,
|
|
427
|
+
timeoutMs: connectMs,
|
|
428
|
+
sendBudget: adapterSendBudget,
|
|
429
|
+
onPhysicalSend: send => noteAdapterPhysicalSend(retryEstimate, send),
|
|
430
|
+
stream: parsed.stream,
|
|
431
|
+
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
432
|
+
dispatchOverride: oauthDispatch(retryRequest),
|
|
433
|
+
providerName: route.providerName,
|
|
434
|
+
modelId: route.modelId,
|
|
435
|
+
}),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
// #2643 review: this leg used to call fetchWithHeaderTimeout directly, so an
|
|
439
|
+
// opted-in provider's transient-5xx policy applied to the initial send and to
|
|
440
|
+
// native chat but was silently bypassed here — a 429 that recovered into a
|
|
441
|
+
// retryable 503 got no retry on the Responses path. Route it through the same
|
|
442
|
+
// selection, and pass what is LEFT of the request-scoped budget rather than a
|
|
443
|
+
// fresh one, so a recovery loop cannot multiply total upstream sends.
|
|
444
|
+
const refetchTransientPolicy = transientRetryPolicyFor(route.provider);
|
|
445
|
+
const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
|
|
446
|
+
? fetchWithTransientRetry
|
|
447
|
+
: fetchWithResetRetry;
|
|
448
|
+
// Same rule as the passthrough rebuild: spend the base allowance first, then the one
|
|
449
|
+
// shared final-recovery reserve, so a recovery that follows a spent streak still gets
|
|
450
|
+
// its single send instead of dying at three.
|
|
451
|
+
const refetchAllowance = refetchTransientPolicy
|
|
452
|
+
? recoverySendAllowance(
|
|
453
|
+
refetchTransientPolicy.attempts,
|
|
454
|
+
recoveryClassFor(recovery),
|
|
455
|
+
`${route.providerName}|${route.modelId}|${recovery}`,
|
|
456
|
+
)
|
|
457
|
+
: undefined;
|
|
458
|
+
try {
|
|
459
|
+
return await refetchWithPolicy(
|
|
460
|
+
recoveryKind => {
|
|
461
|
+
if (refetchAllowance?.permit && !refetchAllowance.permit.use()) {
|
|
462
|
+
throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
|
|
463
|
+
}
|
|
464
|
+
// Same boundary on the helper path: the thunk is what reaches the wire, and it
|
|
465
|
+
// can be refused above before it does. use() past the first attempt is a no-op.
|
|
466
|
+
onDispatch?.();
|
|
467
|
+
return fetchWithHeaderTimeout(retryRequest.url,
|
|
468
|
+
applyUpstreamRecoveryInit({
|
|
469
|
+
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
470
|
+
}, recoveryKind), upstream.signal, connectMs, parsed.stream,
|
|
471
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
472
|
+
dispatchOverride: oauthDispatch(retryRequest),
|
|
473
|
+
providerName: route.providerName,
|
|
474
|
+
modelId: route.modelId,
|
|
475
|
+
}));
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
abortSignal: upstream.signal,
|
|
479
|
+
label: safeHostLabel(retryRequest.url),
|
|
480
|
+
...(refetchAllowance
|
|
481
|
+
? {
|
|
482
|
+
attempts: refetchAllowance.attempts,
|
|
483
|
+
onSendsConsumed: noteTransientSends,
|
|
484
|
+
}
|
|
485
|
+
: {}),
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
} finally {
|
|
489
|
+
// Refunds only a reservation whose send never happened -- an abort settled before
|
|
490
|
+
// the thunk ran. A used or externally settled permit ignores this.
|
|
491
|
+
refetchAllowance?.permit?.release();
|
|
492
|
+
}
|
|
493
|
+
} finally {
|
|
494
|
+
retryRequest.releaseBodyObservation?.();
|
|
495
|
+
}
|
|
496
|
+
} catch (err) {
|
|
497
|
+
cleanupUpstreamAbort();
|
|
498
|
+
upstream.abort();
|
|
499
|
+
if (options.abortSignal?.aborted) {
|
|
500
|
+
return { failed: clientCancelledResponse() };
|
|
501
|
+
}
|
|
502
|
+
const msg = describeUpstreamConnectFailure(err, connectMs);
|
|
503
|
+
return { failed: formatErrorResponse(502, "upstream_error", msg) };
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
// Keep recovery kinds in sync with the native Responses `passthroughRecovery:` loop above.
|
|
507
|
+
recovery: for (;;) {
|
|
508
|
+
if (
|
|
509
|
+
upstreamResponse.status === 401
|
|
510
|
+
&& isOAuth401ReplayProvider
|
|
511
|
+
&& transportState.sentOAuthSnapshot
|
|
512
|
+
&& !oauth401ReplayAttempted
|
|
513
|
+
&& !sendBudgetExhausted()
|
|
514
|
+
) {
|
|
515
|
+
oauth401ReplayAttempted = true;
|
|
516
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
517
|
+
let refreshed: OAuthAccessSnapshot;
|
|
518
|
+
try {
|
|
519
|
+
refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
cleanupUpstreamAbort();
|
|
522
|
+
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
|
|
523
|
+
}
|
|
524
|
+
if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
|
|
525
|
+
cleanupUpstreamAbort();
|
|
526
|
+
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(new Error("Cloud Code Assist project is required")));
|
|
527
|
+
}
|
|
528
|
+
transportState.sentOAuthSnapshot = refreshed;
|
|
529
|
+
transportState.replayOAuthCredentialSnapshot = {
|
|
530
|
+
accountId: refreshed.accountId,
|
|
531
|
+
generation: refreshed.generation,
|
|
532
|
+
};
|
|
533
|
+
if (route.providerName === "kiro") {
|
|
534
|
+
parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
|
|
535
|
+
}
|
|
536
|
+
const refreshedProvider = resolveProviderTransport(
|
|
537
|
+
route.providerName,
|
|
538
|
+
{
|
|
539
|
+
...route.provider,
|
|
540
|
+
apiKey: refreshed.accessToken,
|
|
541
|
+
...(refreshed.projectId ? { project: refreshed.projectId } : {}),
|
|
542
|
+
},
|
|
543
|
+
parsed.options.promptCacheKey,
|
|
544
|
+
route.providerName === "github-copilot"
|
|
545
|
+
? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
|
|
546
|
+
: undefined,
|
|
547
|
+
);
|
|
548
|
+
route.provider = refreshedProvider;
|
|
549
|
+
invalidateSameTargetRequest();
|
|
550
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
551
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
|
|
552
|
+
config.cacheRetention,
|
|
553
|
+
);
|
|
554
|
+
bindRouteReasoningReplayScope({
|
|
555
|
+
parsed,
|
|
556
|
+
providerName: route.providerName,
|
|
557
|
+
provider: refreshedProvider,
|
|
558
|
+
adapterName: transportState.activeAdapter.name,
|
|
559
|
+
oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
|
|
560
|
+
});
|
|
561
|
+
const result = await rebuildAndRefetch("oauth-401");
|
|
562
|
+
if ("failed" in result) return result.failed;
|
|
563
|
+
upstreamResponse = result;
|
|
564
|
+
continue recovery;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Static API-key pools can recover a credential-scoped 401 without abandoning the
|
|
568
|
+
// provider: one revoked or mistyped key says nothing about its siblings. OAuth providers
|
|
569
|
+
// refresh above and never enter here — `hasKeyPoolFailover` rejects oauth/forward modes.
|
|
570
|
+
// Runs after the OAuth replay so a refreshable token is never treated as a dead key.
|
|
571
|
+
while (upstreamResponse.status === 401 && hasKeyPoolFailover(route.provider)) {
|
|
572
|
+
const rotated = rotateProviderTransportOn401(config, route.providerName, route.provider, {
|
|
573
|
+
now: Date.now(),
|
|
574
|
+
attemptedKey: route.provider.apiKey,
|
|
575
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
576
|
+
});
|
|
577
|
+
if (!rotated) break;
|
|
578
|
+
// Release the failed response's socket before retrying; unread bodies otherwise linger
|
|
579
|
+
// until runtime cleanup (one per rotated key).
|
|
580
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
581
|
+
route.provider = rotated;
|
|
582
|
+
invalidateSameTargetRequest();
|
|
583
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
584
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
585
|
+
config.cacheRetention,
|
|
586
|
+
);
|
|
587
|
+
bindRouteReasoningReplayScope({
|
|
588
|
+
parsed,
|
|
589
|
+
providerName: route.providerName,
|
|
590
|
+
provider: route.provider,
|
|
591
|
+
adapterName: transportState.activeAdapter.name,
|
|
592
|
+
});
|
|
593
|
+
const result = await rebuildAndRefetch("key-401");
|
|
594
|
+
if ("failed" in result) return result.failed;
|
|
595
|
+
upstreamResponse = result;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
|
|
599
|
+
// 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
|
|
600
|
+
// so wait (Retry-After or the fixed interval) and replay the IDENTICAL request on the
|
|
601
|
+
// same key first. Pre-stream only: a 429 arrives before any bytes are relayed, so the
|
|
602
|
+
// replay is lossless. Runs before key failover so "primary-first" setups keep the same
|
|
603
|
+
// key on rate-limit blips; only after the attempts are exhausted does failover run.
|
|
604
|
+
while (
|
|
605
|
+
upstreamResponse.status === 429
|
|
606
|
+
&& rateLimitPolicy !== null
|
|
607
|
+
&& rateLimitRetries < rateLimitPolicy.attempts
|
|
608
|
+
&& !sendBudgetExhausted()
|
|
609
|
+
) {
|
|
610
|
+
rateLimitRetries += 1;
|
|
611
|
+
// Release unread body + deliberate wait via the shared same-target helper.
|
|
612
|
+
const retryAfterHeader = upstreamResponse.headers.get("retry-after");
|
|
613
|
+
try {
|
|
614
|
+
for await (const _ of prepareSameTarget429Wait({
|
|
615
|
+
body: upstreamResponse.body,
|
|
616
|
+
signal: options.abortSignal,
|
|
617
|
+
delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
|
|
618
|
+
})) {
|
|
619
|
+
// pre-stream: no stall watchdog to feed
|
|
620
|
+
}
|
|
621
|
+
} catch {
|
|
622
|
+
cleanupUpstreamAbort();
|
|
623
|
+
upstream.abort();
|
|
624
|
+
return clientCancelledResponse();
|
|
625
|
+
}
|
|
626
|
+
// Client cancellation wins over any stale timer edge: re-check before dispatching the
|
|
627
|
+
// replay so an adapter never starts work for a request the client already abandoned.
|
|
628
|
+
if (options.abortSignal?.aborted || upstream.signal.aborted) {
|
|
629
|
+
cleanupUpstreamAbort();
|
|
630
|
+
upstream.abort();
|
|
631
|
+
return clientCancelledResponse();
|
|
632
|
+
}
|
|
633
|
+
const result = await rebuildAndRefetch("rate-limit-429");
|
|
634
|
+
if ("failed" in result) return result.failed;
|
|
635
|
+
upstreamResponse = result;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
|
|
639
|
+
// SAME request once per remaining key. OAuth/forward providers and single-key pools
|
|
640
|
+
// return null immediately, so this stays a no-op for them (src/providers/key-failover.ts).
|
|
641
|
+
while (upstreamResponse.status === 429 && hasKeyPoolFailover(route.provider)) {
|
|
642
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
643
|
+
retryAfter: upstreamResponse.headers.get("retry-after"),
|
|
644
|
+
now: Date.now(),
|
|
645
|
+
attemptedKey: route.provider.apiKey,
|
|
646
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
647
|
+
});
|
|
648
|
+
if (!rotated) break;
|
|
649
|
+
// Release the failed response's socket before retrying; unread bodies otherwise linger
|
|
650
|
+
// until runtime cleanup (one per rotated key under a rate-limit storm).
|
|
651
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
652
|
+
route.provider = rotated;
|
|
653
|
+
invalidateSameTargetRequest();
|
|
654
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
655
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
656
|
+
config.cacheRetention,
|
|
657
|
+
);
|
|
658
|
+
bindRouteReasoningReplayScope({
|
|
659
|
+
parsed,
|
|
660
|
+
providerName: route.providerName,
|
|
661
|
+
provider: route.provider,
|
|
662
|
+
adapterName: transportState.activeAdapter.name,
|
|
663
|
+
});
|
|
664
|
+
const result = await rebuildAndRefetch("key-429");
|
|
665
|
+
if ("failed" in result) return result.failed;
|
|
666
|
+
upstreamResponse = result;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Opt-in Anthropic OAuth account pool (#294): cool the failed account and retry
|
|
670
|
+
// with another eligible OAuth account (bounded per request). Disabled by default.
|
|
671
|
+
while (
|
|
672
|
+
upstreamResponse.status === 429
|
|
673
|
+
&& transportState.anthropicPoolAccountId
|
|
674
|
+
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
675
|
+
) {
|
|
676
|
+
const nextAccountId = rotateAnthropicAccountOn429(
|
|
677
|
+
config,
|
|
678
|
+
transportState.anthropicPoolAccountId,
|
|
679
|
+
upstreamResponse.headers.get("retry-after"),
|
|
680
|
+
anthropicSessionKey,
|
|
681
|
+
Date.now(),
|
|
682
|
+
upstreamResponse.headers,
|
|
683
|
+
);
|
|
684
|
+
if (!nextAccountId) break;
|
|
685
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
686
|
+
try {
|
|
687
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
688
|
+
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
689
|
+
transportState.anthropicPoolAccountId = admitted.accountId;
|
|
690
|
+
transportState.anthropicPoolFailovers += 1;
|
|
691
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
692
|
+
invalidateSameTargetRequest();
|
|
693
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
694
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
695
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
696
|
+
config.cacheRetention,
|
|
697
|
+
);
|
|
698
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
699
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
700
|
+
const result = await rebuildAndRefetch("anthropic-oauth-429");
|
|
701
|
+
if ("failed" in result) return result.failed;
|
|
702
|
+
upstreamResponse = result;
|
|
703
|
+
} catch {
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Generic OAuth account failover (#2568) for providers with no pool of their own.
|
|
708
|
+
// Presence is consent since #2568d: rotation is ON by default once two or more eligible
|
|
709
|
+
// accounts are stored for the provider, because a second deliberate login is read as the
|
|
710
|
+
// operator asking for it. A single-account install is still a strict no-op, and an
|
|
711
|
+
// explicit `oauthAccountFailover.enabled: false` (global or per provider) still wins --
|
|
712
|
+
// see isGenericOAuthFailoverEnabled in src/oauth/generic-account-failover.ts. Codex and
|
|
713
|
+
// Anthropic are excluded by isGenericFailoverProvider: their pools own quota scopes,
|
|
714
|
+
// probe leases and affinity that this must not reimplement.
|
|
715
|
+
while (
|
|
716
|
+
upstreamResponse.status === 429
|
|
717
|
+
&& transportState.genericFailoverAccountId
|
|
718
|
+
&& transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
719
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
720
|
+
) {
|
|
721
|
+
// Intersection with the shared request budget. This arm re-sends through
|
|
722
|
+
// rebuildAndRefetch, so the roster cap alone would let one request walk the roster on
|
|
723
|
+
// an allowance the rest of the request cannot see. A refusal ends the ladder with the
|
|
724
|
+
// real 429 already in hand, which is the decided exhaustion contract.
|
|
725
|
+
const hop = reserveCredentialHop(
|
|
726
|
+
"auth-recovery",
|
|
727
|
+
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
|
|
728
|
+
);
|
|
729
|
+
if (!hop.allowed) break;
|
|
730
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
731
|
+
config,
|
|
732
|
+
route.providerName,
|
|
733
|
+
transportState.genericFailoverAccountId,
|
|
734
|
+
upstreamResponse.headers.get("retry-after"),
|
|
735
|
+
);
|
|
736
|
+
if (!nextAccountId) {
|
|
737
|
+
hop.permit?.release();
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
741
|
+
try {
|
|
742
|
+
// The FULL snapshot, not just the bearer: Antigravity pairs an account-matched
|
|
743
|
+
// projectId with its token and Kiro carries routing metadata, so a token-only swap
|
|
744
|
+
// would mix one account's credential with another's routing data.
|
|
745
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
746
|
+
transportState.genericFailovers += 1;
|
|
747
|
+
if (!await applyFailoverSnapshot(snapshot)) {
|
|
748
|
+
hop.permit?.release();
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
invalidateSameTargetRequest();
|
|
752
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
753
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
754
|
+
config.cacheRetention,
|
|
755
|
+
);
|
|
756
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
757
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
758
|
+
// Confirm at the dispatch boundary, not here: a rebuild can fail while shaping the
|
|
759
|
+
// request and return `{ failed }` without reaching the wire, and a permit confirmed
|
|
760
|
+
// before that would hold the charge for a send that never happened.
|
|
761
|
+
const result = await rebuildAndRefetch("oauth-account-429", () => { hop.permit?.use(); });
|
|
762
|
+
if ("failed" in result) {
|
|
763
|
+
// A no-op if the boundary was reached; a refund if the rebuild died before it.
|
|
764
|
+
hop.permit?.release();
|
|
765
|
+
return result.failed;
|
|
766
|
+
}
|
|
767
|
+
upstreamResponse = result;
|
|
768
|
+
} catch {
|
|
769
|
+
// A throw before the send — snapshot fetch, credential application, adapter
|
|
770
|
+
// resolution — must hand the reservation back. Without this the ladder charges the
|
|
771
|
+
// request for a send it never made, and a later recovery in the same request is
|
|
772
|
+
// refused on an allowance nothing spent. release() is idempotent and a no-op once
|
|
773
|
+
// used, so a throw from the rebuild keeps its charge.
|
|
774
|
+
hop.permit?.release();
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
|
|
779
|
+
// or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
|
|
780
|
+
// the missing authoritative signal. Rebuild once through the same sanitation path used by a
|
|
781
|
+
// known route switch; invalidating is mandatory because `parsed` mutates in place and the
|
|
782
|
+
// same-target cache would otherwise replay the rejected bytes verbatim.
|
|
783
|
+
const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
|
|
784
|
+
response: upstreamResponse,
|
|
785
|
+
outboundBody: transportState.sameTargetRequest?.body,
|
|
786
|
+
adapterName: transportState.activeAdapter.name,
|
|
787
|
+
parsed,
|
|
788
|
+
guard: opaqueBlobRecoveryGuard,
|
|
789
|
+
signal: upstream.signal,
|
|
790
|
+
}, recovery => {
|
|
791
|
+
invalidateSameTargetRequest();
|
|
792
|
+
return rebuildAndRefetch(recovery);
|
|
793
|
+
});
|
|
794
|
+
if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
|
|
795
|
+
if (opaqueBlobRecovery.kind === "recovered") {
|
|
796
|
+
upstreamResponse = opaqueBlobRecovery.response;
|
|
797
|
+
continue recovery;
|
|
798
|
+
}
|
|
799
|
+
// Anthropic 413 request_too_large: rebuild once with every image one tier lower
|
|
800
|
+
// (spiral guard: single attempt). The biased response re-enters the 429 check above.
|
|
801
|
+
if (shouldAttemptImageTierRetry({
|
|
802
|
+
status: upstreamResponse.status,
|
|
803
|
+
adapterName: transportState.activeAdapter.name,
|
|
804
|
+
parsed,
|
|
805
|
+
alreadyAttempted: imageRetryAttempted,
|
|
806
|
+
})) {
|
|
807
|
+
imageRetryAttempted = true;
|
|
808
|
+
transportState.imageTierBias = 1;
|
|
809
|
+
invalidateSameTargetRequest();
|
|
810
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
811
|
+
const result = await rebuildAndRefetch("image-413");
|
|
812
|
+
if ("failed" in result) return result.failed;
|
|
813
|
+
upstreamResponse = result;
|
|
814
|
+
continue recovery;
|
|
815
|
+
}
|
|
816
|
+
// Console Go (opencode-zen / opencode-go) intermittently rejects a body it accepts seconds
|
|
817
|
+
// later with 400 invalid_request_error / "Invalid upload request." Replay the
|
|
818
|
+
// byte-identical request once after the exact gateway rejection.
|
|
819
|
+
if (!consoleGoUploadRetryGuard.attempted) {
|
|
820
|
+
const uploadRejectionBody = await consoleGoUploadRejectionBody(
|
|
821
|
+
upstreamResponse,
|
|
822
|
+
consoleGoUploadRetryGuard.attempted,
|
|
823
|
+
upstream.signal,
|
|
824
|
+
);
|
|
825
|
+
if (uploadRejectionBody !== undefined
|
|
826
|
+
&& isTransientConsoleGoUploadRejection({
|
|
827
|
+
status: upstreamResponse.status,
|
|
828
|
+
errorBody: uploadRejectionBody,
|
|
829
|
+
outboundUrl: transportState.sameTargetRequest?.url,
|
|
830
|
+
})) {
|
|
831
|
+
consoleGoUploadRetryGuard.attempted = true;
|
|
832
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
833
|
+
if (!upstream.signal.aborted) {
|
|
834
|
+
try {
|
|
835
|
+
await sleepWithAbort(CONSOLE_GO_UPLOAD_RETRY_DELAY_MS, upstream.signal);
|
|
836
|
+
} catch { cleanupUpstreamAbort(); return clientCancelledResponse(); }
|
|
837
|
+
}
|
|
838
|
+
if (upstream.signal.aborted) { cleanupUpstreamAbort(); return clientCancelledResponse(); }
|
|
839
|
+
const result = await rebuildAndRefetch("console-go-upload-retry");
|
|
840
|
+
if ("failed" in result) return result.failed;
|
|
841
|
+
upstreamResponse = result;
|
|
842
|
+
continue recovery;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// Reasoning-effort downgrade, mirroring the passthroughRecovery loop above: learn the
|
|
846
|
+
// refused rung, then replay once at the next published one.
|
|
847
|
+
if (!reasoningEffortDowngradeGuard.attempted) {
|
|
848
|
+
const rejectionText = await reasoningEffortRejectionText(
|
|
849
|
+
upstreamResponse,
|
|
850
|
+
reasoningEffortDowngradeGuard.attempted,
|
|
851
|
+
upstream.signal,
|
|
852
|
+
);
|
|
853
|
+
const downgrade = rejectionText === undefined
|
|
854
|
+
? undefined
|
|
855
|
+
: planReasoningEffortDowngrade({
|
|
856
|
+
provider: route.provider,
|
|
857
|
+
modelId: parsed.modelId,
|
|
858
|
+
requested: parsed.options.reasoning,
|
|
859
|
+
rejectionText,
|
|
860
|
+
});
|
|
861
|
+
if (downgrade) {
|
|
862
|
+
reasoningEffortDowngradeGuard.attempted = true;
|
|
863
|
+
parsed.options.reasoning = downgrade.effort;
|
|
864
|
+
// The same-target cache keys on parsed identity, so a mutated effort needs a token bump.
|
|
865
|
+
invalidateSameTargetRequest();
|
|
866
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
867
|
+
const result = await rebuildAndRefetch("reasoning-effort-downgrade");
|
|
868
|
+
if ("failed" in result) return result.failed;
|
|
869
|
+
upstreamResponse = result;
|
|
870
|
+
continue recovery;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
if (!upstreamResponse.ok) {
|
|
876
|
+
if (options.comboAttempt) {
|
|
877
|
+
// No pre-read guard: `consumeComboFailure` -> `readBoundedResponseBody` reads
|
|
878
|
+
// `response.body` itself with the abort signal threaded through, and the combo
|
|
879
|
+
// contract is that this body's getter is touched exactly once. A guard here would be
|
|
880
|
+
// a second `.body` access for no gain, since the bounded reader owns settlement.
|
|
881
|
+
const failure = await consumeComboFailure(upstreamResponse, options.abortSignal)
|
|
882
|
+
.finally(cleanupUpstreamAbort);
|
|
883
|
+
options.onConsumedComboFailure?.(failure);
|
|
884
|
+
return failure.response;
|
|
885
|
+
}
|
|
886
|
+
let errorText: string;
|
|
887
|
+
try {
|
|
888
|
+
errorText = await readDisplaySafeErrorText(
|
|
889
|
+
upstreamResponse,
|
|
890
|
+
upstream.signal,
|
|
891
|
+
"unknown error",
|
|
892
|
+
);
|
|
893
|
+
} finally {
|
|
894
|
+
cleanupUpstreamAbort();
|
|
895
|
+
}
|
|
896
|
+
if (upstreamResponse.status === 413) {
|
|
897
|
+
return clientRequestedStream
|
|
898
|
+
? streamingContextOverflowResponse(parsed._responseModelId ?? parsed.modelId, translatorBudget)
|
|
899
|
+
: jsonContextOverflowResponse();
|
|
900
|
+
}
|
|
901
|
+
if (!isFixedCodexAccount(admissionState.authCtx)) {
|
|
902
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
903
|
+
req.headers,
|
|
904
|
+
subagentQuotaFailureModel,
|
|
905
|
+
upstreamResponse.status === 429 || upstreamResponse.status === 402
|
|
906
|
+
? upstreamResponse.status
|
|
907
|
+
: `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
|
|
908
|
+
config,
|
|
909
|
+
requestState.subagentFallbackAccountId,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
// Upstreams occasionally echo request details in error bodies — scrub token-shaped
|
|
913
|
+
// material before it reaches the client-facing error surface.
|
|
914
|
+
const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
|
|
915
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
916
|
+
const message = normalized.cyberPolicy
|
|
917
|
+
? normalized.message
|
|
918
|
+
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
919
|
+
: enrichOpenCodeZenUpstreamMessage(
|
|
920
|
+
`Provider error ${upstreamResponse.status}: ${normalized.safeText}`,
|
|
921
|
+
{
|
|
922
|
+
status: upstreamResponse.status,
|
|
923
|
+
providerName: route.providerName,
|
|
924
|
+
baseUrl: route.provider.baseUrl,
|
|
925
|
+
adapter: route.provider.adapter,
|
|
926
|
+
authMode: route.provider.authMode,
|
|
927
|
+
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
928
|
+
upstreamRetryAfter,
|
|
929
|
+
// This recovery path is the HTTP Responses wire; custom runTurn transports
|
|
930
|
+
// never reach enrichOpenCodeZenUpstreamMessage here.
|
|
931
|
+
supportsHttpSameKeyRetry: true,
|
|
932
|
+
},
|
|
933
|
+
);
|
|
934
|
+
const retryAfter = normalized.cyberPolicy
|
|
935
|
+
? undefined
|
|
936
|
+
: resolveClientRetryAfter({
|
|
937
|
+
status: upstreamResponse.status,
|
|
938
|
+
message,
|
|
939
|
+
upstreamRetryAfter,
|
|
940
|
+
});
|
|
941
|
+
return formatErrorResponse(
|
|
942
|
+
upstreamResponse.status,
|
|
943
|
+
normalized.cyberPolicy ? (normalized.type ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
944
|
+
message,
|
|
945
|
+
{
|
|
946
|
+
...(normalized.cyberPolicy ? { code: CYBER_POLICY_ERROR_CODE } : {}),
|
|
947
|
+
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
948
|
+
},
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
upstream,
|
|
957
|
+
cleanupUpstreamAbort,
|
|
958
|
+
connectMs,
|
|
959
|
+
stallTimeoutMs,
|
|
960
|
+
upstreamResponse,
|
|
961
|
+
rateLimitPolicy,
|
|
962
|
+
get rateLimitRetries(): typeof rateLimitRetries {
|
|
963
|
+
return rateLimitRetries;
|
|
964
|
+
},
|
|
965
|
+
set rateLimitRetries(value: typeof rateLimitRetries) {
|
|
966
|
+
rateLimitRetries = value;
|
|
967
|
+
},
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export type AdapterExchange = Exclude<Awaited<ReturnType<typeof prepareAdapterExchange>>, Response>;
|