@bitkyc08/opencodex 2.83.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 (216) 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-DiJO6sYY.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/ollama-native.ts +29 -1
  10. package/src/adapters/openai-chat/messages.ts +8 -4
  11. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  12. package/src/adapters/openai-responses/passthrough.ts +3 -0
  13. package/src/adapters/openai-responses/request-strips.ts +10 -2
  14. package/src/adapters/qoder/adapter.ts +16 -14
  15. package/src/bridge/errors.ts +19 -0
  16. package/src/bridge.ts +1 -1
  17. package/src/claude/context-windows.ts +54 -1
  18. package/src/claude/intercept/connect-proxy.ts +10 -3
  19. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  20. package/src/claude/intercept/picker-listener.ts +49 -1
  21. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  22. package/src/claude/intercept/settings.ts +101 -9
  23. package/src/cli/account-api.ts +65 -0
  24. package/src/cli/account-auth.ts +26 -2
  25. package/src/cli/account.ts +41 -5
  26. package/src/cli/capabilities-access-remote.ts +8 -0
  27. package/src/cli/capabilities-base.ts +13 -1
  28. package/src/cli/catalog-command-result.ts +2 -1
  29. package/src/cli/claude.ts +15 -6
  30. package/src/cli/config-command.ts +8 -2
  31. package/src/cli/debug.ts +8 -0
  32. package/src/cli/dispatch.ts +16 -3
  33. package/src/cli/export-command.ts +14 -1
  34. package/src/cli/hub-disable.ts +99 -0
  35. package/src/cli/hub.ts +4 -2
  36. package/src/cli/index.ts +13 -4
  37. package/src/cli/link-child.ts +104 -0
  38. package/src/cli/link-enrollment.ts +96 -0
  39. package/src/cli/link.ts +39 -24
  40. package/src/cli/observe-stream.ts +6 -1
  41. package/src/cli/observe.ts +10 -2
  42. package/src/cli/provider.ts +12 -0
  43. package/src/cli/registry.ts +5 -4
  44. package/src/cli/runtime-api.ts +27 -2
  45. package/src/cli/status.ts +104 -20
  46. package/src/client/connect.ts +39 -4
  47. package/src/client/hub-client.ts +14 -7
  48. package/src/client/hub-state.ts +25 -14
  49. package/src/client/link-home.ts +248 -0
  50. package/src/client/link-management-relay.ts +61 -0
  51. package/src/client/link-relay.ts +6 -1
  52. package/src/client/link-status.ts +14 -0
  53. package/src/client/machine-api.ts +14 -7
  54. package/src/client/machine-listener.ts +29 -4
  55. package/src/clients/config-export.ts +46 -16
  56. package/src/clients/omo-role-models.ts +6 -3
  57. package/src/codex/agent-role-models.ts +26 -8
  58. package/src/codex/auth-api/account-list.ts +17 -2
  59. package/src/codex/auth-api/account-selection.ts +17 -7
  60. package/src/codex/auth-api/routes.ts +5 -1
  61. package/src/codex/autostart-health.ts +3 -1
  62. package/src/codex/catalog/bundled.ts +5 -1
  63. package/src/codex/catalog/effort.ts +2 -2
  64. package/src/codex/catalog/metadata.ts +4 -2
  65. package/src/codex/catalog/native-models.ts +16 -2
  66. package/src/codex/native-profile-startup.ts +4 -1
  67. package/src/codex/prompt-journal.ts +20 -2
  68. package/src/codex/prompt-layers/encoding.ts +49 -0
  69. package/src/codex/prompt-layers/import-source.ts +30 -0
  70. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  71. package/src/codex/prompt-layers/toml-read.ts +52 -4
  72. package/src/codex/prompt-layers.ts +383 -20
  73. package/src/codex/prompt-lock-claim.ts +13 -3
  74. package/src/codex/prompt-lock-evidence.ts +3 -3
  75. package/src/codex/prompt-lock-owner.ts +14 -6
  76. package/src/codex/prompt-text-probe.ts +34 -5
  77. package/src/codex/quota.ts +13 -6
  78. package/src/codex/role-auto-assign.ts +2 -5
  79. package/src/codex/routing/active-account.ts +21 -0
  80. package/src/codex/routing.ts +1 -1
  81. package/src/codex/runtime.ts +196 -8
  82. package/src/codex/shim-overlay.ts +8 -4
  83. package/src/codex/shim.ts +1 -1
  84. package/src/combos/failover.ts +3 -0
  85. package/src/config/api-key-reconcile.ts +34 -0
  86. package/src/config/diagnostics.ts +16 -0
  87. package/src/config/feature-flags.ts +5 -0
  88. package/src/config/live-reconcile.ts +43 -29
  89. package/src/config/live-topology.ts +29 -0
  90. package/src/config/load-degrade.ts +2 -1
  91. package/src/config/paths.ts +3 -0
  92. package/src/config/salvage.ts +3 -1
  93. package/src/config/schema/config-schema.ts +2 -0
  94. package/src/generated/compatibility-version.json +311 -171
  95. package/src/grok/grpc-web.ts +23 -1
  96. package/src/grok/reset-coupon-ledger.ts +75 -5
  97. package/src/grok/reset-coupons.ts +4 -2
  98. package/src/integrations/mutation-plan.ts +13 -7
  99. package/src/integrations/registry.ts +45 -11
  100. package/src/integrations/state.ts +19 -7
  101. package/src/integrations/target.ts +42 -5
  102. package/src/integrations/writer.ts +14 -6
  103. package/src/lib/app-owned-memory-stores.ts +4 -0
  104. package/src/lib/app-owned-memory.ts +11 -0
  105. package/src/lib/json-byte-size.ts +31 -0
  106. package/src/lib/optional-shutdown-hooks.ts +35 -0
  107. package/src/lib/outbound-credential-registry.ts +210 -0
  108. package/src/lib/passive-read.ts +35 -0
  109. package/src/lib/shadow-call.ts +44 -5
  110. package/src/link/codex-settings.ts +155 -0
  111. package/src/link/management-routes.ts +89 -0
  112. package/src/link/remote-probe.ts +38 -0
  113. package/src/link/ssh-argv.ts +26 -5
  114. package/src/link/ssh-runner.ts +58 -27
  115. package/src/link/status-projection.ts +22 -4
  116. package/src/link/supervisor.ts +144 -49
  117. package/src/link/tunnel-state.ts +11 -4
  118. package/src/oauth/anthropic-continuity.ts +4 -3
  119. package/src/oauth/anthropic-identity.ts +13 -2
  120. package/src/oauth/anthropic-model-quota.ts +13 -1
  121. package/src/oauth/anthropic-routing.ts +230 -23
  122. package/src/oauth/anthropic.ts +18 -4
  123. package/src/oauth/google-antigravity.ts +24 -7
  124. package/src/oauth/index.ts +24 -15
  125. package/src/oauth/kiro-account-identity.ts +106 -0
  126. package/src/oauth/kiro.ts +48 -5
  127. package/src/oauth/plan.ts +29 -0
  128. package/src/oauth/store.ts +40 -3
  129. package/src/oauth/types.ts +2 -0
  130. package/src/plugins/loader.ts +11 -0
  131. package/src/providers/anthropic-billing-ownership.ts +74 -0
  132. package/src/providers/anthropic-billing-recovery.ts +31 -0
  133. package/src/providers/anthropic-billing-refusal.ts +55 -0
  134. package/src/providers/anthropic-reset-admission.ts +62 -0
  135. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  136. package/src/providers/api-key-resolve.ts +17 -2
  137. package/src/providers/key-failover.ts +52 -5
  138. package/src/providers/model-rename-startup.ts +5 -3
  139. package/src/providers/quota/account-cache.ts +11 -0
  140. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  141. package/src/providers/quota/report-cache.ts +14 -0
  142. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  143. package/src/providers/quota.ts +4 -1
  144. package/src/providers/registry/entries-core.ts +14 -14
  145. package/src/providers/registry/entries-extended.ts +0 -1
  146. package/src/providers/registry/model-seeds.ts +17 -0
  147. package/src/providers/request-pacing.ts +9 -0
  148. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  149. package/src/responses/compaction.ts +14 -0
  150. package/src/responses/parser.ts +7 -1
  151. package/src/server/auth-cors.ts +42 -0
  152. package/src/server/configured-credentials.ts +194 -0
  153. package/src/server/index/link-management.ts +58 -0
  154. package/src/server/index/optional-listeners.ts +5 -1
  155. package/src/server/index/serve-options.ts +13 -0
  156. package/src/server/lifecycle.ts +2 -1
  157. package/src/server/management/agent-settings-routes.ts +28 -10
  158. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  159. package/src/server/management/codex-agent-role-routes.ts +19 -6
  160. package/src/server/management/codex-prompt-routes.ts +136 -5
  161. package/src/server/management/codex-role-auto-assign.ts +1 -1
  162. package/src/server/management/grok-coupon-routes.ts +78 -27
  163. package/src/server/management/link-enrollment.ts +192 -0
  164. package/src/server/management/link-route-state.ts +185 -0
  165. package/src/server/management/link-routes.ts +82 -402
  166. package/src/server/management/model-rows.ts +3 -1
  167. package/src/server/management/provider-overwrite-carry.ts +10 -0
  168. package/src/server/management/provider-routes.ts +1 -0
  169. package/src/server/management/route-registry.ts +5 -3
  170. package/src/server/management/shared.ts +3 -0
  171. package/src/server/management-api.ts +3 -1
  172. package/src/server/messages-native.ts +68 -42
  173. package/src/server/relay-eager.ts +2 -0
  174. package/src/server/request-log-filter.ts +6 -1
  175. package/src/server/request-log-terminal-status.ts +63 -6
  176. package/src/server/responses/adapter-dispatch.ts +232 -79
  177. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  178. package/src/server/responses/codex-ws-exchange.ts +147 -6
  179. package/src/server/responses/codex-ws-wire.ts +203 -0
  180. package/src/server/responses/collaboration.ts +36 -7
  181. package/src/server/responses/compact.ts +126 -48
  182. package/src/server/responses/context-overflow.ts +13 -1
  183. package/src/server/responses/core-codex-account.ts +105 -1
  184. package/src/server/responses/core-combo-failure.ts +14 -0
  185. package/src/server/responses/core-combo.ts +7 -2
  186. package/src/server/responses/core-errors.ts +29 -1
  187. package/src/server/responses/core-options.ts +2 -0
  188. package/src/server/responses/deferred-collab-surface.ts +158 -0
  189. package/src/server/responses/deferred-send-reporter.ts +25 -0
  190. package/src/server/responses/fetch-helpers.ts +12 -1
  191. package/src/server/responses/passthrough-delivery.ts +4 -1
  192. package/src/server/responses/passthrough-dispatch.ts +15 -4
  193. package/src/server/responses/request-prepare.ts +6 -4
  194. package/src/server/responses/request-transport.ts +6 -8
  195. package/src/server/responses/sidecar-execution.ts +11 -2
  196. package/src/server/responses/ws-upstream.ts +9 -1
  197. package/src/server/startup-action-control.ts +22 -2
  198. package/src/server/startup-health-cache.ts +4 -0
  199. package/src/server/system-env-shell.ts +7 -2
  200. package/src/server/system-env.ts +11 -1
  201. package/src/service/cli.ts +5 -1
  202. package/src/service/desktop-startup.ts +147 -34
  203. package/src/service/desktop-supervision.d.mts +27 -1
  204. package/src/service/desktop-supervision.mjs +111 -5
  205. package/src/service/health-sentinel-launch.ts +176 -0
  206. package/src/service/health-sentinel-main.ts +447 -0
  207. package/src/service/health-sentinel-policy.ts +241 -0
  208. package/src/service/health-sentinel-probe.ts +54 -0
  209. package/src/service/health-sentinel-windows.ts +87 -0
  210. package/src/service/linux-linger.ts +20 -0
  211. package/src/types/config.ts +5 -0
  212. package/src/usage/expected-prices.ts +4 -0
  213. package/src/usage/log.ts +19 -1
  214. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  215. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  216. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
