Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ when account failover is enabled, the request can retry on another eligible acco
Google's account verification, then run `ocx login google-antigravity`. Silent token refresh
does not clear this verification requirement. If OpenCodex cannot save the quarantine, this adapter exchange preserves the original `403` without another recovery send; the account has not been durably quarantined. If a sibling request cannot be built or admitted for sending, the exchange delivers the original `403` through normal error formatting. An enclosing combo or policy route can still apply its existing fallback rules.

For fetch-based web-search requests, automatic sibling rotation accepts only a complete, bounded
Google error envelope containing `error.details[].reason === "VALIDATION_REQUIRED"`. Verification
wording alone does not rotate. This request-scoped recovery uses the sibling's token and project
without persisting a `needs-reauth` mark; cancellation, committed output, budget exhaustion or an
unavailable sibling preserves the failure.

A proxy that is already running picks up the new credential without a restart: the CLI asks it to
reload that one provider from disk, and the request carries no credential of its own. If the
running proxy cannot accept that request — most often because it started from a build that predates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,15 @@ The same main dispatch may also switch once on a 403 when Google's bounded error
finds a complete structured `VALIDATION_REQUIRED` reason. The 401 and 403 paths share one sibling
attempt per request; an unrelated or incomplete 403 keeps its original error. A rejected sibling,
cancellation or exhausted send budget does not cause another upstream send.
Continuations, native Responses passthrough, image and web-search sidecars, and output already sent
The fetch-based web-search loop also rotates once on this structured 403 reason, using the sibling's
OAuth token and matching Cloud Code Assist project. It accepts only complete, bounded Google error
envelopes with `error.details[].reason === "VALIDATION_REQUIRED"`; verification wording alone does
not authorize rotation, and this path does not persist a reauthentication mark. Each physical send,
including a 429 retry, consumes the same request budget exactly once. A disabled pool, cancellation,
exhausted budget or unavailable sibling preserves the failure. If sibling request construction or
dispatch fails, the original bounded 403 remains the error; a replay rejected before physical
dispatch returns its unused send allowance.
Continuations, native Responses passthrough, image sidecars, and output already sent
to the client do not use this rotation.

Current scope is the ordinary Responses request paths. Cursor reports rate limits as adapter
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1667,7 +1667,7 @@
"server-combo-reasoning-replay-eligibility.test.ts": "server",
"server-combo-zero-output-failover.test.ts": "server",
"server-google-antigravity-oauth-401-replay.test.ts": "server",
"server-google-antigravity-oauth-429-budget.test.ts": "server",
"server-antigravity-web-search-validation.test.ts": "server", "server-google-antigravity-oauth-429-budget.test.ts": "server",
"server-gui-bundle-freshness.test.ts": "server",
"server-images-bodyless-content-length.test.ts": "server",
"server-images-pool-admission.test.ts": "server",
Expand Down
26 changes: 20 additions & 6 deletions src/server/responses/antigravity-validation-refusal.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import { ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX } from "../../adapters/google-errors";
import { ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX, safeAntigravityHttpErrorMessage } from "../../adapters/google-errors";
import { readBoundedResponseBody } from "../../lib/bounded-body";

/** Match only the normalized Antigravity validation-refusal marker. */
export function hasAntigravityValidationRefusalMarker(text: string): boolean {
return text.startsWith(`${ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX}: `);
}

/** Inspect only the Google adapter's normalized, already bounded 403 response. */
export async function isAntigravityValidationRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
return inspectValidationRefusal(response, signal, false);
}

/** Sidecar dispatch retains the raw Google envelope; normalize only a complete bounded body. */
export async function isAntigravityRawValidationRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
return inspectValidationRefusal(response, signal, true);
}

