fix(antigravity): rotate image accounts on explicit quota exhaustion (#9908)

Merged after batch validation on a combined worktree cut from `release/v3.8.51` with #9944 and #7138.

**Evidence**
- Focused tests: 36/36 pass on the combined tree, including your 4 classification-boundary cases in `tests/unit/antigravity-image-credential-retry.test.ts` (Antigravity quota-exhausted `429` rotates; generic `RESOURCE_EXHAUSTED`, ordinary image rate-limit and non-Antigravity `429` stay terminal).
- Gates on the combined tree: `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check-changelog-integrity` PASS.
- The red `check-file-size` reproduces byte-identical on the pure `release/v3.8.51` tip — inherited base-red, not from this PR. The red CI run on this PR dates from 2026-09-15 against an older base.

**Related dispositions**
- #8053 is being closed in your favour: it chased the same account-rotation goal across 3 files plus a new `routingInstrumentation.ts`, while its `AbortSignal` half was already superseded on the tip by independent work. This PR does the same job in 31 lines of production code by reusing the existing `classify429` engine.

Thanks, @Ardem2025 — the deliberate narrowness here is the reason this merged and the bigger version didn't. Gating rotation on `provider === "antigravity" && status === 429 && classify429() === "quota_exhausted"` keeps non-idempotent image generation from being retried on ordinary rate limits, and you proved each negative case rather than just the happy path.
This commit is contained in:
Dmitry Kuznetsov
2026-09-17 01:59:43 +03:00
committed by GitHub
parent 2f0a01d75c
commit 00860f3278
3 changed files with 78 additions and 2 deletions

View File

@@ -0,0 +1 @@
- Fix Antigravity image generation not rotating to another account when the upstream returns an explicit quota-exhausted 429, so a second configured account with available quota is no longer stuck behind the first account's terminal quota error.

View File

@@ -1,3 +1,4 @@
import { classify429 } from "@omniroute/open-sse/services/antigravity429Engine.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import { getProviderCredentialsWithQuotaPreflight } from "./auth";
@@ -50,6 +51,29 @@ function isCredentialSentinel(credentials: any): boolean {
return Boolean(credentials?.allRateLimited || credentials?.allExpired);
}
/**
* Image generation is non-idempotent, so account rotation stays deliberately
* narrower than chat failover. Antigravity's explicit exhausted-quota signal
* is safe to retry on another account; an ordinary 429 is not evidence that a
* different account helps and must not cause account rotation.
*/
export function isAntigravityImageQuotaExhausted(
provider: string,
result: ImageGenerationResult
): boolean {
if (provider !== "antigravity" || Number(result.status) !== 429) return false;
let errorText = "";
try {
errorText =
typeof result.error === "string" ? result.error : JSON.stringify(result.error ?? "");
} catch {
return false;
}
return classify429(errorText) === "quota_exhausted";
}
async function defaultSelectNextCredentials(
provider: string,
requestedModel: string | null,
@@ -110,8 +134,11 @@ export async function executeImageWithCredentialFallback({
lastCredentials = currentCredentials;
lastResult = await execute(currentCredentials);
const isAuthFailure = Number(lastResult.status) === 401 || lastResult.retryable === true;
if (lastResult.success || !isAuthFailure || !connectionId) {
const shouldTryAnotherAccount =
Number(lastResult.status) === 401 ||
lastResult.retryable === true ||
isAntigravityImageQuotaExhausted(provider, lastResult);
if (lastResult.success || !shouldTryAnotherAccount || !connectionId) {
return { credentials: lastCredentials, result: lastResult };
}

View File

@@ -0,0 +1,48 @@
import test from "node:test";
import assert from "node:assert/strict";
import { isAntigravityImageQuotaExhausted } from "../../src/sse/services/imageCredentialRetry.ts";
test("Antigravity image quota-exhausted 429 is the only 429 eligible for account rotation", () => {
assert.equal(
isAntigravityImageQuotaExhausted("antigravity", {
success: false,
status: 429,
error: { error: { message: "Individual quota reached" } },
}),
true
);
});
test("ordinary Hermes-shaped 429 does not create a rotation/cooldown signal", () => {
assert.equal(
isAntigravityImageQuotaExhausted("antigravity", {
success: false,
status: 429,
error: { error: { message: "RESOURCE_EXHAUSTED: malformed system payload" } },
}),
false
);
});
test("ordinary image rate-limit 429 does not create a rotation/cooldown signal", () => {
assert.equal(
isAntigravityImageQuotaExhausted("antigravity", {
success: false,
status: 429,
error: { error: { message: "too many requests; retry later" } },
}),
false
);
});
test("non-Antigravity 429 is not eligible for Antigravity account rotation", () => {
assert.equal(
isAntigravityImageQuotaExhausted("openai", {
success: false,
status: 429,
error: { error: { message: "Individual quota reached" } },
}),
false
);
});