Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,17 @@ ocx account login openai --device

See [Codex Integration](/guides/codex-integration/) for what the injection
writes and how routing is chosen.

## OAuth refresh capacity reached

If a Responses request reports `OAuth token refresh capacity reached`, wait at
least one second before retrying. The proxy limits pending OAuth refresh work
to 256 consumers per process, including requests waiting on the same refresh.
A disconnected request keeps its refresh reservation until that work settles;
requests using fresh cached credentials can still proceed.

Before a response starts, this temporary refusal returns HTTP 503 with
`Retry-After: 1`. A stream that has already started instead ends with a server
error. Capacity refusal does not mark the account as needing reauthentication,
so signing in again is unnecessary for this error. A refresh that never settles
can continue occupying capacity; the limit does not impose a refresh deadline.
2 changes: 2 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"worker-embed-coverage.test.ts": "lib",
"jev-stats.test.ts": "usage",
"responses-devin-401-replay.test.ts": "responses",
"responses-oauth-refresh-capacity.test.ts": "responses",
"responses-grok-devin-preflight.test.ts": "responses",
"deepseek-quota-currency.test.ts": "providers",
"mimo-token-plan-capacity.test.ts": "providers",
Expand Down Expand Up @@ -1271,6 +1272,7 @@
"oauth-public-surface.test.ts": "oauth",
"oauth-reauth-bind.test.ts": "oauth",
"oauth-refresh-generic-lock.test.ts": "oauth",
"oauth-refresh-stale-capacity.test.ts": "oauth",
"oauth-refresh-lock-multiprocess.test.ts": "oauth",
"oauth-refresh.test.ts": "oauth",
"oauth-status-privacy.test.ts": "oauth",
Expand Down
104 changes: 66 additions & 38 deletions src/oauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../provider
import { resolveProviderTransport } from "../providers/xai-transport";
import { detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
import { logOAuthEvent } from "./log";
import { MAX_OAUTH_TOKEN_REFRESH_WAITERS, retireSettledOAuthRefreshFlight, tokenRefreshWaiters, type OAuthRefreshFlightEvidence } from "./refresh-flight-retirement";
export { MAX_OAUTH_TOKEN_REFRESH_WAITERS };
import { captureConfigGeneration, sweepExpiredOnWrite } from "../lib/state-store-sweeper";
import { admitLoginFlow, classifyLoginCodeError, clearManualCodeSlot, CodeLoginUnsupportedError, ensureManualCodeSlot, kiroLoginSettling, latestLoginFlowKey, loginAbort, loginFlowProvider, loginState, recordStartedLoginFlow, settleLoginFlow, waitForManualLoginCode, type LoginCodeError, type OAuthLoginHint } from "./login-flow-state";
export { cancelLoginFlow, clearLoginState, CodeLoginUnsupportedError, isCodeLoginFlow, loginFlowAccountId, reconcileOAuthFlowState, submitLoginCode, submitManualLoginCode, waitForLoginSettled } from "./login-flow-state";
Expand Down Expand Up @@ -135,8 +137,7 @@ export type OAuthActiveTokenObservation =

const MAX_OAUTH_TOKEN_REFRESH_FLIGHTS = 32;
const OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS = 120_000;
interface OAuthRefreshFlightEvidence { flightId: string; dispatched: boolean }
interface OAuthTokenRefreshFlight extends OAuthRefreshFlightEvidence { promise: Promise<OAuthAccessSnapshot>; startedAt: number; abort: AbortController }
interface OAuthTokenRefreshFlight extends OAuthRefreshFlightEvidence { promise: Promise<OAuthAccessSnapshot>; startedAt: number; abort: AbortController; settled?: boolean; retirementDeferredLogged?: boolean }
const tokenRefreshes = new Map<string, OAuthTokenRefreshFlight>();
export class OAuthTokenRefreshBusyError extends Error {
readonly code = "OAUTH_TOKEN_REFRESH_BUSY";
Expand All @@ -160,7 +161,8 @@ export function seedOAuthTokenRefreshFlightsForTests(rows: Array<{ key: string;
const promise = new Promise<OAuthAccessSnapshot>((_resolve, reject) => {
abort.signal.addEventListener("abort", () => reject(abort.signal.reason), { once: true });
});
const flight = { promise, startedAt: startedAt ?? Date.now(), abort, flightId: flightId ?? randomUUID(), dispatched: dispatched ?? false };
const flight: OAuthTokenRefreshFlight = { promise, startedAt: startedAt ?? Date.now(), abort, flightId: flightId ?? randomUUID(), dispatched: dispatched ?? false };
abort.signal.addEventListener("abort", () => { flight.settled = true; }, { once: true });
tokenRefreshes.set(key, flight);
inserted.push(flight);
return promise;
Expand Down Expand Up @@ -634,45 +636,71 @@ async function resolveAccessSnapshotForAccount(
const key = `${provider}\u0000${accountId}`;
let existing = tokenRefreshes.get(key);
let replacedStaleFlight: OAuthRefreshFlightEvidence | undefined;
if (existing && Date.now() - existing.startedAt <= OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS) {
logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
return existing.promise;
}
if (existing) {
if (existing && (existing.abort.signal.reason instanceof OAuthTokenRefreshStaleError
|| Date.now() - existing.startedAt > OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS)) {
replacedStaleFlight = { flightId: existing.flightId, dispatched: existing.dispatched };
existing.abort.abort(new OAuthTokenRefreshStaleError());
if (tokenRefreshes.get(key) === existing) tokenRefreshes.delete(key);
existing = undefined;
}
if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) throw new OAuthTokenRefreshBusyError();

const abort = new AbortController();
const flight: OAuthTokenRefreshFlight = {
promise: undefined as unknown as Promise<OAuthAccessSnapshot>,
startedAt: Date.now(),
abort,
flightId: randomUUID(),
dispatched: false,
};
const refresh = (async (): Promise<OAuthAccessSnapshot> => {
const accessToken = await refreshAndPersistAccessToken(provider, accountId, def, cred, abort.signal, flight, replacedStaleFlight);
const persistedRow = getAccountCredentialWithStatus(provider, accountId);
if (!persistedRow) throw new OAuthLoginRequiredError(provider);
if (persistedRow.paused === true) throw new OAuthAccountPausedError();
const persisted = persistedRow.credential;
if (persisted.access !== accessToken) {
throw new Error(`OAuth refresh persisted an unexpected access token for ${provider}`);
// Count every consumer, including same-flight joins, until its promise settles. Cancelling
// a client cannot recycle this permit. Stale-flight abort above still runs under pressure;
// no replacement starts until the old consumers have actually released enough capacity.
const waiter = tokenRefreshWaiters.tryAcquire();
if (!waiter) throw new OAuthTokenRefreshBusyError();
try {
if (existing && replacedStaleFlight) {
// A replacement would wait for this owner's lock anyway. Keep its registry slot
// through definitive-result cleanup and durable retirement, even with spare capacity.
// In particular, an ignored abort must not mark a late explicit rejection as uncertain.
await existing.promise.catch(() => {});
if (tokenRefreshes.get(key) === existing && !retireSettledOAuthRefreshFlight(tokenRefreshes, key, existing)) {
throw new OAuthTokenRefreshStaleError();
}
// Other admitted replacements may have resumed first and installed the next flight.
existing = tokenRefreshes.get(key);
}
return accessSnapshot(provider, accountId, persisted, oauthProvider);
})().catch(error => {
if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) throw abort.signal.reason;
throw error;
}).finally(() => {
if (tokenRefreshes.get(key) === flight) tokenRefreshes.delete(key);
});
flight.promise = refresh;
tokenRefreshes.set(key, flight);
return refresh;
if (existing) {
logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
return await existing.promise;
}
if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) {
for (const [staleKey, stale] of tokenRefreshes) {
if (stale.abort.signal.reason instanceof OAuthTokenRefreshStaleError) retireSettledOAuthRefreshFlight(tokenRefreshes, staleKey, stale);
}
if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) throw new OAuthTokenRefreshBusyError();
}
Comment thread
lidge-jun marked this conversation as resolved.

const abort = new AbortController();
const flight: OAuthTokenRefreshFlight = {
promise: undefined as unknown as Promise<OAuthAccessSnapshot>,
startedAt: Date.now(),
abort,
flightId: randomUUID(),
dispatched: false,
};
const refresh = (async (): Promise<OAuthAccessSnapshot> => {
const accessToken = await refreshAndPersistAccessToken(provider, accountId, def, cred, abort.signal, flight, replacedStaleFlight);
const persistedRow = getAccountCredentialWithStatus(provider, accountId);
if (!persistedRow) throw new OAuthLoginRequiredError(provider);
if (persistedRow.paused === true) throw new OAuthAccountPausedError();
const persisted = persistedRow.credential;
if (persisted.access !== accessToken) {
throw new Error(`OAuth refresh persisted an unexpected access token for ${provider}`);
}
return accessSnapshot(provider, accountId, persisted, oauthProvider);
})().catch(error => {
if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) throw abort.signal.reason;
throw error;
}).finally(() => {
flight.settled = true;
if (tokenRefreshes.get(key) !== flight) return;
if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) {
retireSettledOAuthRefreshFlight(tokenRefreshes, key, flight);
} else tokenRefreshes.delete(key);
});
flight.promise = refresh;
tokenRefreshes.set(key, flight);
return await refresh;
} finally { waiter.release(); }
}

