mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
* fix(sse): parse Gemini 429 RetryInfo.retryDelay for model lockout (#7940) Gemini free-tier 429 bodies carry a short explicit retry hint -- error.details[].{"@type": google.rpc.RetryInfo, retryDelay: "26s"} plus a "Please retry in Ns." message -- but parseRetryFromErrorText only matched 'reset after'/'will reset after' text, so quotaResetHintMs came back null and recordModelLockoutFailure fell back to getMsUntilTomorrow() for quota_exhausted, locking the model out for ~19h instead of ~26s. parseRetryHintFromJsonBody (retryAfterJson.ts) now walks error.details[] for a google.rpc.RetryInfo entry and parses its retryDelay via a shared parseDelayString helper (moved out of accountFallback.ts so parseRetryAfterFromBody and the model-lockout path use the same grammar). parseRetryFromErrorText also gained a 'please retry in Ns' text fallback for bodies without a parseable details[] array. Both new paths are capped by a dedicated MAX_SHORT_RETRY_HINT_MS (24h), independent of the existing 30-day MAX_PROVIDER_COOLDOWN_MS, since RetryInfo/please-retry-in are short throttling hints, not long-lived quota resets like Antigravity's 160h. Regression test: tests/unit/bug-7940-gemini-retrydelay.test.ts (RED before the fix: parseRetryFromErrorText returned null and the resulting lockout was ~19h; GREEN after: ~26s). * chore(quality): register bug-7940-gemini-retrydelay test in stryker tap.testFiles
72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
parseRetryFromErrorText,
|
|
recordModelLockoutFailure,
|
|
clearAllModelLockouts,
|
|
getModelLockoutInfo,
|
|
} from "../../open-sse/services/accountFallback.ts";
|
|
|
|
const GEMINI_429_BODY = JSON.stringify({
|
|
error: {
|
|
code: 429,
|
|
message:
|
|
"You exceeded your current quota, please check your plan and billing details. " +
|
|
"Please retry in 26.660853464s.",
|
|
status: "RESOURCE_EXHAUSTED",
|
|
details: [
|
|
{
|
|
"@type": "type.googleapis.com/google.rpc.QuotaFailure",
|
|
violations: [
|
|
{ quotaMetric: "generativelanguage.googleapis.com/generate_content_free_tier_requests" },
|
|
],
|
|
},
|
|
{ "@type": "type.googleapis.com/google.rpc.RetryInfo", retryDelay: "26s" },
|
|
],
|
|
},
|
|
});
|
|
|
|
test("parseRetryFromErrorText extracts Gemini RetryInfo.retryDelay (26s), not null", () => {
|
|
const parsedMs = parseRetryFromErrorText(GEMINI_429_BODY);
|
|
assert.notEqual(parsedMs, null, "expected the 26s RetryInfo hint to be parsed, got null");
|
|
assert.ok(parsedMs! >= 25_000 && parsedMs! <= 28_000, `expected ~26000ms, got ${parsedMs}ms`);
|
|
});
|
|
|
|
test("recordModelLockoutFailure quota_exhausted with a short upstream hint must NOT fall back to midnight", () => {
|
|
clearAllModelLockouts();
|
|
const provider = "gemini",
|
|
connectionId = "conn-7940",
|
|
model = "gemini-2.5-flash";
|
|
const quotaResetHintMs = parseRetryFromErrorText(GEMINI_429_BODY) ?? undefined;
|
|
const result = recordModelLockoutFailure(provider, connectionId, model, "quota_exhausted", 429, 0, null, {
|
|
exactCooldownMs: quotaResetHintMs ?? null,
|
|
});
|
|
const oneHourMs = 60 * 60 * 1000;
|
|
assert.ok(result.cooldownMs < oneHourMs, `expected ~26s cooldown, got ${result.cooldownMs}ms`);
|
|
clearAllModelLockouts();
|
|
});
|
|
|
|
test("parseRetryFromErrorText falls back to 'please retry in Ns' text when no JSON details are present", () => {
|
|
const plainText =
|
|
"429 RESOURCE_EXHAUSTED: You exceeded your current quota. Please retry in 12.5s.";
|
|
const parsedMs = parseRetryFromErrorText(plainText);
|
|
assert.notEqual(parsedMs, null, "expected the 12.5s text hint to be parsed, got null");
|
|
assert.ok(parsedMs! >= 12_000 && parsedMs! <= 13_500, `expected ~12500ms, got ${parsedMs}ms`);
|
|
});
|
|
|
|
test("getModelLockoutInfo reflects the short lockout, not a multi-hour one", () => {
|
|
clearAllModelLockouts();
|
|
const provider = "gemini",
|
|
connectionId = "conn-7940b",
|
|
model = "gemini-2.5-flash";
|
|
const quotaResetHintMs = parseRetryFromErrorText(GEMINI_429_BODY) ?? undefined;
|
|
recordModelLockoutFailure(provider, connectionId, model, "quota_exhausted", 429, 0, null, {
|
|
exactCooldownMs: quotaResetHintMs ?? null,
|
|
});
|
|
const info = getModelLockoutInfo(provider, connectionId, model);
|
|
assert.ok(info, "expected an active lockout entry");
|
|
const oneHourMs = 60 * 60 * 1000;
|
|
assert.ok(info!.remainingMs < oneHourMs, `expected <1h remaining, got ${info!.remainingMs}ms`);
|
|
clearAllModelLockouts();
|
|
});
|