@bitkyc08/opencodex 2.55.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,540 @@
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
+ sealRequestAttemptIdentity,
14
+ recordAttemptCredentialSource,
15
+ } from "../request-log";
16
+ import { waitForProviderRequestSlot } from "../../providers/request-pacing";
17
+ import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
18
+ import {
19
+ transientRetryPolicyFor,
20
+ rateLimitRetryDelayMs,
21
+ hasKeyPoolFailover,
22
+ rotateProviderTransportOn429,
23
+ } from "../../providers/key-failover";
24
+ import {
25
+ fetchWithTransientRetry,
26
+ fetchWithResetRetry,
27
+ applyUpstreamRecoveryInit,
28
+ isNonReplayableResponse,
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
+ | "noteRoutedAttemptSend"
82
+ >,
83
+ sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
84
+ sendBudgetState: Pick<
85
+ ResponsesSendBudget,
86
+ | "adapterDispatchBudget"
87
+ | "noteAdapterPhysicalSend"
88
+ | "remainingTransientSendBudget"
89
+ | "noteTransientSends"
90
+ | "reserveCredentialHop"
91
+ | "pendingHopPermit"
92
+ | "sendBudgetExhausted"
93
+ >,
94
+ adapterExchange: Pick<
95
+ AdapterExchange,
96
+ | "upstream"
97
+ | "connectMs"
98
+ | "rateLimitPolicy"
99
+ | "rateLimitRetries"
100
+ | "stallTimeoutMs"
101
+ >,
102
+ ) {
103
+ const { options, logCtx, config } = requestContext;
104
+ const {
105
+ oauthDispatch,
106
+ invalidateSameTargetRequest,
107
+ resolveSelectionAdapter,
108
+ anthropicSessionKey,
109
+ commitResolvedOAuthSelection,
110
+ applyFailoverSnapshot,
111
+ } = transportState;
112
+ const { route, translatorBudget, inboundWire, parsed } = requestState;
113
+ const { routedCompaction } = sidecarState;
114
+ const { upstream, connectMs, rateLimitPolicy, stallTimeoutMs } = adapterExchange;
115
+ const {
116
+ adapterDispatchBudget,
117
+ noteAdapterPhysicalSend,
118
+ remainingTransientSendBudget,
119
+ noteTransientSends,
120
+ reserveCredentialHop,
121
+ sendBudgetExhausted,
122
+ } = sendBudgetState;
123
+
124
+
125
+ // One bounded internal continuation re-ask for clean end_turn turns that announced an edit
126
+ // without emitting a tool call. Anthropic gets this by default; openai-chat providers opt in
127
+ // per-provider via `terminalContinuationGuard` (the heuristic was tuned on Anthropic turns,
128
+ // so it stays off for the shared openai-chat adapter unless a provider enables it).
129
+ const terminalGuardEnabled = (transportState.activeAdapter.name === "anthropic"
130
+ || (transportState.activeAdapter.name === "openai-chat" && route.provider.terminalContinuationGuard === true))
131
+ && !options.comboAttempt && !routedCompaction;
132
+ /**
133
+ * One bounded internal re-ask for Anthropic end_turn-without-tool-call turns. Replays the
134
+ * continuation on a 429 with the same-key retry budget (hoisted per request), then falls
135
+ * back to key/account failover; a failure becomes an in-stream adapter error so the client
136
+ * never sees a second hidden HTTP response or an unbounded retry loop.
137
+ */
138
+ const fetchTerminalGuardContinuation = async function* (
139
+ nextParsed: OcxParsedRequest,
140
+ initialRecoveryKind?: AttemptRecoveryKind,
141
+ ): AsyncGenerator<AdapterEvent> {
142
+ let response: Response | undefined;
143
+ // One-shot recovery label for the next top-of-loop continuation send after a failover rotation.
144
+ let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined = initialRecoveryKind;
145
+ /**
146
+ * Build and fetch one terminal-guard continuation. `recoveryKind` tags same-target and
147
+ * failover sends (`empty-completion`, `rate-limit-429`, `key-429`,
148
+ * `anthropic-oauth-429`, `image-413`); the
149
+ * adapter rebuild is deterministic for the same parsed request (tests assert byte-identical
150
+ * replays).
151
+ */
152
+ const fetchContinuation = async (recoveryKind?: AttemptRecoveryKind): Promise<Response> => {
153
+ let continuationRequest: AdapterRequest | undefined;
154
+ if (transportState.sameTargetRequest !== undefined && transportState.sameTargetParsed === nextParsed && transportState.sameTargetToken === transportState.transportToken) {
155
+ // Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
156
+ continuationRequest = transportState.sameTargetRequest;
157
+ } else {
158
+ try {
159
+ continuationRequest = await transportState.activeAdapter.buildRequest(nextParsed, {
160
+ headers: requestState.selectedForwardHeaders,
161
+ translatorBudget,
162
+ ...(transportState.imageTierBias > 0 ? { imageTierBias: transportState.imageTierBias } : {}),
163
+ });
164
+ recordAdapterReasoning(logCtx, continuationRequest);
165
+ recordAdapterTier(logCtx, continuationRequest);
166
+ } catch (err) {
167
+ // The main body is already streaming, so there is no HTTP error surface: release
168
+ // any partial body observation and surface the failure as an in-stream error via
169
+ // the outer catch (no upstream.abort() — that would kill the live body stream).
170
+ continuationRequest?.releaseBodyObservation?.();
171
+ throw err;
172
+ }
173
+ transportState.sameTargetRequest = continuationRequest;
174
+ transportState.sameTargetParsed = nextParsed;
175
+ transportState.sameTargetToken = transportState.transportToken;
176
+ }
177
+ // Both branches assign the request (the build catch rethrows), so capture it in a
178
+ // const for the fetch callback and finally below — a `let` read inside a nested
179
+ // function keeps its undefined half, which would break the byte-identical replay.
180
+ const builtContinuationRequest = continuationRequest;
181
+ const continuationEstimate = typeof builtContinuationRequest.usageLog?.inputTokens === "number"
182
+ ? builtContinuationRequest.usageLog.inputTokens
183
+ : undefined;
184
+ if (continuationEstimate !== undefined) logCtx.usageLogInputTokens = continuationEstimate;
185
+ // Optional recovery label for same-target / failover continuation sends.
186
+ const replayKind: AttemptRecoveryKind | undefined = recoveryKind;
187
+ try {
188
+ if (transportState.activeAdapter.fetchResponse) {
189
+ transportState.noteRoutedAttemptSend(continuationEstimate, replayKind);
190
+ await waitForProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal);
191
+ return await transportState.activeAdapter.fetchResponse(builtContinuationRequest, {
192
+ abortSignal: upstream.signal,
193
+ timeoutMs: connectMs,
194
+ sendBudget: adapterDispatchBudget,
195
+ onPhysicalSend: send => noteAdapterPhysicalSend(continuationEstimate, send),
196
+ stream: nextParsed.stream,
197
+ executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
198
+ pacingSlotAcquired: true,
199
+ dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
200
+ providerName: route.providerName,
201
+ modelId: nextParsed.modelId,
202
+ }),
203
+ });
204
+ }
205
+ // Same #1851 scope guard as the initial send: transient-5xx retry only for direct
206
+ // Google AI Studio; every other adapter keeps reset-only semantics here.
207
+ const continuationTransientPolicy = transientRetryPolicyFor(route.provider);
208
+ const fetchContinuationWithRetryPolicy = (route.provider.adapter === "google" || continuationTransientPolicy)
209
+ ? fetchWithTransientRetry
210
+ : fetchWithResetRetry;
211
+ return await fetchContinuationWithRetryPolicy(
212
+ recovery => {
213
+ transportState.noteRoutedAttemptSend(continuationEstimate, recovery ?? replayKind);
214
+ return fetchWithHeaderTimeout(
215
+ builtContinuationRequest.url,
216
+ applyUpstreamRecoveryInit({
217
+ method: builtContinuationRequest.method,
218
+ headers: builtContinuationRequest.headers,
219
+ body: builtContinuationRequest.body,
220
+ }, recovery),
221
+ upstream.signal,
222
+ connectMs,
223
+ nextParsed.stream,
224
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
225
+ dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
226
+ providerName: route.providerName,
227
+ modelId: nextParsed.modelId,
228
+ }),
229
+ );
230
+ },
231
+ {
232
+ abortSignal: upstream.signal,
233
+ label: safeHostLabel(builtContinuationRequest.url),
234
+ // Same request-scoped budget as the initial send and the 429/rotation refetches:
235
+ // a terminal-guard continuation is another leg of ONE request, so handing it a
236
+ // fresh `attempts` would let one request exceed the configured total-send ceiling.
237
+ ...(continuationTransientPolicy
238
+ ? {
239
+ attempts: remainingTransientSendBudget(continuationTransientPolicy.attempts),
240
+ onSendsConsumed: noteTransientSends,
241
+ }
242
+ : {}),
243
+ },
244
+ );
245
+ } finally {
246
+ builtContinuationRequest.releaseBodyObservation?.();
247
+ }
248
+ };
249
+ while (true) {
250
+ try {
251
+ const recoveryKind = nextContinuationRecoveryKind;
252
+ nextContinuationRecoveryKind = undefined;
253
+ response = await fetchContinuation(recoveryKind);
254
+ } catch (error) {
255
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
256
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
257
+ } else {
258
+ yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
259
+ }
260
+ return;
261
+ }
262
+
263
+ // Same-target 429 wait-and-retry (opt-in `retryOn429`) before key/account failover:
264
+ // a primary-key rate-limit blip replays on the SAME key, matching the main recovery
265
+ // loop; only after the attempts are exhausted does the continuation fail over.
266
+ while (
267
+ response.status === 429
268
+ // A synthesized replay refusal is not a rate limit; replaying the continuation on
269
+ // it would re-send a turn whose first send may already have been processed.
270
+ && !isNonReplayableResponse(response)
271
+ && rateLimitPolicy !== null
272
+ && adapterExchange.rateLimitRetries < rateLimitPolicy.attempts
273
+ // The main recovery loop and the passthrough ladder both consult the shared remainder
274
+ // here; this loop did not, so a request whose budget was already spent could still
275
+ // same-key replay on a live stream. Checked BEFORE the wait below cancels the body, so
276
+ // a refusal keeps the real upstream 429 -- status, Retry-After, quota evidence -- intact.
277
+ && !sendBudgetExhausted()
278
+ ) {
279
+ adapterExchange.rateLimitRetries += 1;
280
+ // Release unread body + heartbeat-fed wait via the shared same-target helper.
281
+ const retryAfterHeader = response.headers.get("retry-after");
282
+ try {
283
+ yield* prepareSameTarget429Wait({
284
+ body: response.body,
285
+ // Listen on the upstream signal: once the SSE body is being streamed, a client
286
+ // cancel aborts `upstream` through the bridge, and upstream is also linked from
287
+ // options.abortSignal — so this covers both cancellation paths.
288
+ signal: upstream.signal,
289
+ delayMs: rateLimitRetryDelayMs(rateLimitPolicy, retryAfterHeader, Date.now()),
290
+ heartbeatIntervalMs: Math.min(10_000, Math.max(250, stallTimeoutMs / 2)),
291
+ });
292
+ } catch {
293
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
294
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
295
+ } else {
296
+ yield { type: "error", message: "Provider continuation failed: retry wait interrupted" };
297
+ }
298
+ return;
299
+ }
300
+ // Client cancellation wins over any stale timer edge: re-check before dispatching the
301
+ // replay so the continuation never starts work for a request the client abandoned.
302
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
303
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
304
+ return;
305
+ }
306
+ try {
307
+ response = await fetchContinuation("rate-limit-429");
308
+ } catch (error) {
309
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
310
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
311
+ } else {
312
+ yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
313
+ }
314
+ return;
315
+ }
316
+ }
317
+
318
+ if (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(route.provider)) {
319
+ const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
320
+ retryAfter: response.headers.get("retry-after"),
321
+ now: Date.now(),
322
+ attemptedKey: route.provider.apiKey,
323
+ promptCacheKey: nextParsed.options.promptCacheKey,
324
+ });
325
+ if (rotated) {
326
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
327
+ route.provider = rotated;
328
+ invalidateSameTargetRequest();
329
+ transportState.activeAdapter = resolveSelectionAdapter(
330
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
331
+ config.cacheRetention,
332
+ );
333
+ bindRouteReasoningReplayScope({
334
+ parsed: nextParsed,
335
+ providerName: route.providerName,
336
+ provider: route.provider,
337
+ adapterName: transportState.activeAdapter.name,
338
+ });
339
+ // Response persistence closes over the outer parsed request; keep its owner binding in
340
+ // sync with the terminal-guard clone that builds the rotated continuation request.
341
+ bindRouteReasoningReplayScope({
342
+ parsed,
343
+ providerName: route.providerName,
344
+ provider: route.provider,
345
+ adapterName: transportState.activeAdapter.name,
346
+ });
347
+ nextContinuationRecoveryKind = "key-429";
348
+ continue;
349
+ }
350
+ }
351
+ if (
352
+ response.status === 429
353
+ && !isNonReplayableResponse(response)
354
+ && transportState.anthropicPoolAccountId
355
+ && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
356
+ ) {
357
+ const nextAccountId = rotateAnthropicAccountOn429(
358
+ config,
359
+ transportState.anthropicPoolAccountId,
360
+ response.headers.get("retry-after"),
361
+ anthropicSessionKey,
362
+ Date.now(),
363
+ response.headers,
364
+ );
365
+ if (nextAccountId) {
366
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
367
+ try {
368
+ const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
369
+ if (!admitted) throw new Error("OAuth selection changed during recovery");
370
+ transportState.anthropicPoolAccountId = admitted.accountId;
371
+ transportState.anthropicPoolFailovers += 1;
372
+ route.provider = { ...route.provider, apiKey: admitted.accessToken };
373
+ invalidateSameTargetRequest();
374
+ logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
375
+ transportState.activeAdapter = resolveSelectionAdapter(
376
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
377
+ config.cacheRetention,
378
+ );
379
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
380
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
381
+ nextContinuationRecoveryKind = "anthropic-oauth-429";
382
+ continue;
383
+ } catch {
384
+ // fall through to emit continuation error below
385
+ }
386
+ }
387
+ }
388
+ // Generic OAuth rotation for the continuation loop. The streaming loop grew this arm with
389
+ // #2568 and this one did not, so an xAI/Cursor/Kimi/Copilot/Antigravity/Nous continuation
390
+ // 429 stayed terminal even with failover fully active -- the same class of divergence the
391
+ // two sidecars already produced once. Request-local state is shared with the other arms so
392
+ // the per-request bound cannot be silently re-armed by reaching a different loop.
393
+ if (
394
+ response.status === 429
395
+ && !isNonReplayableResponse(response)
396
+ && transportState.genericFailoverAccountId
397
+ && transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
398
+ && isGenericOAuthFailoverEnabled(config, route.providerName)
399
+ ) {
400
+ // Intersection with the shared request budget. The continuation loop re-sends the
401
+ // turn, so without this the per-request bound could be re-armed simply by reaching a
402
+ // different loop -- which is the divergence the comment above already warns about.
403
+ //
404
+ // Who settles the reservation depends on who sends the replay (#4709). An adapter that
405
+ // owns its ladder reserves once per physical send and would charge this replay twice;
406
+ // the helper path reports it back instead, which is what `countedExternally` names.
407
+ const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
408
+ const hop = reserveCredentialHop(
409
+ "auth-recovery",
410
+ `${route.providerName}|${route.modelId}|continuation-oauth-429`,
411
+ !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
412
+ );
413
+ const nextAccountId = hop.allowed
414
+ ? rotateGenericOAuthAccountOn429(
415
+ config,
416
+ route.providerName,
417
+ transportState.genericFailoverAccountId,
418
+ response.headers.get("retry-after"),
419
+ )
420
+ : null;
421
+ if (!nextAccountId) hop.permit?.release();
422
+ if (nextAccountId) {
423
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
424
+ try {
425
+ // The FULL snapshot through the shared helper, never a bare bearer: Antigravity
426
+ // pairs an account-matched projectId with its token and Kiro carries routing
427
+ // metadata, so a token-only swap would mix one account's credential with another's
428
+ // routing data.
429
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
430
+ transportState.genericFailovers += 1;
431
+ const applied = await applyFailoverSnapshot(snapshot, nextParsed);
432
+ if (!applied) hop.permit?.release();
433
+ if (applied) {
434
+ invalidateSameTargetRequest();
435
+ transportState.activeAdapter = resolveSelectionAdapter(
436
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
437
+ config.cacheRetention,
438
+ );
439
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
440
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
441
+ // The replay goes out on the next iteration. An adapter that owns its ladder
442
+ // reserves for that send itself, so hand this reservation down rather than let it
443
+ // take a second one for the same replay. A helper-routed replay needs no handoff:
444
+ // its reporter settles the booking made above.
445
+ if (adapterOwnsDispatch) sendBudgetState.pendingHopPermit = hop.permit;
446
+ nextContinuationRecoveryKind = "oauth-account-429";
447
+ continue;
448
+ }
449
+ } catch {
450
+ // Everything in this try runs before the replay: the send happens on the next
451
+ // iteration, after `continue`. A throw here therefore leaves a reservation that
452
+ // never dispatched, and holding it would refuse a later recovery in this same
453
+ // request for a send that never left the process.
454
+ hop.permit?.release();
455
+ // fall through to emit continuation error below
456
+ }
457
+ }
458
+ }
459
+ if (shouldAttemptImageTierRetry({
460
+ status: response.status,
461
+ adapterName: transportState.activeAdapter.name,
462
+ parsed: nextParsed,
463
+ alreadyAttempted: transportState.imageTierBias > 0,
464
+ })) {
465
+ transportState.imageTierBias = 1;
466
+ invalidateSameTargetRequest();
467
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
468
+ nextContinuationRecoveryKind = "image-413";
469
+ continue;
470
+ }
471
+ break;
472
+ }
473
+
474
+ if (!response.ok) {
475
+ const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
476
+ const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
477
+ yield {
478
+ type: "error",
479
+ status: normalized.cyberPolicy ? 400 : response.status,
480
+ message: normalized.cyberPolicy
481
+ ? normalized.message
482
+ ?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
483
+ : `Provider continuation error ${response.status}: ${normalized.safeText}`,
484
+ ...(normalized.cyberPolicy
485
+ ? {
486
+ errorType: normalized.type ?? CYBER_POLICY_ERROR_CODE,
487
+ code: CYBER_POLICY_ERROR_CODE,
488
+ retryable: false,
489
+ }
490
+ : {}),
491
+ };
492
+ return;
493
+ }
494
+
495
+ try {
496
+ // Protect the continuation body against a client abort landing between fetch resolution and
497
+ // reader attach, exactly as the initial response is guarded above (#390/366e3053). Without
498
+ // this, a client cancel during the continuation reopens the Bun fetch-to-reader abort race.
499
+ const detachContinuationBodyGuard = cancelBodyOnAbort(response.body, upstream.signal);
500
+ try {
501
+ if (nextParsed.stream) {
502
+ yield* transportState.activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata);
503
+ } else if (transportState.activeAdapter.parseResponse) {
504
+ yield* await transportState.activeAdapter.parseResponse(response, translatorBudget, logCtx.activeTierMetadata);
505
+ } else {
506
+ yield { type: "error", message: "Provider continuation does not support response parsing" };
507
+ }
508
+ } finally {
509
+ detachContinuationBodyGuard();
510
+ }
511
+ } catch (error) {
512
+ if (options.abortSignal?.aborted) {
513
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
514
+ } else {
515
+ yield { type: "error", message: `Provider continuation parse failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
516
+ }
517
+ }
518
+ };
519
+
520
+ const fetchGuardedEmptyCompletionRetry = (): AsyncIterable<AdapterEvent> => {
521
+ const retryEvents = fetchTerminalGuardContinuation(parsed, "empty-completion");
522
+ return terminalGuardEnabled
523
+ ? guardTerminalEventStream({
524
+ parsed,
525
+ firstEvents: retryEvents,
526
+ adapterName: transportState.activeAdapter.name,
527
+ maxAutoContinuations: 1,
528
+ continuation: fetchTerminalGuardContinuation,
529
+ })
530
+ : retryEvents;
531
+ };
532
+
533
+ return {
534
+ terminalGuardEnabled,
535
+ fetchTerminalGuardContinuation,
536
+ fetchGuardedEmptyCompletionRetry,
537
+ };
538
+ }
539
+
540
+ export type AdapterContinuations = Exclude<ReturnType<typeof createAdapterContinuations>, Response>;