export async function getValidAccessTokenSnapshot(
Expand Down
48 changes: 48 additions & 0 deletions src/oauth/refresh-flight-retirement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createAdmissionGate } from "../lib/admission";
import { isAnthropicOAuthInstance } from "../providers/anthropic-instance";
import { clearObservedAnthropicRefreshIntent } from "./anthropic-oauth-definitions";
import { logOAuthEvent } from "./log";
import { markOAuthRefreshIntentStaleOwner, readOAuthRefreshIntent } from "./store";

/** Independent of client lifetime; a cancelled caller can still be awaiting a shared refresh. */
export const MAX_OAUTH_TOKEN_REFRESH_WAITERS = 256;
export const tokenRefreshWaiters = createAdmissionGate("oauth_token_refresh_waiters", MAX_OAUTH_TOKEN_REFRESH_WAITERS);

export interface OAuthRefreshFlightEvidence { flightId: string; dispatched: boolean }

export interface RetirableOAuthRefreshFlight extends OAuthRefreshFlightEvidence {
settled?: boolean;
retirementDeferredLogged?: boolean;
}

// Retire only after the old attempt has finished its definitive-result cleanup. A provider
// may ignore abort; marking its intent sooner would poison a late, safely retryable rejection.
// Failed durable retirement retains the same bounded registry slot for a later retry.
export function retireSettledOAuthRefreshFlight<F extends RetirableOAuthRefreshFlight>(
tokenRefreshes: Map<string, F>, key: string, flight: F,
): boolean {
if (!flight.settled) return false;
const [provider, accountId] = key.split("\0");
const defer = (reason: "uncertain_intent" | "intent_changed" | "storage_failure"): false => {
if (!flight.retirementDeferredLogged) {
flight.retirementDeferredLogged = true;
logOAuthEvent("OAuth refresh flight retirement deferred", { provider: provider ?? "unknown", reason });
}
return false;
};
try {
if (provider && accountId && isAnthropicOAuthInstance(provider)) {
const intent = readOAuthRefreshIntent(provider, accountId);
if (intent?.uncertain) return defer("uncertain_intent");
if (intent?.flightId === flight.flightId && !intent.staleOwner && !intent.cleanupPending) {
const retired = flight.dispatched
? markOAuthRefreshIntentStaleOwner(provider, accountId, intent.generation, flight.flightId)
: clearObservedAnthropicRefreshIntent(provider, accountId, intent);
if (!retired) return defer("intent_changed");
}
}
} catch { return defer("storage_failure"); }
if (tokenRefreshes.get(key) === flight) tokenRefreshes.delete(key);
return true;
}

