@bitkyc08/opencodex 2.82.0 → 2.84.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 (266) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/cursor/catalog.ts +2 -2
  10. package/src/adapters/cursor/effort-map.ts +1 -1
  11. package/src/adapters/devin/live-models.ts +2 -2
  12. package/src/adapters/google-truncation.ts +22 -0
  13. package/src/adapters/google.ts +7 -3
  14. package/src/adapters/ollama-native.ts +47 -15
  15. package/src/adapters/openai-chat/messages.ts +8 -4
  16. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  17. package/src/adapters/openai-responses/passthrough.ts +5 -0
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/request-strips.ts +10 -2
  21. package/src/adapters/qoder/adapter.ts +16 -14
  22. package/src/bridge/errors.ts +26 -1
  23. package/src/bridge.ts +1 -1
  24. package/src/claude/context-windows.ts +57 -4
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/intercept/connect-proxy.ts +10 -3
  27. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  28. package/src/claude/intercept/picker-listener.ts +49 -1
  29. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  30. package/src/claude/intercept/settings.ts +101 -9
  31. package/src/cli/account-api.ts +65 -0
  32. package/src/cli/account-auth.ts +44 -12
  33. package/src/cli/account.ts +41 -5
  34. package/src/cli/capabilities-access-remote.ts +8 -0
  35. package/src/cli/capabilities-base.ts +13 -1
  36. package/src/cli/catalog-command-result.ts +2 -1
  37. package/src/cli/claude.ts +15 -6
  38. package/src/cli/config-command.ts +8 -2
  39. package/src/cli/debug.ts +8 -0
  40. package/src/cli/dispatch.ts +16 -3
  41. package/src/cli/export-command.ts +14 -1
  42. package/src/cli/hub-disable.ts +99 -0
  43. package/src/cli/hub.ts +4 -2
  44. package/src/cli/index.ts +13 -4
  45. package/src/cli/link-child.ts +104 -0
  46. package/src/cli/link-enrollment.ts +96 -0
  47. package/src/cli/link.ts +39 -24
  48. package/src/cli/observe-stream.ts +6 -1
  49. package/src/cli/observe.ts +10 -2
  50. package/src/cli/provider.ts +12 -0
  51. package/src/cli/registry.ts +5 -4
  52. package/src/cli/resolve.ts +1 -1
  53. package/src/cli/runtime-api.ts +27 -2
  54. package/src/cli/status.ts +104 -20
  55. package/src/client/connect.ts +39 -4
  56. package/src/client/hub-client.ts +14 -7
  57. package/src/client/hub-state.ts +25 -14
  58. package/src/client/link-home.ts +248 -0
  59. package/src/client/link-management-relay.ts +61 -0
  60. package/src/client/link-relay.ts +6 -1
  61. package/src/client/link-status.ts +14 -0
  62. package/src/client/machine-api.ts +14 -7
  63. package/src/client/machine-listener.ts +29 -4
  64. package/src/clients/config-export/model-metadata.ts +16 -0
  65. package/src/clients/config-export/zcode-store.ts +46 -11
  66. package/src/clients/config-export/zcode.ts +3 -5
  67. package/src/clients/config-export.ts +47 -17
  68. package/src/clients/omo-role-models.ts +6 -3
  69. package/src/codex/agent-role-models.ts +26 -8
  70. package/src/codex/auth-api/account-list.ts +17 -2
  71. package/src/codex/auth-api/account-selection.ts +17 -7
  72. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  73. package/src/codex/auth-api/routes.ts +5 -1
  74. package/src/codex/auth-context.ts +8 -3
  75. package/src/codex/autostart-health.ts +3 -1
  76. package/src/codex/catalog/bundled.ts +5 -1
  77. package/src/codex/catalog/effort.ts +2 -2
  78. package/src/codex/catalog/metadata.ts +4 -2
  79. package/src/codex/catalog/native-models.ts +17 -3
  80. package/src/codex/catalog/provider-models.ts +25 -0
  81. package/src/codex/desktop-app/darwin.ts +1 -1
  82. package/src/codex/desktop-app/handoff.ts +1 -1
  83. package/src/codex/desktop-app/linux.ts +1 -1
  84. package/src/codex/desktop-app/lock.ts +1 -1
  85. package/src/codex/desktop-app/types.ts +1 -1
  86. package/src/codex/desktop-app/windows.ts +1 -1
  87. package/src/codex/desktop-app-restart.ts +1 -1
  88. package/src/codex/main-account.ts +6 -7
  89. package/src/codex/native-profile-startup.ts +4 -1
  90. package/src/codex/prompt-journal.ts +20 -2
  91. package/src/codex/prompt-layers/encoding.ts +49 -0
  92. package/src/codex/prompt-layers/import-source.ts +30 -0
  93. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  94. package/src/codex/prompt-layers/toml-read.ts +52 -4
  95. package/src/codex/prompt-layers.ts +383 -20
  96. package/src/codex/prompt-lock-claim.ts +13 -3
  97. package/src/codex/prompt-lock-evidence.ts +3 -3
  98. package/src/codex/prompt-lock-owner.ts +14 -6
  99. package/src/codex/prompt-text-probe.ts +34 -5
  100. package/src/codex/quota.ts +13 -6
  101. package/src/codex/role-auto-assign.ts +2 -5
  102. package/src/codex/routing/active-account.ts +21 -0
  103. package/src/codex/routing.ts +1 -1
  104. package/src/codex/runtime.ts +196 -8
  105. package/src/codex/shim-overlay.ts +8 -4
  106. package/src/codex/shim.ts +1 -1
  107. package/src/combos/failover.ts +3 -0
  108. package/src/config/api-key-reconcile.ts +34 -0
  109. package/src/config/diagnostics.ts +16 -0
  110. package/src/config/feature-flags.ts +5 -0
  111. package/src/config/live-reconcile.ts +43 -29
  112. package/src/config/live-topology.ts +29 -0
  113. package/src/config/load-degrade.ts +2 -1
  114. package/src/config/paths.ts +3 -0
  115. package/src/config/salvage.ts +3 -1
  116. package/src/config/schema/config-schema.ts +2 -0
  117. package/src/generated/compatibility-version.json +373 -217
  118. package/src/grok/grpc-web.ts +23 -1
  119. package/src/grok/inject.ts +245 -221
  120. package/src/grok/reset-coupon-ledger.ts +293 -57
  121. package/src/grok/reset-coupons.ts +15 -6
  122. package/src/integrations/cursor-detect.ts +1 -1
  123. package/src/integrations/mutation-plan.ts +13 -7
  124. package/src/integrations/registry.ts +45 -11
  125. package/src/integrations/state.ts +19 -7
  126. package/src/integrations/target.ts +42 -5
  127. package/src/integrations/writer.ts +14 -6
  128. package/src/lib/app-owned-memory-stores.ts +4 -0
  129. package/src/lib/app-owned-memory.ts +11 -0
  130. package/src/lib/errors.ts +25 -0
  131. package/src/lib/json-byte-size.ts +31 -0
  132. package/src/lib/optional-shutdown-hooks.ts +35 -0
  133. package/src/lib/outbound-credential-registry.ts +210 -0
  134. package/src/lib/passive-read.ts +35 -0
  135. package/src/lib/redact.ts +2 -2
  136. package/src/lib/shadow-call.ts +44 -5
  137. package/src/link/codex-settings.ts +155 -0
  138. package/src/link/management-routes.ts +89 -0
  139. package/src/link/remote-probe.ts +38 -0
  140. package/src/link/ssh-argv.ts +26 -5
  141. package/src/link/ssh-runner.ts +58 -27
  142. package/src/link/status-projection.ts +22 -4
  143. package/src/link/supervisor.ts +144 -49
  144. package/src/link/tunnel-state.ts +11 -4
  145. package/src/oauth/anthropic-continuity.ts +4 -3
  146. package/src/oauth/anthropic-identity.ts +13 -2
  147. package/src/oauth/anthropic-model-quota.ts +13 -1
  148. package/src/oauth/anthropic-routing.ts +230 -23
  149. package/src/oauth/anthropic.ts +18 -4
  150. package/src/oauth/google-antigravity.ts +24 -7
  151. package/src/oauth/index.ts +25 -16
  152. package/src/oauth/kiro-account-identity.ts +106 -0
  153. package/src/oauth/kiro.ts +48 -5
  154. package/src/oauth/meta-muse-device.ts +1 -1
  155. package/src/oauth/plan.ts +29 -0
  156. package/src/oauth/store.ts +40 -3
  157. package/src/oauth/types.ts +3 -1
  158. package/src/plugins/loader.ts +11 -0
  159. package/src/providers/anthropic-billing-ownership.ts +74 -0
  160. package/src/providers/anthropic-billing-recovery.ts +31 -0
  161. package/src/providers/anthropic-billing-refusal.ts +55 -0
  162. package/src/providers/anthropic-fast.ts +1 -1
  163. package/src/providers/anthropic-reset-admission.ts +62 -0
  164. package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
  165. package/src/providers/api-key-resolve.ts +17 -2
  166. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  167. package/src/providers/devin-provider-merge-migration.ts +1 -1
  168. package/src/providers/github-copilot-auto.ts +169 -0
  169. package/src/providers/key-failover.ts +52 -5
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/model-rename-startup.ts +5 -3
  172. package/src/providers/muse-subscription-usage.ts +1 -1
  173. package/src/providers/quota/account-cache.ts +11 -0
  174. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  175. package/src/providers/quota/report-cache.ts +14 -0
  176. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  177. package/src/providers/quota.ts +4 -1
  178. package/src/providers/registry/entries-core.ts +20 -20
  179. package/src/providers/registry/entries-extended.ts +4 -1
  180. package/src/providers/registry/model-seeds.ts +20 -3
  181. package/src/providers/request-pacing.ts +9 -0
  182. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  183. package/src/providers/xai-fast-model.ts +1 -1
  184. package/src/responses/compaction.ts +14 -0
  185. package/src/responses/parser.ts +7 -1
  186. package/src/server/auth-cors.ts +42 -0
  187. package/src/server/configured-credentials.ts +194 -0
  188. package/src/server/inbound-body-admission.ts +24 -7
  189. package/src/server/index/link-listener.ts +30 -17
  190. package/src/server/index/link-management.ts +58 -0
  191. package/src/server/index/optional-listeners.ts +5 -1
  192. package/src/server/index/serve-options.ts +21 -2
  193. package/src/server/index.ts +1 -1
  194. package/src/server/lifecycle.ts +33 -5
  195. package/src/server/management/agent-settings-routes.ts +28 -10
  196. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  197. package/src/server/management/codex-agent-role-routes.ts +19 -6
  198. package/src/server/management/codex-prompt-routes.ts +136 -5
  199. package/src/server/management/codex-role-auto-assign.ts +1 -1
  200. package/src/server/management/grok-coupon-routes.ts +333 -57
  201. package/src/server/management/link-enrollment.ts +192 -0
  202. package/src/server/management/link-route-state.ts +185 -0
  203. package/src/server/management/link-routes.ts +92 -390
  204. package/src/server/management/model-rows.ts +3 -1
  205. package/src/server/management/provider-overwrite-carry.ts +10 -0
  206. package/src/server/management/provider-routes.ts +1 -0
  207. package/src/server/management/route-registry.ts +7 -5
  208. package/src/server/management/shared.ts +3 -0
  209. package/src/server/management-api.ts +3 -1
  210. package/src/server/messages-native.ts +68 -42
  211. package/src/server/relay-eager.ts +4 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-failure-attribution.ts +3 -0
  214. package/src/server/request-log-filter.ts +6 -1
  215. package/src/server/request-log-terminal-status.ts +243 -0
  216. package/src/server/request-log.ts +15 -118
  217. package/src/server/responses/adapter-continuation.ts +9 -1
  218. package/src/server/responses/adapter-dispatch.ts +293 -81
  219. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  220. package/src/server/responses/codex-ws-exchange.ts +147 -6
  221. package/src/server/responses/codex-ws-wire.ts +203 -0
  222. package/src/server/responses/collaboration.ts +36 -7
  223. package/src/server/responses/compact.ts +129 -50
  224. package/src/server/responses/context-overflow.ts +13 -1
  225. package/src/server/responses/core-codex-account.ts +105 -1
  226. package/src/server/responses/core-combo-failure.ts +14 -0
  227. package/src/server/responses/core-combo.ts +7 -2
  228. package/src/server/responses/core-errors.ts +29 -1
  229. package/src/server/responses/core-opaque-recovery.ts +5 -5
  230. package/src/server/responses/core-options.ts +2 -0
  231. package/src/server/responses/core.ts +6 -6
  232. package/src/server/responses/deferred-collab-surface.ts +158 -0
  233. package/src/server/responses/deferred-send-reporter.ts +25 -0
  234. package/src/server/responses/empty-completion-guard.ts +18 -2
  235. package/src/server/responses/fetch-helpers.ts +12 -1
  236. package/src/server/responses/jev-model-invoke.ts +1 -1
  237. package/src/server/responses/passthrough-delivery.ts +4 -1
  238. package/src/server/responses/passthrough-dispatch.ts +130 -64
  239. package/src/server/responses/passthrough-execution.ts +3 -3
  240. package/src/server/responses/request-prepare.ts +6 -4
  241. package/src/server/responses/request-transport.ts +131 -13
  242. package/src/server/responses/run-turn-execution.ts +13 -5
  243. package/src/server/responses/sidecar-execution.ts +16 -2
  244. package/src/server/responses/spend-storage-error.ts +33 -0
  245. package/src/server/responses/ws-upstream.ts +9 -1
  246. package/src/server/startup-action-control.ts +22 -2
  247. package/src/server/startup-health-cache.ts +4 -0
  248. package/src/server/system-env-shell.ts +7 -2
  249. package/src/server/system-env.ts +11 -1
  250. package/src/service/cli.ts +5 -1
  251. package/src/service/desktop-startup.ts +147 -34
  252. package/src/service/desktop-supervision.d.mts +27 -1
  253. package/src/service/desktop-supervision.mjs +111 -5
  254. package/src/service/health-sentinel-launch.ts +176 -0
  255. package/src/service/health-sentinel-main.ts +447 -0
  256. package/src/service/health-sentinel-policy.ts +241 -0
  257. package/src/service/health-sentinel-probe.ts +54 -0
  258. package/src/service/health-sentinel-windows.ts +87 -0
  259. package/src/service/linux-linger.ts +20 -0
  260. package/src/types/config.ts +5 -0
  261. package/src/types/request.ts +5 -0
  262. package/src/usage/expected-prices.ts +5 -1
  263. package/src/usage/log.ts +19 -1
  264. package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
  265. package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
  266. package/gui/dist/assets/index-D3FBCNiP.js +0 -94
