@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.
- package/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +47 -15
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +5 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +26 -1
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +57 -4
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +44 -12
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/resolve.ts +1 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +47 -17
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +373 -217
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +293 -57
- package/src/grok/reset-coupons.ts +15 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +25 -16
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +3 -1
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +20 -20
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +20 -3
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +21 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +33 -5
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +333 -57
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +92 -390
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +7 -5
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +4 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +243 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +293 -81
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +129 -50
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +130 -64
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +131 -13
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +16 -2
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +5 -1
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
- package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
565
|
-
// translator-budget
|
|
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 =
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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:
|
|
792
|
+
onSendsConsumed: deferredRefetch?.report ?? refetchReporter,
|
|
682
793
|
}
|
|
683
794
|
: {}),
|
|
684
795
|
},
|
|
685
796
|
);
|
|
686
797
|
} finally {
|
|
687
|
-
//
|
|
688
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
);
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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 && (
|
|
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)
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
);
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1484
|
-
const result = await rebuildAndRefetch("image-413");
|
|
1485
|
-
if ("failed" in result)
|
|
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");
|