11 changes: 10 additions & 1 deletion src/server/responses/adapter-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import { describeUpstreamConnectFailure } from "./upstream-error";
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
import type { AttemptRecoveryKind } from "../../usage/log";
import type { OAuthAccessSnapshot } from "../../oauth";
import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
import { OAuthAccountPausedError, OAuthLoginRequiredError, OAuthTokenRefreshBusyError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
import { getAccountSet, markAccountNeedsReauthIfGeneration } from "../../oauth/store";
import { tryKiroAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
Expand Down Expand Up @@ -932,6 +932,15 @@ export async function prepareAdapterExchange(
try {
refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
} catch (err) {
const cancelled = options.abortSignal?.aborted || req.signal.aborted;
if (cancelled || err instanceof OAuthTokenRefreshBusyError) {
cleanupUpstreamAbort();
upstream.abort();
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
releaseCodexAuthContextProbeLease(admissionState.authCtx);
return cancelled ? clientCancelledResponse()
: formatErrorResponse(503, "server_error", publicOAuthAuthenticationErrorMessage(err), { retryAfter: "1" });
}
const failed = transportState.sentOAuthSnapshot;
if (route.providerName === "google-antigravity" && !antigravityAuthRotationAttempted
&& err instanceof OAuthLoginRequiredError && failed
Expand Down
4 changes: 3 additions & 1 deletion src/server/responses/passthrough-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ import { resolveWireProtocolOverride } from "../adapter-resolve";
import { refreshPoolForwardAuth, refreshNativeMainForwardAuth, withClaudeNativeSession } from "./core-auth";
import { bindRouteReasoningReplayScope } from "./core-replay";
import type { OAuthAccessSnapshot } from "../../oauth";
import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
import { OAuthAccountPausedError, OAuthLoginRequiredError, OAuthTokenRefreshBusyError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
import { AnthropicAccountCooldownError } from "../../oauth/anthropic-routing";
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
import {
Expand Down Expand Up @@ -1358,6 +1358,8 @@ export async function preparePassthroughExchange(
} catch (err) {
upstream.abort();
releaseCodexAuthContextProbeLease(admissionState.authCtx);
if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
if (err instanceof OAuthTokenRefreshBusyError) return formatErrorResponse(503, "server_error", publicOAuthAuthenticationErrorMessage(err), { retryAfter: "1" });
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
}
if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
Expand Down
5 changes: 5 additions & 0 deletions src/server/responses/request-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from "../../oauth/anthropic-routing";
import {
OAuthAccountPausedError,
OAuthTokenRefreshBusyError,
getValidAccessSnapshotForAccount,
forceRefreshOAuthAccessSnapshot,
getValidAccessTokenSnapshot,
Expand Down Expand Up @@ -965,6 +966,10 @@ export async function prepareResponsesTransport(
`${redactSecretString(err.message)}. Remove or reconfigure provider '${safeProviderName}' in the OpenCodex configuration.`,
);
}
if (err instanceof OAuthTokenRefreshBusyError) {
return options.abortSignal?.aborted || req.signal.aborted ? clientCancelledResponse()
: formatErrorResponse(503, "server_error", err.message, { retryAfter: "1" });
}
if (err instanceof OAuthAccountPausedError) {
return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(err));
}
Expand Down
Loading
Loading