@@ -1,3 +1,4 @@
1
+ import { deferPhysicalSendReporter } from "./deferred-send-reporter";
1
2
  import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
2
3
  import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
3
4
  import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
@@ -23,11 +24,13 @@ import {
23
24
  sealRequestAttemptIdentity,
24
25
  recordAttemptCredentialSource,
25
26
  } from "../request-log";
26
- import { clientCancelledResponse, readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
27
+ import { anthropicPoolCooldownResponse, clientCancelledResponse, readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
28
+ import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
27
29
  import { redactSecretString } from "../../lib/redact";
28
30
  import { rewriteUpstreamPolicyRefusal } from "./policy-refusal";
29
31
  import { withProviderRequestSlot } from "../../providers/request-pacing";
30
32
  import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
33
+ import { isAnthropicBillingKeyPool, rotateAnthropicBillingKeyOnResponse } from "../../providers/anthropic-billing-recovery";
31
34
  import {
32
35
  transientRetryPolicyFor,
33
36
  resetReplayPolicyFor,
@@ -91,7 +94,8 @@ import {
91
94
  } from "../../providers/opencode-zen-rate-limit";
92
95
  import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
93
96
  import { consumeComboFailure } from "./core-combo-failure";
94
- import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
97
+ import { streamingContextOverflowResponse, jsonContextOverflowResponse, lateAttributionLimitResponse } from "./context-overflow";
98
+ import { OllamaLateAttributionBudgetError } from "../../adapters/ollama-native";
95
99
  import { isFixedCodexAccount } from "./core-codex-account";
96
100
  import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-model-fallback";
97
101
  import {
@@ -341,6 +345,14 @@ export async function prepareAdapterExchange(
341
345
  cleanupUpstreamAbort();
342
346
  upstream.abort();
343
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
+ }
344
356
  const msg = err instanceof Error ? err.message : String(err);
345
357
  return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
346
358
  }
@@ -362,6 +374,23 @@ export async function prepareAdapterExchange(
362
374
  : transportState.oauth401ReplayAttempted ? "oauth-401" : undefined
363
375
  : undefined;
364
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;
365
394
  let handoffPermit: ResponsesSendBudget["pendingHopPermit"];
366
395
  try {
367
396
  if (transportState.activeAdapter.fetchResponse) {
@@ -434,6 +463,8 @@ export async function prepareAdapterExchange(
434
463
  const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
435
464
  ? fetchWithTransientRetry
436
465
  : fetchWithResetRetry;
466
+ const initialReporter = transientSendReporter(compactPrepaid ?? options.comboDispatchPermit);
467
+ const deferredInitial = billingPoolRequest && !receiptMode ? deferPhysicalSendReporter(initialReporter) : undefined;
437
468
  try { upstreamResponse = await fetchWithRetryPolicy(
438
469
  recovery => {
439
470
  // Unconfigured generic sends may omit the counting reporter. Capture policy
@@ -442,10 +473,11 @@ export async function prepareAdapterExchange(
442
473
  poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
443
474
  });
444
475
  // Receipt mode books the physical send at executor admission instead of here.
445
- if (!receiptMode && compactPrepaid && !compactPrepaidUsed) {
476
+ if (!deferredInitial && !receiptMode && compactPrepaid && !compactPrepaidUsed) {
446
477
  if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
447
478
  compactPrepaidUsed = true;
448
479
  }
480
+ // Key transport keeps this metadata pending until it binds the actual selection.
449
481
  transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery ?? initialRecovery);
450
482
  return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
451
483
  method: builtInitialRequest.method,
@@ -457,6 +489,13 @@ export async function prepareAdapterExchange(
457
489
  providerName: route.providerName,
458
490
  modelId: route.modelId,
459
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
+ }
460
499
  // Preserve the unconfigured direct-adapter accounting contract; Combo admission
461
500
  // and explicit retry/compaction budgets require a receipt for their booked send.
462
501
  if (receiptMode) {
@@ -473,6 +512,7 @@ export async function prepareAdapterExchange(
473
512
  label: safeHostLabel(builtInitialRequest.url),
474
513
  claimAmbiguousResend: claimPreHeaderResend,
475
514
  ...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
515
+ || billingPoolRequest
476
516
  ? {
477
517
  // A pending compaction permit already booked this leg's first physical send.
478
518
  ...(transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
@@ -485,7 +525,7 @@ export async function prepareAdapterExchange(
485
525
  // settled by `onPhysicalDispatch`, so a reporter here would count them twice.
486
526
  ...(receiptMode
487
527
  ? {}
488
- : { onSendsConsumed: transientSendReporter(compactPrepaid ?? options.comboDispatchPermit) }),
528
+ : { onSendsConsumed: deferredInitial?.report ?? initialReporter }),
489
529
  }
490
530
  : {}),
491
531
  },
@@ -516,8 +556,7 @@ export async function prepareAdapterExchange(
516
556
  return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal));
517
557
  }
518
558
  if (refusal instanceof AnthropicAccountCooldownError) {
519
- return formatErrorResponse(429, "rate_limit_error", refusal.message,
520
- refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
559
+ return anthropicPoolCooldownResponse(refusal, { comboAttempt: options.comboAttempt, logCtx });
521
560
  }
522
561
  // A budget refusal is a decision this process made, not an upstream fault. Reporting it as
523
562
  // 502 does more than mislabel it: the Codex client retries 5xx and does not retry a 429, so
@@ -604,13 +643,26 @@ export async function prepareAdapterExchange(
604
643
  recordAdapterReasoning(logCtx, retryRequest);
605
644
  recordAdapterTier(logCtx, retryRequest);
606
645
  } catch (err) {
607
- 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
+ }
608
652
  // A rotated/rebuilt adapter build failure is a request-shaping error, not an
609
- // upstream connect failure: tear the abort link down and map it as 400 (no 413
610
- // 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.
611
657
  cleanupUpstreamAbort();
612
658
  upstream.abort();
613
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
+ }
614
666
  const msg = err instanceof Error ? err.message : String(err);
615
667
  return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(msg)) };
616
668
  }
@@ -673,7 +725,7 @@ export async function prepareAdapterExchange(
673
725
  const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
674
726
  ? fetchWithTransientRetry
675
727
  : fetchWithResetRetry;
676
- const helperCountsSends = sendBudgetState.adapterSendBudget?.spendEnforced === true || refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
728
+ const helperCountsSends = genericRefetchCountsSends();
677
729
  const prepaid = sendBudgetState.pendingHopPermit;
678
730
  const configuredTotal = refetchTransientPolicy?.attempts;
679
731
  const refetchCap = transientSendCapFor(configuredTotal, sendBudgetState.targetSendsUsed);
@@ -689,27 +741,41 @@ export async function prepareAdapterExchange(
689
741
  { allowFinalRecoveryReserve: configuredTotal === undefined },
690
742
  )
691
743
  : undefined;
744
+ let refetchPermitUsed = false;
745
+ const refetchReporter = refetchAllowance ? transientSendReporter(refetchAllowance.permit) : undefined;
746
+ const deferredRefetch = billingPoolRequest && refetchReporter ? deferPhysicalSendReporter(refetchReporter) : undefined;
692
747
  try {
693
748
  return await refetchWithPolicy(
694
749
  recoveryKind => {
695
750
  sendBudgetState.adapterSendBudget?.startRequest?.({
696
751
  poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
697
752
  });
698
- if (refetchAllowance?.permit && !refetchAllowance.permit.use()) {
699
- throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
700
- }
701
- transportState.noteRoutedAttemptSend(retryEstimate, recoveryKind ?? recovery);
702
- // Same boundary on the helper path: the thunk is what reaches the wire, and it
703
- // can be refused above before it does. use() past the first attempt is a no-op.
704
- onDispatch?.();
705
- if (preserveFailureResponse) replacementAdmitted = true;
706
- 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();
707
769
  return fetchWithHeaderTimeout(retryRequest.url,
708
770
  applyUpstreamRecoveryInit({
709
771
  method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
710
772
  }, recoveryKind), upstream.signal, connectMs, parsed.stream,
711
773
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
712
774
  dispatchOverride: oauthDispatch(retryRequest),
775
+ onPhysicalDispatch: deferredRefetch ? () => {
776
+ if (!deferredRefetch.beforeDispatch()) throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
777
+ confirmDispatch();
778
+ } : undefined,
713
779
  providerName: route.providerName,
714
780
  modelId: route.modelId,
715
781
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
@@ -723,14 +789,14 @@ export async function prepareAdapterExchange(
723
789
  ...(refetchAllowance
724
790
  ? {
725
791
  attempts: refetchAllowance.attempts,
726
- onSendsConsumed: transientSendReporter(refetchAllowance.permit),
792
+ onSendsConsumed: deferredRefetch?.report ?? refetchReporter,
727
793
  }
728
794
  : {}),
729
795
  },
730
796
  );
731
797
  } finally {
732
- // Refunds only a reservation whose send never happened -- an abort settled before
733
- // 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.
734
800
  refetchAllowance?.permit?.release();
735
801
  }
736
802
  } finally {
@@ -768,8 +834,7 @@ export async function prepareAdapterExchange(
768
834
  return { failed: formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal)) };
769
835
  }
770
836
  if (refusal instanceof AnthropicAccountCooldownError) {
771
- return { failed: formatErrorResponse(429, "rate_limit_error", refusal.message,
772
- refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) }) };
837
+ return { failed: anthropicPoolCooldownResponse(refusal, { comboAttempt: options.comboAttempt, logCtx }) };
773
838
  }
774
839
  // Same rule on the recovery leg: the ladder refused to send again, so the answer names
775
840
  // this proxy rather than the provider it never reached.
@@ -980,32 +1045,83 @@ export async function prepareAdapterExchange(
980
1045
  promptCacheKey: parsed.options.promptCacheKey,
981
1046
  });
982
1047
  if (!rotated) break;
983
- // Release the failed response's socket before retrying; unread bodies otherwise linger
984
- // until runtime cleanup (one per rotated key).
985
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
986
- route.provider = rotated;
987
- const selectionFailure = await resolveRotatedCopilotSelection();
988
- if (selectionFailure) return selectionFailure;
989
- invalidateSameTargetRequest();
990
- transportState.activeAdapter = resolveSelectionAdapter(
991
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
992
- config.cacheRetention,
993
- );
994
- bindRouteReasoningReplayScope({
995
- parsed,
996
- providerName: route.providerName,
997
- provider: route.provider,
998
- adapterName: transportState.activeAdapter.name,
999
- });
1000
- const result = await rebuildAndRefetch("key-401");
1001
- if ("failed" in result) return result.failed;
1002
- upstreamResponse = result;
1003
- // A recovery refetch can itself die on an ambiguous pre-header reset, and the refusal
1004
- // that answers it is a 429. Every arm below keys on 429, so letting it fall through
1005
- // hands the marked refusal to the next waiting arm and replays the send it exists to
1006
- // stop. Re-enter the loop guard instead, which returns it unchanged.
1007
- if (isNonReplayableResponse(upstreamResponse)) continue recovery;
1008
- }
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
+ }
1009
1125
 
1010
1126
  // Anthropic fast mode refused (no usage credits, organization not enabled, model outside
1011
1127
  // the lane, fast pool empty): resend once at standard speed, as Claude Code does. This
@@ -1024,7 +1140,7 @@ export async function prepareAdapterExchange(
1024
1140
  const hop = reserveCredentialHop(
1025
1141
  "repair",
1026
1142
  `${route.providerName}|${route.modelId}|anthropic-fast-downgrade`,
1027
- !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
1143
+ !adapterOwnsDispatch && genericRefetchCountsSends(),
1028
1144
  );
1029
1145
  if (hop.allowed) {
1030
1146
  anthropicFastDowngradeGuard.attempted = true;
@@ -1093,7 +1209,9 @@ export async function prepareAdapterExchange(
1093
1209
  upstreamResponse = result;
1094
1210
  // The refusal is a 429 too: without this the while condition is still true and the
1095
1211
  // next configured attempt replays it on the same target.
1096
- if (isNonReplayableResponse(upstreamResponse)) continue recovery;
1212
+ if (isNonReplayableResponse(upstreamResponse)
1213
+ || (isAnthropicBillingKeyPool(route.provider)
1214
+ && [400, 402].includes(upstreamResponse.status))) continue recovery;
1097
1215
  }
1098
1216
 
1099
1217
  // Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
@@ -1128,30 +1246,53 @@ export async function prepareAdapterExchange(
1128
1246
  quotaResetAt,
1129
1247
  });
1130
1248
  if (!rotated) break;
1131
- // Release the failed response's socket before retrying; unread bodies otherwise linger
1132
- // until runtime cleanup (one per rotated key under a rate-limit storm).
1133
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1134
- route.provider = rotated;
1135
- const selectionFailure = await resolveRotatedCopilotSelection();
1136
- if (selectionFailure) return selectionFailure;
1137
- invalidateSameTargetRequest();
1138
- transportState.activeAdapter = resolveSelectionAdapter(
1139
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1140
- config.cacheRetention,
1141
- );
1142
- bindRouteReasoningReplayScope({
1143
- parsed,
1144
- providerName: route.providerName,
1145
- provider: route.provider,
1146
- adapterName: transportState.activeAdapter.name,
1147
- });
1148
- const result = await rebuildAndRefetch("key-429");
1149
- if ("failed" in result) return result.failed;
1150
- upstreamResponse = result;
1151
- // Rotating on the refusal would also write a cooldown against a key that rate-limited
1152
- // nothing, which outlives the request.
1153
- if (isNonReplayableResponse(upstreamResponse)) continue recovery;
1154
- }
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
+ }
1155
1296
 
1156
1297
  // Anthropic OAuth: recover a rate limit or proven account entitlement refusal
1157
1298
  // before output, within the shared request and account rotation limits.
@@ -1539,9 +1680,13 @@ export async function prepareAdapterExchange(
1539
1680
  imageRetryAttempted = true;
1540
1681
  transportState.imageTierBias = 1;
1541
1682
  invalidateSameTargetRequest();
1542
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1543
- const result = await rebuildAndRefetch("image-413");
1544
- 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 */ }
1545
1690
  upstreamResponse = result;
1546
1691
  continue recovery;
1547
1692
  }
@@ -1658,6 +1803,14 @@ export async function prepareAdapterExchange(
1658
1803
  requestState.subagentFallbackAccountId,
1659
1804
  );
1660
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 });
1661
1814
  // Upstreams occasionally echo request details in error bodies — scrub token-shaped
1662
1815
  // material before it reaches the client-facing error surface.
1663
1816
  const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");