async function inspectValidationRefusal(response: Response, signal: AbortSignal | undefined, raw: boolean): Promise<boolean> {
if (response.status !== 403 || signal?.aborted) return false;
try {
const body = await readBoundedResponseBody(response.clone(), {
maxBytes: 1024,
totalTimeoutMs: 1000,
firstByteTimeoutMs: 1000,
inactivityTimeoutMs: 1000,
maxBytes: raw ? 4096 : 1024,
totalTimeoutMs: raw ? 2000 : 1000,
firstByteTimeoutMs: raw ? 2000 : 1000,
inactivityTimeoutMs: raw ? 2000 : 1000,
signal,
});
return !signal?.aborted && body.displaySafe && !body.truncated && !body.timedOut && !body.oversized
&& body.text.startsWith(`${ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX}: `);
&& hasAntigravityValidationRefusalMarker(raw ? safeAntigravityHttpErrorMessage(403, body.text) : body.text);
} catch {
return false;
}
Expand Down
63 changes: 33 additions & 30 deletions src/server/responses/request-send-budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,21 @@ export function createResponsesSendBudget(
* The hop hands its reservation down through `pendingHopPermit`, the same seam the
* passthrough ladder already uses, and this view spends it on the adapter's FIRST
* reservation. Every later send in that ladder is a new physical send and is charged
* normally. A permit the adapter takes but never sends under is released through the same
* call it would have used for a reservation of its own, so an abandoned replay is refunded
* rather than left charged.
* normally. The refundable view below keeps an unused hop open until executor invocation;
* its adapter release hands back an abandoned replay. The ordinary view retains early
* settlement for existing callers.
*/
const claimHopPermit = (): SingleUseDispatchPermit | undefined => {
const permit = pendingHopPermit;
pendingHopPermit = undefined;
return permit;
};
const adapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
? undefined
: adapterDispatchBudgetView(adapterSendBudget, {
claimHopPermit: () => {
const permit = pendingHopPermit;
pendingHopPermit = undefined;
return permit;
},
});
? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit });
// Opt in only where the adapter's executor is the confirmation boundary. Ordinary callers
// keep the existing early settlement contract, including already-settled permit fallback.
const refundableAdapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit }, true);
/**
* How many sends a recovery leg may make, and the permit that authorises the last one.
*
Expand Down Expand Up @@ -279,6 +281,7 @@ export function createResponsesSendBudget(
get sendsUsed(): number { return sendBudget.used; },
adapterSendBudget,
adapterDispatchBudget,
refundableAdapterDispatchBudget,
noteAdapterPhysicalSend,
noteAdapterRecoveryWithheld,
sendBudgetExhausted,
Expand Down Expand Up @@ -312,6 +315,7 @@ export type ResponsesSendBudget = Exclude<ReturnType<typeof createResponsesSendB
function adapterDispatchBudgetView(
budget: RequestExecutionBudget,
hop: { claimHopPermit: () => SingleUseDispatchPermit | undefined },
refundableHop = false,
): RequestExecutionBudget {
return {
get used(): number { return budget.used; },
Expand All @@ -338,30 +342,29 @@ function adapterDispatchBudgetView(
hopPermit.release();
return budget.reserveDispatch({ ...intent, sendClass: hopPermit.sendClass });
}
// Confirmed here rather than in `use()`: the adapter reserves immediately before it
// opens the transport, which is the same boundary the hop's own confirmation uses.
// A permit some other leg already settled returns false, and this falls through to a
// real reservation rather than handing the adapter a dead permit -- an adapter whose
// `use()` fails treats the request as exhausted and stops sending entirely.
if (hopPermit !== undefined && hopPermit.assumeCharge()) {
let spent = false;
if (hopPermit !== undefined && (refundableHop || hopPermit.assumeCharge())) {
let settled = false;
const confirm = (): boolean => {
if (settled) return false;
settled = true;
// Admission can still reject this replay. Close its external booking only when
// the adapter invokes its executor, leaving release() able to refund it until then.
if (!refundableHop || hopPermit.assumeCharge()) return true;
// Preserve the old fallback for a permit another leg already settled.
const fresh = budget.reserveDispatch(intent);
return fresh.allowed && fresh.permit.assumeCharge();
};
return {
allowed: true,
permit: {
sendClass: hopPermit.sendClass,
use: (): boolean => {
if (spent) return false;
spent = true;
return true;
},
assumeCharge: (): boolean => {
if (spent) return false;
spent = true;
return true;
use: confirm,
assumeCharge: confirm,
release: (): void => {
if (!refundableHop || settled) return;
settled = true;
hopPermit.release();
},
// The hop's charge is already settled and belongs to the leg that asked for it,
// so there is nothing here to refund.
release: (): void => {},
},
};
}
Expand Down
85 changes: 80 additions & 5 deletions src/server/responses/sidecar-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import {
isGenericOAuthFailoverEnabled,
rotateGenericOAuthAccountOn429,
rotateGenericOAuthAccountOnRefusal,
rotateAntigravityAccountOnAuthRefusal,
quarantineKiroSuspendedAccount,
failoverAccountSnapshot,
} from "../../oauth/generic-account-failover";
import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
import { isNonReplayableResponse } from "../../lib/upstream-retry";
import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
import { readDisplaySafeErrorText } from "./core-errors";
Expand All @@ -45,6 +47,7 @@ import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
import { normalizeLogConversationId } from "../request-log-conversation";
import { rememberResponseState } from "../../responses/state";
import { trackStreamLifetime } from "../lifecycle";
import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";

/** One responsibility of the Responses request pipeline; state owners are explicit. */
export async function executeResponsesSidecars(
Expand Down Expand Up @@ -85,9 +88,11 @@ export async function executeResponsesSidecars(
| "notifyResponseComplete"
| "cancelResponseCompletion"
>,
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop">,
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "pendingHopPermit" | "noteAdapterPhysicalSend">,
) {
const { config, options, logCtx } = requestContext;
const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
&& isGenericOAuthFailoverEnabled(config, requestState.route.providerName);
const {
applyFailoverSnapshot,
anthropicSessionKey,
Expand Down Expand Up @@ -182,13 +187,29 @@ export async function executeResponsesSidecars(
|| (logCtx.activeAttempt?.deliverySummary?.semanticBytes ?? 0) > 0
|| (logCtx.activeAttempt?.deliverySummary?.sideEffectEvents ?? 0) > 0;
const noteSidecarOutput = () => { sidecarOutputStarted = true; options.onFirstOutput?.(); };
// A single request may rotate once for a verification refusal. Keep this separate from the
// broader OAuth failover count: a sidecar turn can also spend that count on rate-limit recovery.
let antigravityValidationRotationAttempted = false;
const rotateSidecarProviderOn429 = async (
retryAfter: string | null,
responseHeaders?: Headers,
retryParsed?: OcxParsedRequest,
originalResponse?: Response,
): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
if (route.providerName !== "kiro" && !(route.providerName === "anthropic" && originalResponse?.status === 403) && originalResponse && originalResponse.status !== 429) return null;
const antigravityValidationResponse = canRunWebSearch && route.providerName === "google-antigravity"
&& route.provider.authMode === "oauth" && originalResponse?.status === 403;
if (route.providerName !== "kiro" && !(route.providerName === "anthropic" && originalResponse?.status === 403)
&& !antigravityValidationResponse && originalResponse && originalResponse.status !== 429) return null;
let antigravityVerification = false;
if (antigravityValidationResponse) {
if (transportState.adapter.name !== "google" || !antigravityPoolActivated
|| options.abortSignal?.aborted || isNonReplayableResponse(originalResponse)
|| antigravityValidationRotationAttempted
|| transportState.genericFailovers >= transportState.genericFailoverLimit
|| sidecarHasCommittedOutput()) return null;
antigravityVerification = await isAntigravityRawValidationRefusal(originalResponse, options.abortSignal);
if (!antigravityVerification) return null;
}
const refusal = route.providerName === "kiro" && originalResponse
? classifyKiroRefusal(originalResponse.status,
await readDisplaySafeErrorText(originalResponse.clone(), options.abortSignal ?? new AbortController().signal, "")).kind
Expand All @@ -205,14 +226,52 @@ export async function executeResponsesSidecars(
// sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
// rotation in the attempt row and in the Logs UI.
let recoveryKind: AttemptRecoveryKind = "key-429";
if (antigravityVerification) {
const sent = transportState.replayOAuthCredentialSnapshot;
const failedAccountId = transportState.genericFailoverAccountId;
if (!sent || !failedAccountId || sent.accountId !== failedAccountId) return null;
if (options.abortSignal?.aborted || sidecarHasCommittedOutput()) return null;

const hop = reserveCredentialHop(
"auth-recovery",
`${route.providerName}|${route.modelId}|sidecar-oauth-verify`,
);
if (!hop.allowed) return null;
antigravityValidationRotationAttempted = true;
const nextAccountId = rotateAntigravityAccountOnAuthRefusal(
antigravityPoolActivated, sent.accountId, sent.generation, route.modelId,
);
if (!nextAccountId) {
hop.permit?.release();
return null;
}
try {
// Keep the sibling's bearer and its account-matched project together; never inherit the
// failed account's identity or project.
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
if (options.abortSignal?.aborted || !await applyFailoverSnapshot(snapshot, retryParsed)
|| transportState.replayOAuthCredentialSnapshot?.accountId !== nextAccountId) {
hop.permit?.release();
return null;
}
transportState.genericFailovers += 1;
} catch {
hop.permit?.release();
return null;
}
recoveryKind = "oauth-account-403";
sendBudgetState.pendingHopPermit = hop.permit;
}
const rotated = !originalResponse || originalResponse.status === 429
? rotateProviderTransportOn429(config, route.providerName, route.provider, {
retryAfter,
now: Date.now(),
attemptedKey: route.provider.apiKey,
promptCacheKey: parsed.options.promptCacheKey,
}) : null;
if (rotated) {
if (antigravityVerification) {
// `applyFailoverSnapshot` has already rebound the route to the sibling identity/project.
} else if (rotated) {
route.provider = rotated;
} else if (
// A POSITIVE gate, not an early return. An early `return null` here made every later arm
Expand All @@ -225,7 +284,8 @@ export async function executeResponsesSidecars(
) {
// Intersection with the request's shared budget. The sidecar replay is dispatched by the
// web-search/image loop and never reaches `onSendsConsumed`, so this reservation is the
// charge; a refusal returns null and the caller keeps the real 429 it already has.
// charge for other sidecars. Antigravity web search hands its reservation to the adapter
// physical-send owner instead; a refusal preserves the real 429.
const hop = reserveCredentialHop(
"auth-recovery",
`${route.providerName}|${route.modelId}|sidecar-oauth-429`,
Expand Down Expand Up @@ -259,7 +319,8 @@ export async function executeResponsesSidecars(
return null;
}
recoveryKind = "oauth-account-429";
hop.permit?.use();
if (canRunWebSearch && route.providerName === "google-antigravity") sendBudgetState.pendingHopPermit = hop.permit;
else hop.permit?.use();
} else if (
// Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
// web-search or image-bridge turn was terminal even with the pool fully enabled -- while
Expand Down Expand Up @@ -321,13 +382,15 @@ export async function executeResponsesSidecars(
providerName: route.providerName,
provider: route.provider,
adapterName: rotatedAdapter.name,
...(antigravityVerification ? { oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot } : {}),
});
}
bindRouteReasoningReplayScope({
parsed,
providerName: route.providerName,
provider: route.provider,
adapterName: rotatedAdapter.name,
...(antigravityVerification ? { oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot } : {}),
});
return { adapter: rotatedAdapter, recoveryKind };
};
Expand Down Expand Up @@ -484,6 +547,11 @@ export async function executeResponsesSidecars(
abortSignal: options.abortSignal,
translatorBudget,
providerFetch: routedProviderFetch,
...(route.providerName === "google-antigravity" ? {
sendBudget: sendBudgetState.refundableAdapterDispatchBudget,
onPhysicalSend: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) =>
sendBudgetState.noteAdapterPhysicalSend(logCtx.usageLogInputTokens, send),
} : {}),
},
backend: wsPlan.backend,
forwardProvider: wsPlan.forwardSidecar?.provider,
Expand Down Expand Up @@ -516,6 +584,13 @@ export async function executeResponsesSidecars(
stallTimeoutSec: wsPlan.stallTimeoutSec,
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
on429: rotateSidecarProviderOn429,
...(route.providerName === "google-antigravity" ? { onIterationEnd: () => {
// The next Antigravity adapter dispatch claims this permit. If request construction,
// admission or cancellation stopped it first, return the unused send allowance.
const permit = sendBudgetState.pendingHopPermit;
sendBudgetState.pendingHopPermit = undefined;
permit?.release();
} } : {}),
retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
? null : rateLimitRetryPolicyFor(route.provider),
onCompletedResponse: response => {
Expand Down
Loading
Loading