@bitkyc08/opencodex 2.55.0-preview.20260914 → 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-DH2PUHqr.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,514 @@
|
|
|
1
|
+
import type { ResponsesRequestContext } from "./core-options";
|
|
2
|
+
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
|
+
import type { ResponsesTransport } from "./request-transport";
|
|
4
|
+
import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
|
|
5
|
+
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
6
|
+
import type { AdapterExchange } from "./adapter-dispatch";
|
|
7
|
+
import type { OcxParsedRequest, AdapterEvent } from "../../types";
|
|
8
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
9
|
+
import type { AdapterRequest } from "../../adapters/base";
|
|
10
|
+
import {
|
|
11
|
+
recordAdapterReasoning,
|
|
12
|
+
recordAdapterTier,
|
|
13
|
+
noteAttemptSend,
|
|
14
|
+
sealRequestAttemptIdentity,
|
|
15
|
+
recordAttemptCredentialSource,
|
|
16
|
+
} from "../request-log";
|
|
17
|
+
import { waitForProviderRequestSlot } from "../../providers/request-pacing";
|
|
18
|
+
import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
|
|
19
|
+
import {
|
|
20
|
+
transientRetryPolicyFor,
|
|
21
|
+
rateLimitRetryDelayMs,
|
|
22
|
+
hasKeyPoolFailover,
|
|
23
|
+
rotateProviderTransportOn429,
|
|
24
|
+
} from "../../providers/key-failover";
|
|
25
|
+
import {
|
|
26
|
+
fetchWithTransientRetry,
|
|
27
|
+
fetchWithResetRetry,
|
|
28
|
+
applyUpstreamRecoveryInit,
|
|
29
|
+
prepareSameTarget429Wait,
|
|
30
|
+
} from "../../lib/upstream-retry";
|
|
31
|
+
import { redactSecretString } from "../../lib/redact";
|
|
32
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
33
|
+
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
34
|
+
import {
|
|
35
|
+
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
36
|
+
rotateAnthropicAccountOn429,
|
|
37
|
+
getAnthropicPoolAccessSnapshot,
|
|
38
|
+
formatAnthropicProviderForLog,
|
|
39
|
+
} from "../../oauth/anthropic-routing";
|
|
40
|
+
import {
|
|
41
|
+
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
42
|
+
isGenericOAuthFailoverEnabled,
|
|
43
|
+
rotateGenericOAuthAccountOn429,
|
|
44
|
+
failoverAccountSnapshot,
|
|
45
|
+
} from "../../oauth/generic-account-failover";
|
|
46
|
+
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
47
|
+
import { readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
|
|
48
|
+
import { isCyberPolicyCode, CYBER_POLICY_FALLBACK_MESSAGE, CYBER_POLICY_ERROR_CODE } from "../../lib/errors";
|
|
49
|
+
import { cancelBodyOnAbort } from "../../lib/abort";
|
|
50
|
+
import { guardTerminalEventStream } from "./terminal-guard";
|
|
51
|
+
|
|
52
|
+
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
53
|
+
export function createAdapterContinuations(
|
|
54
|
+
requestContext: Pick<ResponsesRequestContext, "options" | "logCtx" | "config">,
|
|
55
|
+
requestState: Pick<
|
|
56
|
+
PreparedResponsesRequest,
|
|
57
|
+
| "route"
|
|
58
|
+
| "selectedForwardHeaders"
|
|
59
|
+
| "translatorBudget"
|
|
60
|
+
| "inboundWire"
|
|
61
|
+
| "parsed"
|
|
62
|
+
>,
|
|
63
|
+
transportState: Pick<
|
|
64
|
+
ResponsesTransport,
|
|
65
|
+
| "activeAdapter"
|
|
66
|
+
| "sameTargetRequest"
|
|
67
|
+
| "sameTargetParsed"
|
|
68
|
+
| "sameTargetToken"
|
|
69
|
+
| "transportToken"
|
|
70
|
+
| "imageTierBias"
|
|
71
|
+
| "oauthDispatch"
|
|
72
|
+
| "invalidateSameTargetRequest"
|
|
73
|
+
| "resolveSelectionAdapter"
|
|
74
|
+
| "anthropicPoolAccountId"
|
|
75
|
+
| "anthropicPoolFailovers"
|
|
76
|
+
| "anthropicSessionKey"
|
|
77
|
+
| "commitResolvedOAuthSelection"
|
|
78
|
+
| "genericFailoverAccountId"
|
|
79
|
+
| "genericFailovers"
|
|
80
|
+
| "applyFailoverSnapshot"
|
|
81
|
+
>,
|
|
82
|
+
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
|
|
83
|
+
sendBudgetState: Pick<
|
|
84
|
+
ResponsesSendBudget,
|
|
85
|
+
| "adapterSendBudget"
|
|
86
|
+
| "noteAdapterPhysicalSend"
|
|
87
|
+
| "remainingTransientSendBudget"
|
|
88
|
+
| "noteTransientSends"
|
|
89
|
+
| "reserveCredentialHop"
|
|
90
|
+
>,
|
|
91
|
+
adapterExchange: Pick<
|
|
92
|
+
AdapterExchange,
|
|
93
|
+
| "upstream"
|
|
94
|
+
| "connectMs"
|
|
95
|
+
| "rateLimitPolicy"
|
|
96
|
+
| "rateLimitRetries"
|
|
97
|
+
| "stallTimeoutMs"
|
|
98
|
+
>,
|
|
99
|
+
) {
|
|
100
|
+
const { options, logCtx, config } = requestContext;
|
|
101
|
+
const {
|
|
102
|
+
oauthDispatch,
|
|
103
|
+
invalidateSameTargetRequest,
|
|
104
|
+
resolveSelectionAdapter,
|
|
105
|
+
anthropicSessionKey,
|
|
106
|
+
commitResolvedOAuthSelection,
|
|
107
|
+
applyFailoverSnapshot,
|
|
108
|
+
} = transportState;
|
|
109
|
+
const { route, translatorBudget, inboundWire, parsed } = requestState;
|
|
110
|
+
const { routedCompaction } = sidecarState;
|
|
111
|
+
const { upstream, connectMs, rateLimitPolicy, stallTimeoutMs } = adapterExchange;
|
|
112
|
+
const {
|
|
113
|
+
adapterSendBudget,
|
|
114
|
+
noteAdapterPhysicalSend,
|
|
115
|
+
remainingTransientSendBudget,
|
|
116
|
+
noteTransientSends,
|
|
117
|
+
reserveCredentialHop,
|
|
118
|
+
} = sendBudgetState;
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// One bounded internal continuation re-ask for clean end_turn turns that announced an edit
|
|
122
|
+
// without emitting a tool call. Anthropic gets this by default; openai-chat providers opt in
|
|
123
|
+
// per-provider via `terminalContinuationGuard` (the heuristic was tuned on Anthropic turns,
|
|
124
|
+
// so it stays off for the shared openai-chat adapter unless a provider enables it).
|
|
125
|
+
const terminalGuardEnabled = (transportState.activeAdapter.name === "anthropic"
|
|
126
|
+
|| (transportState.activeAdapter.name === "openai-chat" && route.provider.terminalContinuationGuard === true))
|
|
127
|
+
&& !options.comboAttempt && !routedCompaction;
|
|
128
|
+
/**
|
|
129
|
+
* One bounded internal re-ask for Anthropic end_turn-without-tool-call turns. Replays the
|
|
130
|
+
* continuation on a 429 with the same-key retry budget (hoisted per request), then falls
|
|
131
|
+
* back to key/account failover; a failure becomes an in-stream adapter error so the client
|
|
132
|
+
* never sees a second hidden HTTP response or an unbounded retry loop.
|
|
133
|
+
*/
|
|
134
|
+
const fetchTerminalGuardContinuation = async function* (
|
|
135
|
+
nextParsed: OcxParsedRequest,
|
|
136
|
+
initialRecoveryKind?: AttemptRecoveryKind,
|
|
137
|
+
): AsyncGenerator<AdapterEvent> {
|
|
138
|
+
let response: Response | undefined;
|
|
139
|
+
// One-shot recovery label for the next top-of-loop continuation send after a failover rotation.
|
|
140
|
+
let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined = initialRecoveryKind;
|
|
141
|
+
/**
|
|
142
|
+
* Build and fetch one terminal-guard continuation. `recoveryKind` tags same-target and
|
|
143
|
+
* failover sends (`empty-completion`, `rate-limit-429`, `key-429`,
|
|
144
|
+
* `anthropic-oauth-429`, `image-413`); the
|
|
145
|
+
* adapter rebuild is deterministic for the same parsed request (tests assert byte-identical
|
|
146
|
+
* replays).
|
|
147
|
+
*/
|
|
148
|
+
const fetchContinuation = async (recoveryKind?: AttemptRecoveryKind): Promise<Response> => {
|
|
149
|
+
let continuationRequest: AdapterRequest | undefined;
|
|
150
|
+
if (transportState.sameTargetRequest !== undefined && transportState.sameTargetParsed === nextParsed && transportState.sameTargetToken === transportState.transportToken) {
|
|
151
|
+
// Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
|
|
152
|
+
continuationRequest = transportState.sameTargetRequest;
|
|
153
|
+
} else {
|
|
154
|
+
try {
|
|
155
|
+
continuationRequest = await transportState.activeAdapter.buildRequest(nextParsed, {
|
|
156
|
+
headers: requestState.selectedForwardHeaders,
|
|
157
|
+
translatorBudget,
|
|
158
|
+
...(transportState.imageTierBias > 0 ? { imageTierBias: transportState.imageTierBias } : {}),
|
|
159
|
+
});
|
|
160
|
+
recordAdapterReasoning(logCtx, continuationRequest);
|
|
161
|
+
recordAdapterTier(logCtx, continuationRequest);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
// The main body is already streaming, so there is no HTTP error surface: release
|
|
164
|
+
// any partial body observation and surface the failure as an in-stream error via
|
|
165
|
+
// the outer catch (no upstream.abort() — that would kill the live body stream).
|
|
166
|
+
continuationRequest?.releaseBodyObservation?.();
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
transportState.sameTargetRequest = continuationRequest;
|
|
170
|
+
transportState.sameTargetParsed = nextParsed;
|
|
171
|
+
transportState.sameTargetToken = transportState.transportToken;
|
|
172
|
+
}
|
|
173
|
+
// Both branches assign the request (the build catch rethrows), so capture it in a
|
|
174
|
+
// const for the fetch callback and finally below — a `let` read inside a nested
|
|
175
|
+
// function keeps its undefined half, which would break the byte-identical replay.
|
|
176
|
+
const builtContinuationRequest = continuationRequest;
|
|
177
|
+
const continuationEstimate = typeof builtContinuationRequest.usageLog?.inputTokens === "number"
|
|
178
|
+
? builtContinuationRequest.usageLog.inputTokens
|
|
179
|
+
: undefined;
|
|
180
|
+
if (continuationEstimate !== undefined) logCtx.usageLogInputTokens = continuationEstimate;
|
|
181
|
+
// Optional recovery label for same-target / failover continuation sends.
|
|
182
|
+
const replayKind: AttemptRecoveryKind | undefined = recoveryKind;
|
|
183
|
+
try {
|
|
184
|
+
if (transportState.activeAdapter.fetchResponse) {
|
|
185
|
+
noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replayKind);
|
|
186
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal);
|
|
187
|
+
return await transportState.activeAdapter.fetchResponse(builtContinuationRequest, {
|
|
188
|
+
abortSignal: upstream.signal,
|
|
189
|
+
timeoutMs: connectMs,
|
|
190
|
+
sendBudget: adapterSendBudget,
|
|
191
|
+
onPhysicalSend: send => noteAdapterPhysicalSend(continuationEstimate, send),
|
|
192
|
+
stream: nextParsed.stream,
|
|
193
|
+
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
194
|
+
dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
|
|
195
|
+
providerName: route.providerName,
|
|
196
|
+
modelId: nextParsed.modelId,
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Same #1851 scope guard as the initial send: transient-5xx retry only for direct
|
|
201
|
+
// Google AI Studio; every other adapter keeps reset-only semantics here.
|
|
202
|
+
const continuationTransientPolicy = transientRetryPolicyFor(route.provider);
|
|
203
|
+
const fetchContinuationWithRetryPolicy = (route.provider.adapter === "google" || continuationTransientPolicy)
|
|
204
|
+
? fetchWithTransientRetry
|
|
205
|
+
: fetchWithResetRetry;
|
|
206
|
+
return await fetchContinuationWithRetryPolicy(
|
|
207
|
+
recovery => {
|
|
208
|
+
noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind);
|
|
209
|
+
return fetchWithHeaderTimeout(
|
|
210
|
+
builtContinuationRequest.url,
|
|
211
|
+
applyUpstreamRecoveryInit({
|
|
212
|
+
method: builtContinuationRequest.method,
|
|
213
|
+
headers: builtContinuationRequest.headers,
|
|
214
|
+
body: builtContinuationRequest.body,
|
|
215
|
+
}, recovery),
|
|
216
|
+
upstream.signal,
|
|
217
|
+
connectMs,
|
|
218
|
+
nextParsed.stream,
|
|
219
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
220
|
+
dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
|
|
221
|
+
providerName: route.providerName,
|
|
222
|
+
modelId: nextParsed.modelId,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
abortSignal: upstream.signal,
|
|
228
|
+
label: safeHostLabel(builtContinuationRequest.url),
|
|
229
|
+
// Same request-scoped budget as the initial send and the 429/rotation refetches:
|
|
230
|
+
// a terminal-guard continuation is another leg of ONE request, so handing it a
|
|
231
|
+
// fresh `attempts` would let one request exceed the configured total-send ceiling.
|
|
232
|
+
...(continuationTransientPolicy
|
|
233
|
+
? {
|
|
234
|
+
attempts: remainingTransientSendBudget(continuationTransientPolicy.attempts),
|
|
235
|
+
onSendsConsumed: noteTransientSends,
|
|
236
|
+
}
|
|
237
|
+
: {}),
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
} finally {
|
|
241
|
+
builtContinuationRequest.releaseBodyObservation?.();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
while (true) {
|
|
245
|
+
try {
|
|
246
|
+
const recoveryKind = nextContinuationRecoveryKind;
|
|
247
|
+
nextContinuationRecoveryKind = undefined;
|
|
248
|
+
response = await fetchContinuation(recoveryKind);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (options.abortSignal?.aborted || upstream.signal.aborted) {
|
|
251
|
+
yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
|
|
252
|
+
} else {
|
|
253
|
+
yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Same-target 429 wait-and-retry (opt-in `retryOn429`) before key/account failover:
|
|
259
|
+
// a primary-key rate-limit blip replays on the SAME key, matching the main recovery
|
|
260
|
+
// loop; only after the attempts are exhausted does the continuation fail over.
|
|
261
|
+
while (
|
|
262
|
+
response.status === 429
|
|
263
|
+
&& rateLimitPolicy !== null
|
|
264
|
+
&& adapterExchange.rateLimitRetries < rateLimitPolicy.attempts
|
|
265
|
+
) {
|
|
266
|
+
adapterExchange.rateLimitRetries += 1;
|
|
267
|
+
// Release unread body + heartbeat-fed wait via the shared same-target helper.
|
|
268
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
269
|
+
try {
|
|
270
|
+
yield* prepareSameTarget429Wait({
|
|
271
|
+
body: response.body,
|
|
272
|
+
// Listen on the upstream signal: once the SSE body is being streamed, a client
|
|
273
|
+
// cancel aborts `upstream` through the bridge, and upstream is also linked from
|
|
274
|
+
// options.abortSignal — so this covers both cancellation paths.
|
|
275
|
+
signal: upstream.signal,
|
|
276
|
+
delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
|
|
277
|
+
heartbeatIntervalMs: Math.min(10_000, Math.max(250, stallTimeoutMs / 2)),
|
|
278
|
+
});
|
|
279
|
+
} catch {
|
|
280
|
+
if (options.abortSignal?.aborted || upstream.signal.aborted) {
|
|
281
|
+
yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
|
|
282
|
+
} else {
|
|
283
|
+
yield { type: "error", message: "Provider continuation failed: retry wait interrupted" };
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Client cancellation wins over any stale timer edge: re-check before dispatching the
|
|
288
|
+
// replay so the continuation never starts work for a request the client abandoned.
|
|
289
|
+
if (options.abortSignal?.aborted || upstream.signal.aborted) {
|
|
290
|
+
yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
response = await fetchContinuation("rate-limit-429");
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (options.abortSignal?.aborted || upstream.signal.aborted) {
|
|
297
|
+
yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
|
|
298
|
+
} else {
|
|
299
|
+
yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (response.status === 429 && hasKeyPoolFailover(route.provider)) {
|
|
306
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
307
|
+
retryAfter: response.headers.get("retry-after"),
|
|
308
|
+
now: Date.now(),
|
|
309
|
+
attemptedKey: route.provider.apiKey,
|
|
310
|
+
promptCacheKey: nextParsed.options.promptCacheKey,
|
|
311
|
+
});
|
|
312
|
+
if (rotated) {
|
|
313
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
314
|
+
route.provider = rotated;
|
|
315
|
+
invalidateSameTargetRequest();
|
|
316
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
317
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
318
|
+
config.cacheRetention,
|
|
319
|
+
);
|
|
320
|
+
bindRouteReasoningReplayScope({
|
|
321
|
+
parsed: nextParsed,
|
|
322
|
+
providerName: route.providerName,
|
|
323
|
+
provider: route.provider,
|
|
324
|
+
adapterName: transportState.activeAdapter.name,
|
|
325
|
+
});
|
|
326
|
+
// Response persistence closes over the outer parsed request; keep its owner binding in
|
|
327
|
+
// sync with the terminal-guard clone that builds the rotated continuation request.
|
|
328
|
+
bindRouteReasoningReplayScope({
|
|
329
|
+
parsed,
|
|
330
|
+
providerName: route.providerName,
|
|
331
|
+
provider: route.provider,
|
|
332
|
+
adapterName: transportState.activeAdapter.name,
|
|
333
|
+
});
|
|
334
|
+
nextContinuationRecoveryKind = "key-429";
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
response.status === 429
|
|
340
|
+
&& transportState.anthropicPoolAccountId
|
|
341
|
+
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
342
|
+
) {
|
|
343
|
+
const nextAccountId = rotateAnthropicAccountOn429(
|
|
344
|
+
config,
|
|
345
|
+
transportState.anthropicPoolAccountId,
|
|
346
|
+
response.headers.get("retry-after"),
|
|
347
|
+
anthropicSessionKey,
|
|
348
|
+
Date.now(),
|
|
349
|
+
response.headers,
|
|
350
|
+
);
|
|
351
|
+
if (nextAccountId) {
|
|
352
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
353
|
+
try {
|
|
354
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
355
|
+
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
356
|
+
transportState.anthropicPoolAccountId = admitted.accountId;
|
|
357
|
+
transportState.anthropicPoolFailovers += 1;
|
|
358
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
359
|
+
invalidateSameTargetRequest();
|
|
360
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
361
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
362
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
363
|
+
config.cacheRetention,
|
|
364
|
+
);
|
|
365
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
366
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
367
|
+
nextContinuationRecoveryKind = "anthropic-oauth-429";
|
|
368
|
+
continue;
|
|
369
|
+
} catch {
|
|
370
|
+
// fall through to emit continuation error below
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Generic OAuth rotation for the continuation loop. The streaming loop grew this arm with
|
|
375
|
+
// #2568 and this one did not, so an xAI/Cursor/Kimi/Copilot/Antigravity/Nous continuation
|
|
376
|
+
// 429 stayed terminal even with failover fully active -- the same class of divergence the
|
|
377
|
+
// two sidecars already produced once. Request-local state is shared with the other arms so
|
|
378
|
+
// the per-request bound cannot be silently re-armed by reaching a different loop.
|
|
379
|
+
if (
|
|
380
|
+
response.status === 429
|
|
381
|
+
&& transportState.genericFailoverAccountId
|
|
382
|
+
&& transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
383
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
384
|
+
) {
|
|
385
|
+
// Intersection with the shared request budget. The continuation loop re-sends the
|
|
386
|
+
// turn, so without this the per-request bound could be re-armed simply by reaching a
|
|
387
|
+
// different loop -- which is the divergence the comment above already warns about.
|
|
388
|
+
const hop = reserveCredentialHop(
|
|
389
|
+
"auth-recovery",
|
|
390
|
+
`${route.providerName}|${route.modelId}|continuation-oauth-429`,
|
|
391
|
+
);
|
|
392
|
+
const nextAccountId = hop.allowed
|
|
393
|
+
? rotateGenericOAuthAccountOn429(
|
|
394
|
+
config,
|
|
395
|
+
route.providerName,
|
|
396
|
+
transportState.genericFailoverAccountId,
|
|
397
|
+
response.headers.get("retry-after"),
|
|
398
|
+
)
|
|
399
|
+
: null;
|
|
400
|
+
if (!nextAccountId) hop.permit?.release();
|
|
401
|
+
if (nextAccountId) {
|
|
402
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
403
|
+
try {
|
|
404
|
+
// The FULL snapshot through the shared helper, never a bare bearer: Antigravity
|
|
405
|
+
// pairs an account-matched projectId with its token and Kiro carries routing
|
|
406
|
+
// metadata, so a token-only swap would mix one account's credential with another's
|
|
407
|
+
// routing data.
|
|
408
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
409
|
+
transportState.genericFailovers += 1;
|
|
410
|
+
const applied = await applyFailoverSnapshot(snapshot, nextParsed);
|
|
411
|
+
if (!applied) hop.permit?.release();
|
|
412
|
+
if (applied) {
|
|
413
|
+
invalidateSameTargetRequest();
|
|
414
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
415
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
416
|
+
config.cacheRetention,
|
|
417
|
+
);
|
|
418
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
419
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
420
|
+
nextContinuationRecoveryKind = "oauth-account-429";
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
// Everything in this try runs before the replay: the send happens on the next
|
|
425
|
+
// iteration, after `continue`. A throw here therefore leaves a reservation that
|
|
426
|
+
// never dispatched, and holding it would refuse a later recovery in this same
|
|
427
|
+
// request for a send that never left the process.
|
|
428
|
+
hop.permit?.release();
|
|
429
|
+
// fall through to emit continuation error below
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (shouldAttemptImageTierRetry({
|
|
434
|
+
status: response.status,
|
|
435
|
+
adapterName: transportState.activeAdapter.name,
|
|
436
|
+
parsed: nextParsed,
|
|
437
|
+
alreadyAttempted: transportState.imageTierBias > 0,
|
|
438
|
+
})) {
|
|
439
|
+
transportState.imageTierBias = 1;
|
|
440
|
+
invalidateSameTargetRequest();
|
|
441
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
442
|
+
nextContinuationRecoveryKind = "image-413";
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!response.ok) {
|
|
449
|
+
const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
|
|
450
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
451
|
+
yield {
|
|
452
|
+
type: "error",
|
|
453
|
+
status: normalized.cyberPolicy ? 400 : response.status,
|
|
454
|
+
message: normalized.cyberPolicy
|
|
455
|
+
? normalized.message
|
|
456
|
+
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
457
|
+
: `Provider continuation error ${response.status}: ${normalized.safeText}`,
|
|
458
|
+
...(normalized.cyberPolicy
|
|
459
|
+
? {
|
|
460
|
+
errorType: normalized.type ?? CYBER_POLICY_ERROR_CODE,
|
|
461
|
+
code: CYBER_POLICY_ERROR_CODE,
|
|
462
|
+
retryable: false,
|
|
463
|
+
}
|
|
464
|
+
: {}),
|
|
465
|
+
};
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
// Protect the continuation body against a client abort landing between fetch resolution and
|
|
471
|
+
// reader attach, exactly as the initial response is guarded above (#390/366e3053). Without
|
|
472
|
+
// this, a client cancel during the continuation reopens the Bun fetch-to-reader abort race.
|
|
473
|
+
const detachContinuationBodyGuard = cancelBodyOnAbort(response.body, upstream.signal);
|
|
474
|
+
try {
|
|
475
|
+
if (nextParsed.stream) {
|
|
476
|
+
yield* transportState.activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata);
|
|
477
|
+
} else if (transportState.activeAdapter.parseResponse) {
|
|
478
|
+
yield* await transportState.activeAdapter.parseResponse(response, translatorBudget, logCtx.activeTierMetadata);
|
|
479
|
+
} else {
|
|
480
|
+
yield { type: "error", message: "Provider continuation does not support response parsing" };
|
|
481
|
+
}
|
|
482
|
+
} finally {
|
|
483
|
+
detachContinuationBodyGuard();
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (options.abortSignal?.aborted) {
|
|
487
|
+
yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
|
|
488
|
+
} else {
|
|
489
|
+
yield { type: "error", message: `Provider continuation parse failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const fetchGuardedEmptyCompletionRetry = (): AsyncIterable<AdapterEvent> => {
|
|
495
|
+
const retryEvents = fetchTerminalGuardContinuation(parsed, "empty-completion");
|
|
496
|
+
return terminalGuardEnabled
|
|
497
|
+
? guardTerminalEventStream({
|
|
498
|
+
parsed,
|
|
499
|
+
firstEvents: retryEvents,
|
|
500
|
+
adapterName: transportState.activeAdapter.name,
|
|
501
|
+
maxAutoContinuations: 1,
|
|
502
|
+
continuation: fetchTerminalGuardContinuation,
|
|
503
|
+
})
|
|
504
|
+
: retryEvents;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
terminalGuardEnabled,
|
|
509
|
+
fetchTerminalGuardContinuation,
|
|
510
|
+
fetchGuardedEmptyCompletionRetry,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export type AdapterContinuations = Exclude<ReturnType<typeof createAdapterContinuations>, Response>;
|