@@ -1,3 +1,5 @@
1
+ import { deferPhysicalSendReporter } from "./deferred-send-reporter";
2
+ import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
1
3
  import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
2
4
  import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
3
5
  import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
@@ -22,11 +24,13 @@ import {
22
24
  sealRequestAttemptIdentity,
23
25
  recordAttemptCredentialSource,
24
26
  } from "../request-log";
25
- import { clientCancelledResponse, readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
27
+ import { anthropicPoolCooldownResponse, clientCancelledResponse, readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
28
+ import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
26
29
  import { redactSecretString } from "../../lib/redact";
27
30
  import { rewriteUpstreamPolicyRefusal } from "./policy-refusal";
28
31
  import { withProviderRequestSlot } from "../../providers/request-pacing";
29
32
  import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
33
+ import { isAnthropicBillingKeyPool, rotateAnthropicBillingKeyOnResponse } from "../../providers/anthropic-billing-recovery";
30
34
  import {
31
35
  transientRetryPolicyFor,
32
36
  resetReplayPolicyFor,
@@ -90,7 +94,8 @@ import {
90
94
  } from "../../providers/opencode-zen-rate-limit";
91
95
  import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
92
96
  import { consumeComboFailure } from "./core-combo-failure";
93
- import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
97
+ import { streamingContextOverflowResponse, jsonContextOverflowResponse, lateAttributionLimitResponse } from "./context-overflow";
98
+ import { OllamaLateAttributionBudgetError } from "../../adapters/ollama-native";
94
99
  import { isFixedCodexAccount } from "./core-codex-account";
95
100
  import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-model-fallback";
96
101
  import {
@@ -134,12 +139,15 @@ export async function prepareAdapterExchange(
134
139
  | "transportToken"
135
140
  | "oauthDispatch"
136
141
  | "imageTierBias"
142
+ | "oauth401ReplayAttempted"
137
143
  | "isOAuth401ReplayProvider"
138
144
  | "sentOAuthSnapshot"
139
145
  | "refreshResolvedOAuthSelection"
140
146
  | "replayOAuthCredentialSnapshot"
141
147
  | "invalidateSameTargetRequest"
142
148
  | "resolveSelectionAdapter"
149
+ | "resolveCopilotSelection"
150
+ | "copilotRefusalResponse"
143
151
  | "anthropicInstance"
144
152
  | "currentAnthropicRouteDecision"
145
153
  | "anthropicRouteDecision"
@@ -156,6 +164,7 @@ export async function prepareAdapterExchange(
156
164
  responseEffects: Pick<ResponsesEffects, "cancelResponseCompletion" | "notifyResponseComplete" | "refreshRequestToolAliases">,
157
165
  sendBudgetState: Pick<
158
166
  ResponsesSendBudget,
167
+ | "adapterSendBudget"
159
168
  | "adapterDispatchBudget"
160
169
  | "noteAdapterPhysicalSend"
161
170
  | "noteAdapterRecoveryWithheld"
@@ -182,6 +191,8 @@ export async function prepareAdapterExchange(
182
191
  refreshResolvedOAuthSelection,
183
192
  invalidateSameTargetRequest,
184
193
  resolveSelectionAdapter,
194
+ resolveCopilotSelection,
195
+ copilotRefusalResponse,
185
196
  anthropicInstance,
186
197
  currentAnthropicRouteDecision,
187
198
  anthropicSessionKey,
@@ -199,6 +210,7 @@ export async function prepareAdapterExchange(
199
210
  } = requestState;
200
211
  const { cancelResponseCompletion, notifyResponseComplete, refreshRequestToolAliases } = responseEffects;
201
212
  const {
213
+ adapterSendBudget,
202
214
  adapterDispatchBudget,
203
215
  noteAdapterPhysicalSend,
204
216
  noteAdapterRecoveryWithheld,
@@ -222,6 +234,17 @@ export async function prepareAdapterExchange(
222
234
 
223
235
  const upstream = new AbortController();
224
236
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
237
+ /** Contain replacement negotiation before adapter admission and release its upstream abort link. */
238
+ const resolveRotatedCopilotSelection = async (): Promise<Response | undefined> => {
239
+ try { await resolveCopilotSelection(parsed); }
240
+ catch (error) {
241
+ cleanupUpstreamAbort();
242
+ upstream.abort();
243
+ if ((options.abortSignal ?? req.signal).aborted) return clientCancelledResponse();
244
+ return copilotRefusalResponse(error)
245
+ ?? formatErrorResponse(502, "upstream_error", "GitHub Copilot Auto negotiation failed.");
246
+ }
247
+ };
225
248
  const connectMs = config.connectTimeoutMs ?? 200_000;
226
249
  // Bridge stall budget (seconds of silence before upstream_stall_timeout); the retry backoff
227
250
  // heartbeat interval is derived from it so the watchdog is always fed during deliberate waits.
@@ -322,6 +345,14 @@ export async function prepareAdapterExchange(
322
345
  cleanupUpstreamAbort();
323
346
  upstream.abort();
324
347
  if (options.abortSignal?.aborted) return clientCancelledResponse();
348
+ if (err instanceof OllamaLateAttributionBudgetError) return lateAttributionLimitResponse();
349
+ // An adapter can also throw the shared translator budget while sizing generated
350
+ // request content (e.g. Ollama late-tool attribution); keep its established 413.
351
+ if (isTranslatorBudgetExceededError(err)) {
352
+ return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
353
+ code: "translation_buffer_limit",
354
+ });
355
+ }
325
356
  const msg = err instanceof Error ? err.message : String(err);
326
357
  return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
327
358
  }
@@ -338,10 +369,32 @@ export async function prepareAdapterExchange(
338
369
  * is invisible to it and a missed bump would replay a request built with a stale key.
339
370
  */
340
371
 
372
+ const initialRecovery = route.providerName === "github-copilot"
373
+ ? sendBudgetState.pendingHopPermit?.sendClass === "auth-recovery" ? "oauth-account-429"
374
+ : transportState.oauth401ReplayAttempted ? "oauth-401" : undefined
375
+ : undefined;
341
376
  let upstreamResponse: Response;
377
+ // Preserve bounded terminal bytes without retaining the refused send's pacing lease.
378
+ // Reading the owned body to EOF (or cancelling on its bound) frees capacity BEFORE
379
+ // replacement admission; keeping that live body would wait on its own concurrency slot.
380
+ const retainFailureResponse = async (): Promise<Response> => {
381
+ const original = upstreamResponse;
382
+ const text = await readDisplaySafeErrorText(original, upstream.signal, "");
383
+ const headers = new Headers(original.headers);
384
+ headers.delete("content-length");
385
+ headers.delete("content-encoding");
386
+ upstreamResponse = new Response(text, { status: original.status, statusText: original.statusText, headers });
387
+ return upstreamResponse;
388
+ };
389
+ const billingPoolRequest = isAnthropicBillingKeyPool(route.provider);
390
+ let billingRecoveryActive = false;
391
+ const genericRefetchCountsSends = () => sendBudgetState.adapterSendBudget?.spendEnforced === true
392
+ || transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null
393
+ || billingPoolRequest || billingRecoveryActive;
394
+ let handoffPermit: ResponsesSendBudget["pendingHopPermit"];
342
395
  try {
343
396
  if (transportState.activeAdapter.fetchResponse) {
344
- transportState.noteRoutedAttemptSend(inputTokenEstimate);
397
+ transportState.noteRoutedAttemptSend(inputTokenEstimate, initialRecovery);
345
398
  const producer = adapterDispatchBudget?.beginSpendProducer?.();
346
399
  try {
347
400
  upstreamResponse = await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot =>
@@ -374,7 +427,22 @@ export async function prepareAdapterExchange(
374
427
  // reset-only semantics so combo failover hops on the first 5xx.
375
428
  const transientPolicy = transientRetryPolicyFor(route.provider);
376
429
  const resetPolicy = resetReplayPolicyFor(route.provider);
377
- const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
430
+ // Rebook a native Copilot hop against the negotiated destination before dispatch.
431
+ // Raw externally counted permits settle through the retry helper's send reporter.
432
+ if (route.providerName === "github-copilot" && adapterSendBudget && sendBudgetState.pendingHopPermit
433
+ && sendBudgetState.pendingHopPermit.sendClass === "auth-recovery") {
434
+ const original = sendBudgetState.pendingHopPermit;
435
+ sendBudgetState.pendingHopPermit = undefined;
436
+ original.release();
437
+ const decision = adapterSendBudget.reserveDispatch({
438
+ sendClass: original.sendClass,
439
+ targetKey: `${route.providerName}|${builtInitialRequest.url}|${route.modelId}`,
440
+ countedExternally: true,
441
+ });
442
+ if (!decision.allowed) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
443
+ handoffPermit = decision.permit;
444
+ }
445
+ const compactPrepaid = handoffPermit ?? (options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined);
378
446
  if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
379
447
  let compactPrepaidUsed = false;
380
448
  // A combo-owned or compaction-prepaid send is settled by the physical-dispatch receipt
@@ -395,6 +463,8 @@ export async function prepareAdapterExchange(
395
463
  const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
396
464
  ? fetchWithTransientRetry
397
465
  : fetchWithResetRetry;
466
+ const initialReporter = transientSendReporter(compactPrepaid ?? options.comboDispatchPermit);
467
+ const deferredInitial = billingPoolRequest && !receiptMode ? deferPhysicalSendReporter(initialReporter) : undefined;
398
468
  try { upstreamResponse = await fetchWithRetryPolicy(
399
469
  recovery => {
400
470
  // Unconfigured generic sends may omit the counting reporter. Capture policy
@@ -403,11 +473,12 @@ export async function prepareAdapterExchange(
403
473
  poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
404
474
  });
405
475
  // Receipt mode books the physical send at executor admission instead of here.
406
- if (!receiptMode && compactPrepaid && !compactPrepaidUsed) {
476
+ if (!deferredInitial && !receiptMode && compactPrepaid && !compactPrepaidUsed) {
407
477
  if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
408
478
  compactPrepaidUsed = true;
409
479
  }
410
- transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery);
480
+ // Key transport keeps this metadata pending until it binds the actual selection.
481
+ transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery ?? initialRecovery);
411
482
  return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
412
483
  method: builtInitialRequest.method,
413
484
  headers: builtInitialRequest.headers,
@@ -418,6 +489,13 @@ export async function prepareAdapterExchange(
418
489
  providerName: route.providerName,
419
490
  modelId: route.modelId,
420
491
  onPhysicalDispatch: () => {
492
+ if (deferredInitial) {
493
+ if (!deferredInitial.beforeDispatch()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
494
+ if (compactPrepaid && !compactPrepaidUsed) {
495
+ if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
496
+ compactPrepaidUsed = true;
497
+ }
498
+ }
421
499
  // Preserve the unconfigured direct-adapter accounting contract; Combo admission
422
500
  // and explicit retry/compaction budgets require a receipt for their booked send.
423
501
  if (receiptMode) {
@@ -434,6 +512,7 @@ export async function prepareAdapterExchange(
434
512
  label: safeHostLabel(builtInitialRequest.url),
435
513
  claimAmbiguousResend: claimPreHeaderResend,
436
514
  ...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
515
+ || billingPoolRequest
437
516
  ? {
438
517
  // A pending compaction permit already booked this leg's first physical send.
439
518
  ...(transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
@@ -446,7 +525,7 @@ export async function prepareAdapterExchange(
446
525
  // settled by `onPhysicalDispatch`, so a reporter here would count them twice.
447
526
  ...(receiptMode
448
527
  ? {}
449
- : { onSendsConsumed: transientSendReporter(compactPrepaid ?? options.comboDispatchPermit) }),
528
+ : { onSendsConsumed: deferredInitial?.report ?? initialReporter }),
450
529
  }
451
530
  : {}),
452
531
  },
@@ -456,6 +535,11 @@ export async function prepareAdapterExchange(
456
535
  cleanupUpstreamAbort();
457
536
  upstream.abort();
458
537
  if (options.abortSignal?.aborted) return clientCancelledResponse();
538
+ const storageRefusal = spendLedgerStorageErrorResponse(err, logCtx);
539
+ if (storageRefusal) {
540
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
541
+ return storageRefusal;
542
+ }
459
543
  const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
460
544
  const codexRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
461
545
  now: Date.now(), accountSelector: route.codexAccountNamespace,
@@ -472,8 +556,7 @@ export async function prepareAdapterExchange(
472
556
  return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal));
473
557
  }
474
558
  if (refusal instanceof AnthropicAccountCooldownError) {
475
- return formatErrorResponse(429, "rate_limit_error", refusal.message,
476
- refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
559
+ return anthropicPoolCooldownResponse(refusal, { comboAttempt: options.comboAttempt, logCtx });
477
560
  }
478
561
  // A budget refusal is a decision this process made, not an upstream fault. Reporting it as
479
562
  // 502 does more than mislabel it: the Codex client retries 5xx and does not retry a 429, so
@@ -486,6 +569,7 @@ export async function prepareAdapterExchange(
486
569
  const msg = describeUpstreamConnectFailure(err, connectMs);
487
570
  return formatErrorResponse(502, "upstream_error", msg);
488
571
  } finally {
572
+ handoffPermit?.release();
489
573
  builtInitialRequest.releaseBodyObservation?.();
490
574
  }
491
575
 
@@ -509,7 +593,7 @@ export async function prepareAdapterExchange(
509
593
  // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
510
594
  // moments later; at most one byte-identical replay is allowed per request.
511
595
  const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
512
- let oauth401ReplayAttempted = false;
596
+ let oauth401ReplayAttempted = transportState.oauth401ReplayAttempted;
513
597
  let antigravityAuthRotationAttempted = false;
514
598
  // At most one reasoning-effort downgrade per request. This sits outside the recovery loop
515
599
  // below for the same reason the two guards above do: a guard declared inside it is reset by
@@ -559,13 +643,26 @@ export async function prepareAdapterExchange(
559
643
  recordAdapterReasoning(logCtx, retryRequest);
560
644
  recordAdapterTier(logCtx, retryRequest);
561
645
  } catch (err) {
562
- if (preserveFailureResponse && !options.abortSignal?.aborted) return { failed: preserveFailureResponse };
646
+ // A rotated credential that cannot be shaped keeps the refusal it was meant to
647
+ // replace. The image-tier rebuild is different: the request itself failed to shape,
648
+ // and its established 413/400 build-error contract stays authoritative.
649
+ if (preserveFailureResponse && recovery !== "image-413" && !options.abortSignal?.aborted) {
650
+ return { failed: preserveFailureResponse };
651
+ }
563
652
  // A rotated/rebuilt adapter build failure is a request-shaping error, not an
564
- // upstream connect failure: tear the abort link down and map it as 400 (no 413
565
- // translator-budget mapping here — that stays with parseRequest/buildToolBridgeMaps).
653
+ // upstream connect failure: tear the abort link down and map it as 400. A
654
+ // translator-budget refusal keeps its established 413 here as well — adapters
655
+ // can throw it while sizing generated content (e.g. Ollama late-tool
656
+ // attribution), not only parseRequest/buildToolBridgeMaps.
566
657
  cleanupUpstreamAbort();
567
658
  upstream.abort();
568
659
  if (options.abortSignal?.aborted) return { failed: clientCancelledResponse() };
660
+ if (err instanceof OllamaLateAttributionBudgetError) return { failed: lateAttributionLimitResponse() };
661
+ if (isTranslatorBudgetExceededError(err)) {
662
+ return { failed: formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
663
+ code: "translation_buffer_limit",
664
+ }) };
665
+ }
569
666
  const msg = err instanceof Error ? err.message : String(err);
570
667
  return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(msg)) };
571
668
  }
@@ -628,7 +725,7 @@ export async function prepareAdapterExchange(
628
725
  const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
629
726
  ? fetchWithTransientRetry
630
727
  : fetchWithResetRetry;
631
- const helperCountsSends = sendBudgetState.adapterSendBudget?.spendEnforced === true || refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
728
+ const helperCountsSends = genericRefetchCountsSends();
632
729
  const prepaid = sendBudgetState.pendingHopPermit;
633
730
  const configuredTotal = refetchTransientPolicy?.attempts;
634
731
  const refetchCap = transientSendCapFor(configuredTotal, sendBudgetState.targetSendsUsed);
@@ -644,27 +741,41 @@ export async function prepareAdapterExchange(
644
741
  { allowFinalRecoveryReserve: configuredTotal === undefined },
645
742
  )
646
743
  : undefined;
744
+ let refetchPermitUsed = false;
745
+ const refetchReporter = refetchAllowance ? transientSendReporter(refetchAllowance.permit) : undefined;
746
+ const deferredRefetch = billingPoolRequest && refetchReporter ? deferPhysicalSendReporter(refetchReporter) : undefined;
647
747
  try {
648
748
  return await refetchWithPolicy(
649
749
  recoveryKind => {
650
750
  sendBudgetState.adapterSendBudget?.startRequest?.({
651
751
  poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
652
752
  });
653
- if (refetchAllowance?.permit && !refetchAllowance.permit.use()) {
654
- throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
655
- }
656
- transportState.noteRoutedAttemptSend(retryEstimate, recoveryKind ?? recovery);
657
- // Same boundary on the helper path: the thunk is what reaches the wire, and it
658
- // can be refused above before it does. use() past the first attempt is a no-op.
659
- onDispatch?.();
660
- if (preserveFailureResponse) replacementAdmitted = true;
661
- if (!refetchAllowance) chargeFastDowngradeWorkflowSend();
753
+ // Prepare key metadata before the guarded transport commits its attempt;
754
+ // counters and permits still wait for the physical executor after pacing.
755
+ if (deferredRefetch) transportState.noteRoutedAttemptSend(retryEstimate, recoveryKind ?? recovery);
756
+ const confirmDispatch = () => {
757
+ if (refetchAllowance?.permit && !refetchPermitUsed) {
758
+ if (!refetchAllowance.permit.use()) throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
759
+ refetchPermitUsed = true;
760
+ }
761
+ if (!deferredRefetch) transportState.noteRoutedAttemptSend(retryEstimate, recoveryKind ?? recovery);
762
+ // Eligible pools confirm after pacing at the physical executor; legacy
763
+ // helper paths retain their existing thunk admission boundary.
764
+ onDispatch?.();
765
+ if (preserveFailureResponse) replacementAdmitted = true;
766
+ if (!refetchAllowance) chargeFastDowngradeWorkflowSend();
767
+ };
768
+ if (!deferredRefetch) confirmDispatch();
662
769
  return fetchWithHeaderTimeout(retryRequest.url,
663
770
  applyUpstreamRecoveryInit({
664
771
  method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
665
772
  }, recoveryKind), upstream.signal, connectMs, parsed.stream,
666
773
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
667
774
  dispatchOverride: oauthDispatch(retryRequest),
775
+ onPhysicalDispatch: deferredRefetch ? () => {
776
+ if (!deferredRefetch.beforeDispatch()) throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
777
+ confirmDispatch();
778
+ } : undefined,
668
779
  providerName: route.providerName,
669
780
  modelId: route.modelId,
670
781
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
@@ -678,20 +789,27 @@ export async function prepareAdapterExchange(
678
789
  ...(refetchAllowance
679
790
  ? {
680
791
  attempts: refetchAllowance.attempts,
681
- onSendsConsumed: transientSendReporter(refetchAllowance.permit),
792
+ onSendsConsumed: deferredRefetch?.report ?? refetchReporter,
682
793
  }
683
794
  : {}),
684
795
  },
685
796
  );
686
797
  } finally {
687
- // Refunds only a reservation whose send never happened -- an abort settled before
688
- // the thunk ran. A used or externally settled permit ignores this.
798
+ // Refund only a reservation whose send never reached its admission boundary.
799
+ // A used or externally settled permit ignores this.
689
800
  refetchAllowance?.permit?.release();
690
801
  }
691
802
  } finally {
692
803
  retryRequest.releaseBodyObservation?.();
693
804
  }
694
805
  } catch (err) {
806
+ const storageRefusal = !options.abortSignal?.aborted && spendLedgerStorageErrorResponse(err, logCtx);
807
+ if (storageRefusal) {
808
+ cleanupUpstreamAbort();
809
+ upstream.abort();
810
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
811
+ return { failed: storageRefusal };
812
+ }
695
813
  const codexRefusal = !options.abortSignal?.aborted && mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
696
814
  now: Date.now(), accountSelector: route.codexAccountNamespace,
697
815
  });
@@ -716,8 +834,7 @@ export async function prepareAdapterExchange(
716
834
  return { failed: formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal)) };
717
835
  }
718
836
  if (refusal instanceof AnthropicAccountCooldownError) {
719
- return { failed: formatErrorResponse(429, "rate_limit_error", refusal.message,
720
- refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) }) };
837
+ return { failed: anthropicPoolCooldownResponse(refusal, { comboAttempt: options.comboAttempt, logCtx }) };
721
838
  }
722
839
  // Same rule on the recovery leg: the ladder refused to send again, so the answer names
723
840
  // this proxy rather than the provider it never reached.
@@ -793,7 +910,7 @@ export async function prepareAdapterExchange(
793
910
  && !oauth401ReplayAttempted
794
911
  && !sendBudgetExhausted()
795
912
  ) {
796
- oauth401ReplayAttempted = true;
913
+ oauth401ReplayAttempted = transportState.oauth401ReplayAttempted = true;
797
914
  let refreshed: OAuthAccessSnapshot;
798
915
  try {
799
916
  refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
@@ -864,7 +981,7 @@ export async function prepareAdapterExchange(
864
981
  if (route.providerName === "kiro") {
865
982
  parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
866
983
  }
867
- const refreshedProvider = resolveProviderTransport(
984
+ let refreshedProvider = resolveProviderTransport(
868
985
  route.providerName,
869
986
  {
870
987
  ...route.provider,
@@ -877,6 +994,9 @@ export async function prepareAdapterExchange(
877
994
  : undefined,
878
995
  );
879
996
  route.provider = refreshedProvider;
997
+ const copilotRefusal = await resolveRotatedCopilotSelection();
998
+ if (copilotRefusal) return copilotRefusal;
999
+ refreshedProvider = route.provider;
880
1000
  invalidateSameTargetRequest();
881
1001
  transportState.activeAdapter = resolveSelectionAdapter(
882
1002
  resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
@@ -925,30 +1045,83 @@ export async function prepareAdapterExchange(
925
1045
  promptCacheKey: parsed.options.promptCacheKey,
926
1046
  });
927
1047
  if (!rotated) break;
928
- // Release the failed response's socket before retrying; unread bodies otherwise linger
929
- // until runtime cleanup (one per rotated key).
930
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
931
- route.provider = rotated;
932
- invalidateSameTargetRequest();
933
- transportState.activeAdapter = resolveSelectionAdapter(
934
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
935
- config.cacheRetention,
936
- );
937
- bindRouteReasoningReplayScope({
938
- parsed,
939
- providerName: route.providerName,
940
- provider: route.provider,
941
- adapterName: transportState.activeAdapter.name,
942
- });
943
- const result = await rebuildAndRefetch("key-401");
944
- if ("failed" in result) return result.failed;
945
- upstreamResponse = result;
946
- // A recovery refetch can itself die on an ambiguous pre-header reset, and the refusal
947
- // that answers it is a 429. Every arm below keys on 429, so letting it fall through
948
- // hands the marked refusal to the next waiting arm and replays the send it exists to
949
- // stop. Re-enter the loop guard instead, which returns it unchanged.
950
- if (isNonReplayableResponse(upstreamResponse)) continue recovery;
951
- }
1048
+ if (billingRecoveryActive && (options.abortSignal?.aborted || upstream.signal.aborted)) {
1049
+ cleanupUpstreamAbort(); upstream.abort(); return clientCancelledResponse();
1050
+ }
1051
+ const billingHop = (billingPoolRequest || billingRecoveryActive)
1052
+ ? reserveCredentialHop("auth-recovery", `${route.providerName}|${route.modelId}|key-401`,
1053
+ transportState.activeAdapter.fetchResponse === undefined)
1054
+ : undefined;
1055
+ if (billingHop && !billingHop.allowed) break;
1056
+ const failedResponse = billingPoolRequest || billingRecoveryActive ? await retainFailureResponse() : undefined;
1057
+ try {
1058
+ // Release the failed response's socket before retrying; unread bodies otherwise linger
1059
+ // until runtime cleanup (one per rotated key).
1060
+ if (!failedResponse) try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1061
+ route.provider = rotated;
1062
+ const selectionFailure = await resolveRotatedCopilotSelection();
1063
+ if (selectionFailure) return selectionFailure;
1064
+ invalidateSameTargetRequest();
1065
+ transportState.activeAdapter = resolveSelectionAdapter(
1066
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1067
+ config.cacheRetention,
1068
+ );
1069
+ bindRouteReasoningReplayScope({
1070
+ parsed,
1071
+ providerName: route.providerName,
1072
+ provider: route.provider,
1073
+ adapterName: transportState.activeAdapter.name,
1074
+ });
1075
+ if (billingHop) sendBudgetState.pendingHopPermit = billingHop.permit;
1076
+ const result = await rebuildAndRefetch("key-401",
1077
+ billingHop ? () => { billingHop.permit?.use(); } : undefined, failedResponse);
1078
+ if ("failed" in result) {
1079
+ if (result.failed !== failedResponse) return result.failed;
1080
+ break;
1081
+ }
1082
+ if (failedResponse) {
1083
+ try { void failedResponse.body?.cancel().catch(() => {}); } catch { /* closed */ }
1084
+ }
1085
+ upstreamResponse = result;
1086
+ // A recovery refetch can itself die on an ambiguous pre-header reset, and the refusal
1087
+ // that answers it is a 429. Every arm below keys on 429, so letting it fall through
1088
+ // hands the marked refusal to the next waiting arm and replays the send it exists to
1089
+ // stop. Re-enter the loop guard instead, which returns it unchanged.
1090
+ if (isNonReplayableResponse(upstreamResponse)) continue recovery;
1091
+ } finally {
1092
+ if (billingHop) { sendBudgetState.pendingHopPermit = undefined; billingHop.permit?.release(); }
1093
+ }
1094
+ }
1095
+
1096
+ while ((upstreamResponse.status === 400 || upstreamResponse.status === 402) && hasKeyPoolFailover(route.provider)) {
1097
+ const rotated = await rotateAnthropicBillingKeyOnResponse(upstreamResponse, config, route.providerName, route.provider,
1098
+ { signal: upstream.signal, promptCacheKey: parsed.options.promptCacheKey });
1099
+ if (upstream.signal.aborted || options.abortSignal?.aborted) {
1100
+ cleanupUpstreamAbort(); upstream.abort(); return clientCancelledResponse();
1101
+ }
1102
+ if (!rotated) break;
1103
+ billingRecoveryActive = true;
1104
+ const hop = reserveCredentialHop("auth-recovery", `${route.providerName}|${route.modelId}|key-401`,
1105
+ transportState.activeAdapter.fetchResponse === undefined);
1106
+ if (!hop.allowed) break;
1107
+ const failedResponse = await retainFailureResponse();
1108
+ try {
1109
+ route.provider = rotated;
1110
+ invalidateSameTargetRequest();
1111
+ transportState.activeAdapter = resolveSelectionAdapter(
1112
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy), config.cacheRetention);
1113
+ bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: route.provider, adapterName: transportState.activeAdapter.name });
1114
+ sendBudgetState.pendingHopPermit = hop.permit;
1115
+ const result = await rebuildAndRefetch("key-401", () => { hop.permit?.use(); }, failedResponse);
1116
+ if ("failed" in result) {
1117
+ if (result.failed !== failedResponse) return result.failed;
1118
+ break;
1119
+ }
1120
+ try { void failedResponse.body?.cancel().catch(() => {}); } catch { /* closed */ }
1121
+ upstreamResponse = result;
1122
+ continue recovery;
1123
+ } finally { sendBudgetState.pendingHopPermit = undefined; hop.permit?.release(); }
1124
+ }
952
1125
 
953
1126
  // Anthropic fast mode refused (no usage credits, organization not enabled, model outside
954
1127
  // the lane, fast pool empty): resend once at standard speed, as Claude Code does. This
@@ -967,7 +1140,7 @@ export async function prepareAdapterExchange(
967
1140
  const hop = reserveCredentialHop(
968
1141
  "repair",
969
1142
  `${route.providerName}|${route.modelId}|anthropic-fast-downgrade`,
970
- !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
1143
+ !adapterOwnsDispatch && genericRefetchCountsSends(),
971
1144
  );
972
1145
  if (hop.allowed) {
973
1146
  anthropicFastDowngradeGuard.attempted = true;
@@ -1036,7 +1209,9 @@ export async function prepareAdapterExchange(
1036
1209
  upstreamResponse = result;
1037
1210
  // The refusal is a 429 too: without this the while condition is still true and the
1038
1211
  // next configured attempt replays it on the same target.
1039
- if (isNonReplayableResponse(upstreamResponse)) continue recovery;
1212
+ if (isNonReplayableResponse(upstreamResponse)
1213
+ || (isAnthropicBillingKeyPool(route.provider)
1214
+ && [400, 402].includes(upstreamResponse.status))) continue recovery;
1040
1215
  }
1041
1216
 
1042
1217
  // Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
@@ -1071,28 +1246,53 @@ export async function prepareAdapterExchange(
1071
1246
  quotaResetAt,
1072
1247
  });
1073
1248
  if (!rotated) break;
1074
- // Release the failed response's socket before retrying; unread bodies otherwise linger
1075
- // until runtime cleanup (one per rotated key under a rate-limit storm).
1076
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1077
- route.provider = rotated;
1078
- invalidateSameTargetRequest();
1079
- transportState.activeAdapter = resolveSelectionAdapter(
1080
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1081
- config.cacheRetention,
1082
- );
1083
- bindRouteReasoningReplayScope({
1084
- parsed,
1085
- providerName: route.providerName,
1086
- provider: route.provider,
1087
- adapterName: transportState.activeAdapter.name,
1088
- });
1089
- const result = await rebuildAndRefetch("key-429");
1090
- if ("failed" in result) return result.failed;
1091
- upstreamResponse = result;
1092
- // Rotating on the refusal would also write a cooldown against a key that rate-limited
1093
- // nothing, which outlives the request.
1094
- if (isNonReplayableResponse(upstreamResponse)) continue recovery;
1095
- }
1249
+ if (billingRecoveryActive && (options.abortSignal?.aborted || upstream.signal.aborted)) {
1250
+ cleanupUpstreamAbort(); upstream.abort(); return clientCancelledResponse();
1251
+ }
1252
+ const billingHop = (billingPoolRequest || billingRecoveryActive)
1253
+ ? reserveCredentialHop("auth-recovery", `${route.providerName}|${route.modelId}|key-429`,
1254
+ transportState.activeAdapter.fetchResponse === undefined)
1255
+ : undefined;
1256
+ if (billingHop && !billingHop.allowed) break;
1257
+ const failedResponse = billingPoolRequest || billingRecoveryActive ? await retainFailureResponse() : undefined;
1258
+ try {
1259
+ // Release the failed response's socket before retrying; unread bodies otherwise linger
1260
+ // until runtime cleanup (one per rotated key under a rate-limit storm).
1261
+ if (!failedResponse) try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1262
+ route.provider = rotated;
1263
+ const selectionFailure = await resolveRotatedCopilotSelection();
1264
+ if (selectionFailure) return selectionFailure;
1265
+ invalidateSameTargetRequest();
1266
+ transportState.activeAdapter = resolveSelectionAdapter(
1267
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1268
+ config.cacheRetention,
1269
+ );
1270
+ bindRouteReasoningReplayScope({
1271
+ parsed,
1272
+ providerName: route.providerName,
1273
+ provider: route.provider,
1274
+ adapterName: transportState.activeAdapter.name,
1275
+ });
1276
+ if (billingHop) sendBudgetState.pendingHopPermit = billingHop.permit;
1277
+ const result = await rebuildAndRefetch("key-429",
1278
+ billingHop ? () => { billingHop.permit?.use(); } : undefined, failedResponse);
1279
+ if ("failed" in result) {
1280
+ if (result.failed !== failedResponse) return result.failed;
1281
+ break;
1282
+ }
1283
+ if (failedResponse) {
1284
+ try { void failedResponse.body?.cancel().catch(() => {}); } catch { /* closed */ }
1285
+ }
1286
+ upstreamResponse = result;
1287
+ // Rotating on the refusal would also write a cooldown against a key that rate-limited
1288
+ // nothing, which outlives the request.
1289
+ if (isNonReplayableResponse(upstreamResponse)
1290
+ || (billingRecoveryActive && upstreamResponse.status === 401)
1291
+ || (route.provider.adapter === "anthropic" && [400, 402].includes(upstreamResponse.status))) continue recovery;
1292
+ } finally {
1293
+ if (billingHop) { sendBudgetState.pendingHopPermit = undefined; billingHop.permit?.release(); }
1294
+ }
1295
+ }
1096
1296
 
1097
1297
  // Anthropic OAuth: recover a rate limit or proven account entitlement refusal
1098
1298
  // before output, within the shared request and account rotation limits.
@@ -1480,9 +1680,13 @@ export async function prepareAdapterExchange(
1480
1680
  imageRetryAttempted = true;
1481
1681
  transportState.imageTierBias = 1;
1482
1682
  invalidateSameTargetRequest();
1483
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1484
- const result = await rebuildAndRefetch("image-413");
1485
- if ("failed" in result) return result.failed;
1683
+ const failedResponse = await retainFailureResponse();
1684
+ const result = await rebuildAndRefetch("image-413", undefined, failedResponse);
1685
+ if ("failed" in result) {
1686
+ if (result.failed !== failedResponse) return result.failed;
1687
+ break recovery;
1688
+ }
1689
+ try { void failedResponse.body?.cancel().catch(() => {}); } catch { /* closed */ }
1486
1690
  upstreamResponse = result;
1487
1691
  continue recovery;
1488
1692
  }
@@ -1599,6 +1803,14 @@ export async function prepareAdapterExchange(
1599
1803
  requestState.subagentFallbackAccountId,
1600
1804
  );
1601
1805
  }
1806
+ // The last pool account just refused on spent quota and recovery found no replacement:
1807
+ // answer the usage limit now (#6991), so Codex stops instead of retrying into the local
1808
+ // refusal that every later attempt meets until the first window reopens.
1809
+ const poolUsageLimit = upstreamResponse.status === 429 && anthropicInstance && route.provider.authMode === "oauth"
1810
+ ? anthropicRoutingFor(anthropicInstance).anthropicPoolUsageLimitAfterRefusal(Date.now(), transportState.anthropicRouteDecision, route.modelId)
1811
+ : null;
1812
+ // Combo children returned above; the flag is passed anyway so this site states the same rule.
1813
+ if (poolUsageLimit) return anthropicPoolCooldownResponse(poolUsageLimit, { comboAttempt: options.comboAttempt, logCtx });
1602
1814
  // Upstreams occasionally echo request details in error bodies — scrub token-shaped
1603
1815
  // material before it reaches the client-facing error surface.
1604
1816
  const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");