@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.
Files changed (167) hide show
  1. package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,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>;