@bitkyc08/opencodex 2.88.0 → 2.90.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/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +57 -10
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/combos/failover.ts +10 -2
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureAccountAutoPauseAttempt, wasAccountAutoPaused } from "../oauth/account-auto-pause";
|
|
1
2
|
import { createPhysicalSendReporter } from "../lib/request-execution-budget";
|
|
2
3
|
import { createInferenceSendBudget } from "./inference/context";
|
|
3
4
|
/**
|
|
@@ -120,7 +121,7 @@ import {
|
|
|
120
121
|
recordKeyWireAttemptUsage,
|
|
121
122
|
type RequestLogContext,
|
|
122
123
|
} from "./request-log";
|
|
123
|
-
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
|
|
124
|
+
import { fetchWithHeaderTimeout, physicalSendObserverInit, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
|
|
124
125
|
import { anthropicBillingPhysicalFetch } from "../providers/anthropic-billing-ownership";
|
|
125
126
|
import { rotateAnthropicBillingKeyOnResponse } from "../providers/anthropic-billing-recovery";
|
|
126
127
|
import { linkAbortSignal } from "./responses/core-lifetime";
|
|
@@ -545,6 +546,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
545
546
|
pendingDistributedFailure = undefined;
|
|
546
547
|
record?.();
|
|
547
548
|
};
|
|
549
|
+
/** Send the native Messages request under the transient retry policy, re-validating the selected OAuth account or API key at the dispatch boundary. */
|
|
548
550
|
const send = async (recovery?: "rate-limit-429" | "oauth-401" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
|
|
549
551
|
const remaining = remainingTransientSends();
|
|
550
552
|
if (requestTransientPolicy && remaining <= 0) {
|
|
@@ -616,6 +618,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
616
618
|
}
|
|
617
619
|
const releaseFamily = snapshot && oauthBinding ? anthropicModelQuotaFor(oauthBinding.instance).claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
|
|
618
620
|
if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
|
|
621
|
+
const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, activeProvider, { config, oauth: sendingBinding?.snapshot }, headers);
|
|
619
622
|
const selection = activeProvider._apiKeyAttempt;
|
|
620
623
|
let dispatched: Response;
|
|
621
624
|
const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
|
|
@@ -641,7 +644,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
641
644
|
anthropicBillingPhysicalFetch((activeProvider as OcxProviderTransport).fetch ?? execute, route.providerName, activeProvider),
|
|
642
645
|
route.providerName, activeProvider, wire.url, activeProvider.apiKeyTransport === "bearer" ? "authorization" : "x-api-key"),
|
|
643
646
|
wire.url,
|
|
644
|
-
applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
|
|
647
|
+
applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body, ...physicalSendObserverInit(observeAutoPause, wire.url) }, transportRecovery),
|
|
645
648
|
{ providerName: route.providerName, provider: activeProvider },
|
|
646
649
|
);
|
|
647
650
|
} finally {
|
|
@@ -659,7 +662,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
659
662
|
&& credentialGeneration(current.credential) === snapshot.generation
|
|
660
663
|
&& current.credential.accountId === providerAccountUuid) {
|
|
661
664
|
bindAnthropicRefusalCredentialForSend(dispatched, sendOwner, providerAccountUuid);
|
|
662
|
-
recordAnthropicAccountQuotaFromHeadersForInstance(nativeInstance!, snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
|
|
665
|
+
recordAnthropicAccountQuotaFromHeadersForInstance(nativeInstance!, snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId, snapshot.generation);
|
|
663
666
|
}
|
|
664
667
|
} catch { /* Passive observation must not fail the response. */ }
|
|
665
668
|
}
|
|
@@ -672,7 +675,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
672
675
|
? () => { recordDistributedResponseFailure(dispatched, config, route.providerName, req.signal); }
|
|
673
676
|
: undefined;
|
|
674
677
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
675
|
-
return dispatched;
|
|
678
|
+
return observeAutoPause ? observeAutoPause(dispatched) : dispatched;
|
|
676
679
|
},
|
|
677
680
|
}),
|
|
678
681
|
),
|
|
@@ -700,9 +703,19 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
700
703
|
let billingRecoveryActive = false;
|
|
701
704
|
const keySendAvailable = () => transientSendAvailable()
|
|
702
705
|
&& (!billingRecoveryActive || Math.max(physicalSendsUsed, physicalBudget.used) < physicalBudget.policy.maxTotalModelSends);
|
|
706
|
+
let autoPauseMoves = 0;
|
|
703
707
|
const recoveryPasses = activeProvider._apiKeyAttempt?.distributed ? activeProvider.apiKeyPool?.length ?? 1 : 1;
|
|
704
708
|
for (let pass = 0; ; pass++) {
|
|
705
709
|
const priorResponse = response;
|
|
710
|
+
for (; wasAccountAutoPaused(response) && !oauthBinding && config.providers[route.providerName]?.apiKey && autoPauseMoves < 3 && keySendAvailable(); autoPauseMoves++) {
|
|
711
|
+
const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...activeProvider, _apiKeyAttempt: undefined });
|
|
712
|
+
const next = fresh && (selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh);
|
|
713
|
+
if (!next) break;
|
|
714
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
715
|
+
rebuildFor(next);
|
|
716
|
+
response = await send("key-401");
|
|
717
|
+
}
|
|
718
|
+
|
|
706
719
|
// A credential-scoped 401 on a static key pool says nothing about the sibling keys.
|
|
707
720
|
while (response.status === 401 && distributedResponseOwnsKey && hasKeyPoolFailover(activeProvider) && keySendAvailable()) {
|
|
708
721
|
const rotated = rotateProviderTransportOn401(config, route.providerName, activeProvider, {
|
|
@@ -762,6 +775,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
762
775
|
if (response === priorResponse || isNonReplayableResponse(response) || !keySendAvailable()) break;
|
|
763
776
|
// Billing class transitions share the physical-send ceiling; distributed-only
|
|
764
777
|
// transitions additionally retain their original bounded pool-pass allowance.
|
|
778
|
+
if (wasAccountAutoPaused(response) && autoPauseMoves < 3) continue;
|
|
765
779
|
if (distributedResponseOwnsKey && (
|
|
766
780
|
(billingRecoveryActive && [401, 429].includes(response.status))
|
|
767
781
|
|| (activeProvider.adapter === "anthropic" && hasKeyPoolFailover(activeProvider) && [400, 402].includes(response.status))
|
|
@@ -835,6 +849,17 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
835
849
|
}
|
|
836
850
|
releaseRetainedRequest();
|
|
837
851
|
|
|
852
|
+
// The last pool account refused on spent quota and rotation found no replacement: answer the
|
|
853
|
+
// usage limit with its reset (#6991), as the bridge lane does, not the upstream "try again later".
|
|
854
|
+
const poolUsageLimit = response.status === 429 && oauthBinding
|
|
855
|
+
? anthropicRoutingFor(nativeInstance!).anthropicPoolUsageLimitAfterRefusal(Date.now(), oauthBinding.routeDecision, route.modelId)
|
|
856
|
+
: null;
|
|
857
|
+
if (poolUsageLimit) {
|
|
858
|
+
discard(response);
|
|
859
|
+
cleanupAbort();
|
|
860
|
+
logCtx.errorCode = "usage_limit_reached";
|
|
861
|
+
return retainUpstreamMessagesRequestId(localOAuthFailure(poolUsageLimit)!, response.headers);
|
|
862
|
+
}
|
|
838
863
|
if (!response.ok) {
|
|
839
864
|
let bodyText = "";
|
|
840
865
|
try {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* other.
|
|
14
14
|
*/
|
|
15
15
|
import { sniffImageDimensions } from "../adapters/anthropic-image-guard";
|
|
16
|
+
import { anthropicCacheWrite1hTokens } from "../adapters/anthropic/cache-retention";
|
|
16
17
|
import { createToolCallIdAllocator } from "../adapters/tool-call-id";
|
|
17
18
|
import { idleDeadline } from "../lib/abort";
|
|
18
19
|
import {
|
|
@@ -29,12 +30,13 @@ import type { RequestLogContext } from "./request-log";
|
|
|
29
30
|
import { recordGenerationEvent } from "./request-log-generation-window";
|
|
30
31
|
import type { OcxConfig } from "../types";
|
|
31
32
|
|
|
32
|
-
export function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
|
|
33
|
+
export function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number; cacheCreation1hInputTokens?: number } | undefined {
|
|
33
34
|
if (!usage) return undefined;
|
|
34
35
|
const num = (v: unknown) => typeof v === "number" ? v : 0;
|
|
35
36
|
const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
|
|
36
37
|
const read = num(usage.cache_read_input_tokens);
|
|
37
38
|
const write = num(usage.cache_creation_input_tokens);
|
|
39
|
+
const write1h = anthropicCacheWrite1hTokens(usage, write);
|
|
38
40
|
// Anthropic input_tokens excludes cache read/write; normalize to the canonical
|
|
39
41
|
// inclusive convention (types.ts OcxUsage / devlog 070). cached = READS only.
|
|
40
42
|
return {
|
|
@@ -44,6 +46,7 @@ export function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: numb
|
|
|
44
46
|
cachedInputTokens: read,
|
|
45
47
|
cacheReadInputTokens: read,
|
|
46
48
|
cacheCreationInputTokens: write,
|
|
49
|
+
...(write1h !== undefined ? { cacheCreation1hInputTokens: write1h } : {}),
|
|
47
50
|
} : {}),
|
|
48
51
|
};
|
|
49
52
|
}
|
|
@@ -982,7 +982,7 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
|
|
|
982
982
|
const raw = usage as {
|
|
983
983
|
input_tokens?: unknown;
|
|
984
984
|
output_tokens?: unknown;
|
|
985
|
-
input_tokens_details?: { cached_tokens?: unknown; cache_write_tokens?: unknown };
|
|
985
|
+
input_tokens_details?: { cached_tokens?: unknown; cache_write_tokens?: unknown; cache_write_1h_tokens?: unknown };
|
|
986
986
|
output_tokens_details?: { reasoning_tokens?: unknown };
|
|
987
987
|
total_tokens?: unknown;
|
|
988
988
|
prompt_tokens?: unknown;
|
|
@@ -1004,6 +1004,9 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
|
|
|
1004
1004
|
...(typeof raw.input_tokens_details?.cache_write_tokens === "number"
|
|
1005
1005
|
? { cacheCreationInputTokens: raw.input_tokens_details.cache_write_tokens }
|
|
1006
1006
|
: {}),
|
|
1007
|
+
...(typeof raw.input_tokens_details?.cache_write_1h_tokens === "number"
|
|
1008
|
+
? { cacheCreation1hInputTokens: raw.input_tokens_details.cache_write_1h_tokens }
|
|
1009
|
+
: {}),
|
|
1007
1010
|
...(typeof raw.output_tokens_details?.reasoning_tokens === "number"
|
|
1008
1011
|
? { reasoningOutputTokens: raw.output_tokens_details.reasoning_tokens }
|
|
1009
1012
|
: {}),
|
|
@@ -1717,8 +1720,10 @@ export function noteProviderAttemptSend(
|
|
|
1717
1720
|
if (attempt && attempt.sendCount > 0 && attemptAccountChanged(previous, next, attempt.provider, logCtx.provider)) {
|
|
1718
1721
|
// An input estimate is not evidence that a failed send used that many tokens.
|
|
1719
1722
|
delete attempt.inputTokenEstimate;
|
|
1723
|
+
// An account-scoped recovery kind names the status the sealed account answered.
|
|
1720
1724
|
finishRequestAttempt(attempt, attempt.status >= 100 ? attempt.status
|
|
1721
|
-
: recovery === "key-401" ? 401 : recovery
|
|
1725
|
+
: recovery === "key-401" || recovery === "oauth-401" ? 401 : recovery === "oauth-account-403" ? 403
|
|
1726
|
+
: recovery === "oauth-account-402" ? 402 : recovery?.includes("429") ? 429 : 502,
|
|
1722
1727
|
Date.now() - (logCtx.activeAttemptStartedAt ?? Date.now()), attempt.usage);
|
|
1723
1728
|
// This attempt is being sealed because a NAMED recovery rejected it, so the recovery kind
|
|
1724
1729
|
// is direct evidence here rather than an inference from history. Without this the sealed
|
|
@@ -1847,16 +1852,20 @@ export function finishRequestAttempt(
|
|
|
1847
1852
|
export function aggregateAttemptUsage(
|
|
1848
1853
|
attempts: readonly PersistedUsageAttempt[],
|
|
1849
1854
|
): FinalizedUsageResult {
|
|
1850
|
-
|
|
1851
|
-
|
|
1855
|
+
// A usage-less attempt that never reached upstream spent nothing (summary.ts `spentNothing`),
|
|
1856
|
+
// so it cannot leave the row's usage unknown.
|
|
1857
|
+
const measured = attempts.filter(attempt => attempt.usage
|
|
1858
|
+
|| (attempt.sendCount !== 0 && attempt.locallyAnswered !== true));
|
|
1859
|
+
const status: UsageStatus = measured.length > 0
|
|
1860
|
+
&& measured.every(attempt => attempt.usageStatus === "unsupported")
|
|
1852
1861
|
? "unsupported"
|
|
1853
|
-
:
|
|
1862
|
+
: measured.some(attempt => (
|
|
1854
1863
|
attempt.usageStatus === "unreported" || attempt.usageStatus === "unsupported"
|
|
1855
1864
|
))
|
|
1856
1865
|
? "unreported"
|
|
1857
|
-
:
|
|
1866
|
+
: measured.some(attempt => attempt.usageStatus === "estimated")
|
|
1858
1867
|
? "estimated"
|
|
1859
|
-
:
|
|
1868
|
+
: measured.length > 0
|
|
1860
1869
|
? "reported"
|
|
1861
1870
|
: "unreported";
|
|
1862
1871
|
|
|
@@ -1864,7 +1873,7 @@ export function aggregateAttemptUsage(
|
|
|
1864
1873
|
if (usages.length === 0) return { status };
|
|
1865
1874
|
|
|
1866
1875
|
const sumOptional = (
|
|
1867
|
-
key: "cachedInputTokens" | "cacheReadInputTokens" | "cacheCreationInputTokens"
|
|
1876
|
+
key: "cachedInputTokens" | "cacheReadInputTokens" | "cacheCreationInputTokens" | "cacheCreation1hInputTokens"
|
|
1868
1877
|
| "reasoningOutputTokens" | "providerCredits",
|
|
1869
1878
|
): number | undefined => {
|
|
1870
1879
|
const present = usages.flatMap(usage => (
|
|
@@ -1873,8 +1882,12 @@ export function aggregateAttemptUsage(
|
|
|
1873
1882
|
return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) : undefined;
|
|
1874
1883
|
};
|
|
1875
1884
|
const cachedInputTokens = sumOptional("cachedInputTokens");
|
|
1876
|
-
|
|
1885
|
+
// Readers take `cacheReadInputTokens ?? cachedInputTokens` (usage/cost.ts), so once any attempt
|
|
1886
|
+
// reports the read field, an attempt that only reports `cachedInputTokens` must count in it too.
|
|
1887
|
+
const cacheReadInputTokens = sumOptional("cacheReadInputTokens") === undefined ? undefined
|
|
1888
|
+
: usages.reduce((sum, usage) => sum + (usage.cacheReadInputTokens ?? usage.cachedInputTokens ?? 0), 0);
|
|
1877
1889
|
const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
|
|
1890
|
+
const cacheCreation1hInputTokens = sumOptional("cacheCreation1hInputTokens");
|
|
1878
1891
|
const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
|
|
1879
1892
|
const providerCredits = sumOptional("providerCredits");
|
|
1880
1893
|
const totalTokens = usages.reduce(
|
|
@@ -1888,6 +1901,7 @@ export function aggregateAttemptUsage(
|
|
|
1888
1901
|
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
1889
1902
|
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
1890
1903
|
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
1904
|
+
...(cacheCreation1hInputTokens !== undefined ? { cacheCreation1hInputTokens } : {}),
|
|
1891
1905
|
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
1892
1906
|
...(providerCredits !== undefined ? { providerCredits } : {}),
|
|
1893
1907
|
...(status === "estimated" ? { estimated: true } : {}),
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
2
|
+
|
|
3
|
+
const moves = new WeakMap<object, number>();
|
|
4
|
+
/** One same-provider credential move, funded by the existing recovery owner. */
|
|
5
|
+
export async function recoverAutoPausedAccount<T>(options: {
|
|
6
|
+
requestKey: object;
|
|
7
|
+
remaining?: number;
|
|
8
|
+
reserve: ResponsesSendBudget["reserveCredentialHop"];
|
|
9
|
+
refresh: () => Promise<boolean>;
|
|
10
|
+
refetch: () => Promise<T>;
|
|
11
|
+
setPermit: (permit: ResponsesSendBudget["pendingHopPermit"]) => void;
|
|
12
|
+
}): Promise<T | undefined> {
|
|
13
|
+
const count = moves.get(options.requestKey) ?? 0;
|
|
14
|
+
if (count >= 3 || options.remaining !== undefined && options.remaining <= 0) return;
|
|
15
|
+
const hop = options.reserve("auth-recovery", "account-auto-pause", true);
|
|
16
|
+
if (!hop.allowed) return;
|
|
17
|
+
try {
|
|
18
|
+
if (!await options.refresh()) return;
|
|
19
|
+
moves.set(options.requestKey, count + 1);
|
|
20
|
+
options.setPermit(hop.permit);
|
|
21
|
+
return await options.refetch();
|
|
22
|
+
} finally {
|
|
23
|
+
options.setPermit(undefined);
|
|
24
|
+
hop.permit?.release();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
fetchWithTransientRetry,
|
|
29
29
|
fetchWithResetRetry,
|
|
30
30
|
applyUpstreamRecoveryInit,
|
|
31
|
-
isNonReplayableResponse,
|
|
31
|
+
isNonReplayableResponse, isSameTargetRetryHold,
|
|
32
32
|
prepareSameTarget429Wait,
|
|
33
33
|
SendBudgetExhaustedError,
|
|
34
34
|
} from "../../lib/upstream-retry";
|
|
@@ -349,7 +349,7 @@ export function createAdapterContinuations(
|
|
|
349
349
|
&& !kiroReplaySetupFailed
|
|
350
350
|
// A synthesized replay refusal is not a rate limit; replaying the continuation on
|
|
351
351
|
// it would re-send a turn whose first send may already have been processed.
|
|
352
|
-
&& !isNonReplayableResponse(response)
|
|
352
|
+
&& !isNonReplayableResponse(response) && !isSameTargetRetryHold(response)
|
|
353
353
|
&& rateLimitPolicy !== null
|
|
354
354
|
&& adapterExchange.rateLimitRetries < rateLimitPolicy.attempts
|
|
355
355
|
// The main recovery loop and the passthrough ladder both consult the shared remainder
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { recoverAutoPausedAccount } from "./account-auto-pause-recovery";
|
|
2
|
+
import { wasAccountAutoPaused, wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
|
|
1
3
|
import { bindAnthropicRejectionResponse } from "../../adapters/anthropic-rejection-memo";
|
|
2
4
|
import { deferPhysicalSendReporter } from "./deferred-send-reporter";
|
|
5
|
+
import { usesApiKeyAccount } from "../../providers/label";
|
|
3
6
|
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
4
7
|
import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
|
|
5
8
|
import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
|
|
6
9
|
import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
7
10
|
import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
|
|
8
11
|
import { transientSendCapFor } from "./request-send-budget";
|
|
9
|
-
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
12
|
+
import { isNonReplayableResponse, isSameTargetRetryHold } from "../../lib/upstream-retry";
|
|
10
13
|
import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
|
|
11
14
|
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
12
15
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
@@ -141,6 +144,7 @@ export async function prepareAdapterExchange(
|
|
|
141
144
|
| "sameTargetToken"
|
|
142
145
|
| "transportToken"
|
|
143
146
|
| "oauthDispatch"
|
|
147
|
+
| "recoverAutoPausedSelection"
|
|
144
148
|
| "imageTierBias"
|
|
145
149
|
| "oauth401ReplayAttempted"
|
|
146
150
|
| "isOAuth401ReplayProvider"
|
|
@@ -394,11 +398,12 @@ export async function prepareAdapterExchange(
|
|
|
394
398
|
upstreamResponse = new Response(text, { status: original.status, statusText: original.statusText, headers });
|
|
395
399
|
return upstreamResponse;
|
|
396
400
|
};
|
|
401
|
+
const autoPauseEnabled = config.providers[route.providerName]?.accountAutoPause?.enabled === true;
|
|
397
402
|
const billingPoolRequest = isAnthropicBillingKeyPool(route.provider);
|
|
398
403
|
let billingRecoveryActive = false;
|
|
399
|
-
const genericRefetchCountsSends = () => sendBudgetState.adapterSendBudget?.spendEnforced === true
|
|
404
|
+
const genericRefetchCountsSends = () => options.comboAttempt === true || sendBudgetState.adapterSendBudget?.spendEnforced === true
|
|
400
405
|
|| transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null
|
|
401
|
-
|| billingPoolRequest || billingRecoveryActive;
|
|
406
|
+
|| billingPoolRequest || billingRecoveryActive || autoPauseEnabled;
|
|
402
407
|
let handoffPermit: ResponsesSendBudget["pendingHopPermit"];
|
|
403
408
|
try {
|
|
404
409
|
if (transportState.activeAdapter.fetchResponse) {
|
|
@@ -520,10 +525,10 @@ export async function prepareAdapterExchange(
|
|
|
520
525
|
label: safeHostLabel(builtInitialRequest.url),
|
|
521
526
|
claimAmbiguousResend: claimPreHeaderResend,
|
|
522
527
|
...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
|
|
523
|
-
|| billingPoolRequest
|
|
528
|
+
|| billingPoolRequest || autoPauseEnabled
|
|
524
529
|
? {
|
|
525
530
|
// A pending compaction permit already booked this leg's first physical send.
|
|
526
|
-
...(transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
|
|
531
|
+
...(transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt || autoPauseEnabled
|
|
527
532
|
? {
|
|
528
533
|
attempts: Math.min(initialSendCap,
|
|
529
534
|
sendBudgetState.initialSendAllowance(initialSendCap) + (compactPrepaid ? 1 : 0)),
|
|
@@ -758,7 +763,9 @@ export async function prepareAdapterExchange(
|
|
|
758
763
|
: undefined;
|
|
759
764
|
let refetchPermitUsed = false;
|
|
760
765
|
const refetchReporter = refetchAllowance ? transientSendReporter(refetchAllowance.permit) : undefined;
|
|
761
|
-
const deferredRefetch = billingPoolRequest
|
|
766
|
+
const deferredRefetch = (billingPoolRequest || options.comboAttempt) && refetchReporter
|
|
767
|
+
? deferPhysicalSendReporter(refetchReporter) : undefined;
|
|
768
|
+
const pendingKeyMetadata = deferredRefetch && usesApiKeyAccount(route.provider);
|
|
762
769
|
try {
|
|
763
770
|
return await refetchWithPolicy(
|
|
764
771
|
recoveryKind => {
|
|
@@ -767,14 +774,14 @@ export async function prepareAdapterExchange(
|
|
|
767
774
|
});
|
|
768
775
|
// Prepare key metadata before the guarded transport commits its attempt;
|
|
769
776
|
// counters and permits still wait for the physical executor after pacing.
|
|
770
|
-
if (
|
|
777
|
+
if (pendingKeyMetadata) transportState.noteRoutedAttemptSend(retryEstimate, recoveryKind ?? recovery);
|
|
771
778
|
const confirmDispatch = () => {
|
|
772
779
|
if (refetchAllowance?.permit && !refetchPermitUsed) {
|
|
773
780
|
if (!refetchAllowance.permit.use()) throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
|
|
774
781
|
refetchPermitUsed = true;
|
|
775
782
|
}
|
|
776
|
-
if (!
|
|
777
|
-
//
|
|
783
|
+
if (!pendingKeyMetadata) transportState.noteRoutedAttemptSend(retryEstimate, recoveryKind ?? recovery);
|
|
784
|
+
// Counted Combo/pool sends confirm at the physical executor; legacy
|
|
778
785
|
// helper paths retain their existing thunk admission boundary.
|
|
779
786
|
onDispatch?.();
|
|
780
787
|
if (preserveFailureResponse) replacementAdmitted = true;
|
|
@@ -887,7 +894,7 @@ export async function prepareAdapterExchange(
|
|
|
887
894
|
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
888
895
|
const hop = reserveCredentialHop("auth-recovery",
|
|
889
896
|
`${route.providerName}|${route.modelId}|antigravity-auth`,
|
|
890
|
-
!adapterOwnsDispatch && (
|
|
897
|
+
!adapterOwnsDispatch && genericRefetchCountsSends());
|
|
891
898
|
if (!hop.allowed) return null;
|
|
892
899
|
try {
|
|
893
900
|
const snapshot = await failoverAccountSnapshot(route.providerName, nextId);
|
|
@@ -923,6 +930,15 @@ export async function prepareAdapterExchange(
|
|
|
923
930
|
};
|
|
924
931
|
// Keep recovery kinds in sync with the native Responses `passthroughRecovery:` loop above.
|
|
925
932
|
recovery: for (;;) {
|
|
933
|
+
if (wasAccountAutoPaused(upstreamResponse) && route.provider.authMode !== "forward") {
|
|
934
|
+
const recovered = await recoverAutoPausedAccount({ requestKey: transportState, reserve: reserveCredentialHop,
|
|
935
|
+
remaining: transientSendCapFor(transientRetryPolicyFor(route.provider)?.attempts, sendBudgetState.targetSendsUsed),
|
|
936
|
+
refresh: () => transportState.recoverAutoPausedSelection(parsed), refetch: () => rebuildAndRefetch("oauth-account-429"), setPermit: permit => { sendBudgetState.pendingHopPermit = permit; } });
|
|
937
|
+
if (!recovered) break;
|
|
938
|
+
if ("failed" in recovered) return recovered.failed;
|
|
939
|
+
upstreamResponse = recovered; continue recovery;
|
|
940
|
+
}
|
|
941
|
+
|
|
926
942
|
// Preserve the terminal verdict through adapter and combo error formatting.
|
|
927
943
|
// This also covers a reset reached by a 401/429/413 recovery refetch.
|
|
928
944
|
if (isNonReplayableResponse(upstreamResponse)) {
|
|
@@ -942,7 +958,7 @@ export async function prepareAdapterExchange(
|
|
|
942
958
|
continue recovery;
|
|
943
959
|
}
|
|
944
960
|
if (
|
|
945
|
-
upstreamResponse.status === 401
|
|
961
|
+
upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)
|
|
946
962
|
&& isOAuth401ReplayProvider
|
|
947
963
|
&& transportState.sentOAuthSnapshot
|
|
948
964
|
&& !oauth401ReplayAttempted
|
|
@@ -978,7 +994,7 @@ export async function prepareAdapterExchange(
|
|
|
978
994
|
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
979
995
|
const hop = reserveCredentialHop("auth-recovery",
|
|
980
996
|
`${route.providerName}|${route.modelId}|terminal-refresh-account`,
|
|
981
|
-
!adapterOwnsDispatch && (
|
|
997
|
+
!adapterOwnsDispatch && genericRefetchCountsSends());
|
|
982
998
|
if (hop.allowed) {
|
|
983
999
|
try {
|
|
984
1000
|
const admitted = await applyFailoverSnapshot(alternate);
|
|
@@ -1062,7 +1078,7 @@ export async function prepareAdapterExchange(
|
|
|
1062
1078
|
continue recovery;
|
|
1063
1079
|
}
|
|
1064
1080
|
|
|
1065
|
-
if (route.providerName === "google-antigravity" && upstreamResponse.status === 401
|
|
1081
|
+
if (route.providerName === "google-antigravity" && upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)
|
|
1066
1082
|
&& oauth401ReplayAttempted && !antigravityAuthRotationAttempted
|
|
1067
1083
|
&& !isNonReplayableResponse(upstreamResponse)) {
|
|
1068
1084
|
antigravityAuthRotationAttempted = true;
|
|
@@ -1223,7 +1239,7 @@ export async function prepareAdapterExchange(
|
|
|
1223
1239
|
// replay is lossless. Runs before key failover so "primary-first" setups keep the same
|
|
1224
1240
|
// key on rate-limit blips; only after the attempts are exhausted does failover run.
|
|
1225
1241
|
while (
|
|
1226
|
-
upstreamResponse.status === 429
|
|
1242
|
+
upstreamResponse.status === 429 && !isSameTargetRetryHold(upstreamResponse)
|
|
1227
1243
|
&& rateLimitPolicy !== null
|
|
1228
1244
|
&& rateLimitRetries < rateLimitPolicy.attempts
|
|
1229
1245
|
&& !sendBudgetExhausted()
|
|
@@ -1348,12 +1364,31 @@ export async function prepareAdapterExchange(
|
|
|
1348
1364
|
&& anthropicInstance
|
|
1349
1365
|
&& transportState.anthropicPoolAccountId
|
|
1350
1366
|
) {
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1367
|
+
// Same admission as the other rotation ladders: the roster cap cannot see the shared
|
|
1368
|
+
// send budget. A canRetry term, not a break -- the call also records the refusal.
|
|
1369
|
+
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
1370
|
+
const hop = reserveCredentialHop(
|
|
1371
|
+
"auth-recovery",
|
|
1372
|
+
`${route.providerName}|${route.modelId}|anthropic-pool-rotation`,
|
|
1373
|
+
!adapterOwnsDispatch && genericRefetchCountsSends(),
|
|
1374
|
+
);
|
|
1375
|
+
let nextAccountId: string | null;
|
|
1376
|
+
try {
|
|
1377
|
+
nextAccountId = await rotateAnthropicAccountOnResponseForInstance(anthropicInstance, upstreamResponse, {
|
|
1378
|
+
config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
|
|
1379
|
+
model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, currentDecision: currentAnthropicRouteDecision, signal: upstream.signal,
|
|
1380
|
+
// A denied reservation must not rotate on base headroom its bounds cannot see.
|
|
1381
|
+
canRetry: hop.allowed && (hop.permit !== undefined || !sendBudgetExhausted())
|
|
1382
|
+
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
1383
|
+
});
|
|
1384
|
+
} catch (rotationError) {
|
|
1385
|
+
hop.permit?.release();
|
|
1386
|
+
throw rotationError;
|
|
1387
|
+
}
|
|
1388
|
+
if (!nextAccountId) {
|
|
1389
|
+
hop.permit?.release();
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1357
1392
|
try {
|
|
1358
1393
|
const admitted = await commitResolvedOAuthSelection(await anthropicRoutingFor(anthropicInstance).getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
1359
1394
|
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
@@ -1376,12 +1411,26 @@ export async function prepareAdapterExchange(
|
|
|
1376
1411
|
});
|
|
1377
1412
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
1378
1413
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1414
|
+
// The replay IS this hop's send; the dispatching layer settles the reservation.
|
|
1415
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
1416
|
+
let result: Response | { failed: Response };
|
|
1417
|
+
try {
|
|
1418
|
+
result = await rebuildAndRefetch(upstreamResponse.status === 401 ? "oauth-401"
|
|
1419
|
+
: upstreamResponse.status === 403 ? "oauth-account-403" : "anthropic-oauth-429",
|
|
1420
|
+
() => { if (!adapterOwnsDispatch) hop.permit?.use(); });
|
|
1421
|
+
} finally {
|
|
1422
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
1423
|
+
}
|
|
1424
|
+
if ("failed" in result) {
|
|
1425
|
+
// A no-op once the boundary was reached; a refund when the rebuild died first.
|
|
1426
|
+
hop.permit?.release();
|
|
1427
|
+
return result.failed;
|
|
1428
|
+
}
|
|
1429
|
+
upstreamResponse = result;
|
|
1383
1430
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
1384
|
-
|
|
1431
|
+
} catch {
|
|
1432
|
+
// release() refunds a pre-send hop and is a no-op once the send settled it.
|
|
1433
|
+
hop.permit?.release();
|
|
1385
1434
|
break;
|
|
1386
1435
|
}
|
|
1387
1436
|
}
|
|
@@ -1421,9 +1470,9 @@ export async function prepareAdapterExchange(
|
|
|
1421
1470
|
const hop = reserveCredentialHop(
|
|
1422
1471
|
"auth-recovery",
|
|
1423
1472
|
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
|
|
1424
|
-
//
|
|
1473
|
+
// Counted generic helpers report physical sends; an adapter-owned
|
|
1425
1474
|
// ladder settles its own booking.
|
|
1426
|
-
!adapterOwnsDispatch && (
|
|
1475
|
+
!adapterOwnsDispatch && genericRefetchCountsSends(),
|
|
1427
1476
|
);
|
|
1428
1477
|
if (!hop.allowed) break;
|
|
1429
1478
|
const nextAccountId = rotateGenericOAuthAccountOnRefusal(
|
|
@@ -1525,9 +1574,9 @@ export async function prepareAdapterExchange(
|
|
|
1525
1574
|
const hop = reserveCredentialHop(
|
|
1526
1575
|
"auth-recovery",
|
|
1527
1576
|
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
|
|
1528
|
-
//
|
|
1577
|
+
// Counted generic helpers report physical sends; an adapter-owned
|
|
1529
1578
|
// ladder settles its own booking.
|
|
1530
|
-
!adapterOwnsDispatch && (
|
|
1579
|
+
!adapterOwnsDispatch && genericRefetchCountsSends(),
|
|
1531
1580
|
);
|
|
1532
1581
|
if (!hop.allowed) break;
|
|
1533
1582
|
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
@@ -1646,7 +1695,7 @@ export async function prepareAdapterExchange(
|
|
|
1646
1695
|
const hop = reserveCredentialHop(
|
|
1647
1696
|
"auth-recovery",
|
|
1648
1697
|
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-verify`,
|
|
1649
|
-
!adapterOwnsDispatch && (
|
|
1698
|
+
!adapterOwnsDispatch && genericRefetchCountsSends(),
|
|
1650
1699
|
);
|
|
1651
1700
|
if (!hop.allowed) break;
|
|
1652
1701
|
// Auth-refusal rotation, not the rate-limit one: a verification refusal
|
|
@@ -458,6 +458,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
458
458
|
return;
|
|
459
459
|
}
|
|
460
460
|
if (terminal || settledPreOpen || signal?.aborted) return;
|
|
461
|
+
// New sockets have just validated their upgrade; retained sockets carry the
|
|
462
|
+
// same connection signal into a fresh exchange, never its predecessor's frames.
|
|
463
|
+
metadata?.seed(session.upgradeHeaders());
|
|
461
464
|
sent = true;
|
|
462
465
|
try {
|
|
463
466
|
if (nativeControl) {
|
|
@@ -6,11 +6,48 @@ export const CODEX_WS_METADATA_MAX_HEADERS = 128;
|
|
|
6
6
|
export const CODEX_WS_METADATA_MAX_VALUE_BYTES = 4096;
|
|
7
7
|
|
|
8
8
|
export type CodexWsQuotaObserver = (headers: Headers) => void;
|
|
9
|
+
const REASONING_INCLUDED_HEADER = "x-reasoning-included";
|
|
10
|
+
const HTTP_HEADER_TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
|
|
9
11
|
|
|
10
12
|
function record(value: unknown): value is Record<string, unknown> {
|
|
11
13
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/** Bun emits this payload before validating the upgrade; the session must wait for open. */
|
|
17
|
+
export function readCodexWsUpgradeHeaders(payload: unknown): Headers {
|
|
18
|
+
try {
|
|
19
|
+
if (!record(payload) || payload.statusCode !== 101 || !Array.isArray(payload.rawHeaders)
|
|
20
|
+
|| payload.rawHeaders.length % 2 !== 0 || payload.rawHeaders.length > CODEX_WS_METADATA_MAX_HEADERS * 2) return new Headers();
|
|
21
|
+
const headers = new Headers();
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
let connectionNominated = false;
|
|
24
|
+
for (let i = 0; i < payload.rawHeaders.length; i += 2) {
|
|
25
|
+
const name = payload.rawHeaders[i];
|
|
26
|
+
const value = payload.rawHeaders[i + 1];
|
|
27
|
+
if (typeof name !== "string" || typeof value !== "string"
|
|
28
|
+
|| name.length + value.length > CODEX_WS_METADATA_MAX_BYTES - bytes || !HTTP_HEADER_TOKEN.test(name)
|
|
29
|
+
|| /[\r\n\0]/.test(value)) return new Headers();
|
|
30
|
+
bytes += Buffer.byteLength(name) + Buffer.byteLength(value);
|
|
31
|
+
if (bytes > CODEX_WS_METADATA_MAX_BYTES) return new Headers();
|
|
32
|
+
const lower = name.toLowerCase();
|
|
33
|
+
if (lower === "connection" && value.split(",").some(token => token.trim().toLowerCase() === REASONING_INCLUDED_HEADER)) {
|
|
34
|
+
connectionNominated = true;
|
|
35
|
+
}
|
|
36
|
+
// Other allowlisted response headers (quota, request id, turn state, model)
|
|
37
|
+
// describe a response, not a reusable connection. Never retain them here.
|
|
38
|
+
if (lower === REASONING_INCLUDED_HEADER) {
|
|
39
|
+
if (Buffer.byteLength(value) > CODEX_WS_METADATA_MAX_VALUE_BYTES) return new Headers();
|
|
40
|
+
headers.append(lower, value);
|
|
41
|
+
if (Buffer.byteLength(headers.get(lower)!) > CODEX_WS_METADATA_MAX_VALUE_BYTES) return new Headers();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return connectionNominated ? new Headers() : headers;
|
|
45
|
+
} catch {
|
|
46
|
+
// Missing/unsupported or malformed optional metadata cannot authorize a retry.
|
|
47
|
+
return new Headers();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
function finiteNonnegative(value: unknown): value is number {
|
|
15
52
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
16
53
|
}
|
|
@@ -91,6 +128,13 @@ export class CodexWsMetadata {
|
|
|
91
128
|
|
|
92
129
|
snapshot(): Headers { return new Headers(this.headers); }
|
|
93
130
|
|
|
131
|
+
/** Seed only the connection-level inclusion signal; per-response frames still win. */
|
|
132
|
+
seed(headers: Headers): void {
|
|
133
|
+
if (this.ended || this.committed || this.headers.has(REASONING_INCLUDED_HEADER)) return;
|
|
134
|
+
const value = headers.get(REASONING_INCLUDED_HEADER);
|
|
135
|
+
if (value !== null) setMetadataHeader(this.headers, REASONING_INCLUDED_HEADER, value);
|
|
136
|
+
}
|
|
137
|
+
|
|
94
138
|
private publishQuota(headers: Headers): void {
|
|
95
139
|
// Observation is auxiliary bookkeeping; a consumer exception cannot turn
|
|
96
140
|
// a valid provider frame into a retryable transport failure.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readCodexWsUpgradeHeaders } from "./codex-ws-metadata";
|
|
2
|
+
|
|
1
3
|
export const MAX_CODEX_WS_SESSION_EXCHANGES = 32;
|
|
2
4
|
|
|
3
5
|
/** Owns one physical socket; request listeners belong to the exchange, not this object. */
|
|
@@ -8,10 +10,13 @@ export class CodexWsSession {
|
|
|
8
10
|
busy = false;
|
|
9
11
|
private owner?: (reason: Error) => void;
|
|
10
12
|
private readonly completedIds = new Set<string>();
|
|
13
|
+
private pendingUpgradeHeaders?: Headers;
|
|
14
|
+
private connectionHeaders?: Headers;
|
|
11
15
|
|
|
12
16
|
constructor(url: string, headers: Record<string, string>, readonly retainable = false,
|
|
13
17
|
private readonly changed: () => void = () => {}, proxy?: string) {
|
|
14
18
|
this.socket = new WebSocket(url, { headers, ...(proxy ? { proxy } : {}) } as unknown as string[]);
|
|
19
|
+
this.socket.addEventListener("handshake", this.onHandshake);
|
|
15
20
|
this.socket.addEventListener("open", this.onOpen);
|
|
16
21
|
this.socket.addEventListener("message", this.onIdleMessage);
|
|
17
22
|
this.socket.addEventListener("close", this.onClose);
|
|
@@ -20,6 +25,7 @@ export class CodexWsSession {
|
|
|
20
25
|
|
|
21
26
|
get reused(): boolean { return this.completedIds.size > 0; }
|
|
22
27
|
hasCompleted(id: string): boolean { return this.completedIds.has(id); }
|
|
28
|
+
upgradeHeaders(): Headers { return new Headers(this.opened && !this.closed ? this.connectionHeaders : undefined); }
|
|
23
29
|
|
|
24
30
|
reserve(): boolean {
|
|
25
31
|
if (this.closed || this.busy || (this.opened && this.socket.readyState !== undefined && this.socket.readyState !== 1)) return false;
|
|
@@ -71,7 +77,18 @@ export class CodexWsSession {
|
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
private
|
|
80
|
+
private onHandshake = (event: Event): void => {
|
|
81
|
+
if (this.opened || this.closed) return;
|
|
82
|
+
this.pendingUpgradeHeaders = readCodexWsUpgradeHeaders((event as MessageEvent<unknown>).data);
|
|
83
|
+
};
|
|
84
|
+
private onOpen = (): void => {
|
|
85
|
+
if (this.closed) return;
|
|
86
|
+
// A 101 payload alone is not proof that Bun accepted the handshake.
|
|
87
|
+
this.connectionHeaders = this.pendingUpgradeHeaders;
|
|
88
|
+
this.pendingUpgradeHeaders = undefined;
|
|
89
|
+
this.socket.removeEventListener("handshake", this.onHandshake);
|
|
90
|
+
this.opened = true;
|
|
91
|
+
};
|
|
75
92
|
private onIdleMessage = (): void => {
|
|
76
93
|
if (!this.busy) this.dispose(new Error("codex websocket received unsolicited idle data"));
|
|
77
94
|
};
|
|
@@ -85,6 +102,9 @@ export class CodexWsSession {
|
|
|
85
102
|
// The active exchange's close listener retains pre-send fallback semantics.
|
|
86
103
|
};
|
|
87
104
|
private detach(): void {
|
|
105
|
+
this.pendingUpgradeHeaders = undefined;
|
|
106
|
+
this.connectionHeaders = undefined;
|
|
107
|
+
this.socket.removeEventListener("handshake", this.onHandshake);
|
|
88
108
|
this.socket.removeEventListener("open", this.onOpen);
|
|
89
109
|
this.socket.removeEventListener("message", this.onIdleMessage);
|
|
90
110
|
this.socket.removeEventListener("close", this.onClose);
|