mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 20:32:25 +03:00
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:
@@ -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.
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
48
tests/unit/antigravity-image-credential-retry.test.ts
Normal file
48
tests/unit/antigravity-image-credential-retry.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user