mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(sse): parse Gemini CLI 429 retryDelay from structured RetryInfo (#4738)
Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.
This commit is contained in:
committed by
GitHub
parent
b9fdcfc006
commit
549ea427fd
@@ -534,11 +534,18 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse retry time from Gemini error message body
|
||||
// Format: "Your quota will reset after 2h7m23s"
|
||||
// Parse retry time from Gemini error message body. Two shapes are handled:
|
||||
// 1. Structured google.rpc.RetryInfo in the 429/503 JSON body:
|
||||
// { error: { details: [{ "@type": ".../google.rpc.RetryInfo", retryDelay: "30s" }] } }
|
||||
// 2. Human-readable prose: "Your quota will reset after 2h7m23s"
|
||||
// The structured hint is authoritative (it is what the Cloud Code Assist API
|
||||
// sends), so it is checked first and falls through to the prose form on miss.
|
||||
parseRetryFromErrorMessage(errorMessage: unknown): number | null {
|
||||
if (!errorMessage || typeof errorMessage !== "string") return null;
|
||||
|
||||
const structuredMs = this.parseStructuredRetryDelay(errorMessage);
|
||||
if (structuredMs !== null) return structuredMs;
|
||||
|
||||
const match = errorMessage.match(/reset (?:after|in) (\d+h)?(\d+m)?(\d+s)?/i);
|
||||
if (!match) return null;
|
||||
|
||||
@@ -549,6 +556,32 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
|
||||
return totalMs || 2_000;
|
||||
}
|
||||
|
||||
// Read google.rpc.RetryInfo.retryDelay from a Google API error JSON body and
|
||||
// convert the protobuf Duration string ("30s", "1.5s", "0.500s") into ms.
|
||||
// Returns null when the body is not JSON or carries no RetryInfo detail.
|
||||
private parseStructuredRetryDelay(bodyText: string): number | null {
|
||||
if (!bodyText.includes("RetryInfo")) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(bodyText);
|
||||
const details = parsed?.error?.details;
|
||||
if (!Array.isArray(details)) return null;
|
||||
for (const detail of details) {
|
||||
if (
|
||||
detail?.["@type"] === "type.googleapis.com/google.rpc.RetryInfo" &&
|
||||
typeof detail?.retryDelay === "string"
|
||||
) {
|
||||
const seconds = parseFloat(detail.retryDelay.replace(/s$/i, ""));
|
||||
if (Number.isFinite(seconds) && seconds >= 0) {
|
||||
return Math.round(seconds * 1000) || 2_000;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* not JSON — caller falls back to prose parsing */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default GeminiCLIExecutor;
|
||||
|
||||
@@ -473,3 +473,55 @@ test("GeminiCLIExecutor.execute applies CLI fingerprint to the final Cloud Code
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.parseRetryFromErrorMessage reads structured google.rpc.RetryInfo.retryDelay", () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
|
||||
const body = JSON.stringify({
|
||||
error: {
|
||||
code: 429,
|
||||
message: "Resource has been exhausted (e.g. check quota).",
|
||||
status: "RESOURCE_EXHAUSTED",
|
||||
details: [
|
||||
{ "@type": "type.googleapis.com/google.rpc.QuotaFailure", violations: [] },
|
||||
{ "@type": "type.googleapis.com/google.rpc.RetryInfo", retryDelay: "30s" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// The prose regex would not match this JSON body, so a positive result proves
|
||||
// the structured RetryInfo path is what produced the hint.
|
||||
assert.equal(executor.parseRetryFromErrorMessage(body), 30_000);
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.parseRetryFromErrorMessage parses fractional retryDelay seconds", () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
|
||||
const body = JSON.stringify({
|
||||
error: {
|
||||
details: [
|
||||
{ "@type": "type.googleapis.com/google.rpc.RetryInfo", retryDelay: "1.5s" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(executor.parseRetryFromErrorMessage(body), 1_500);
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.parseRetryFromErrorMessage still parses prose 'reset after' format", () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
|
||||
assert.equal(
|
||||
executor.parseRetryFromErrorMessage("Your quota will reset after 1h2m3s"),
|
||||
1 * 3600 * 1000 + 2 * 60 * 1000 + 3 * 1000
|
||||
);
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.parseRetryFromErrorMessage returns null for unrelated JSON", () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
|
||||
assert.equal(
|
||||
executor.parseRetryFromErrorMessage(JSON.stringify({ error: { message: "nope" } })),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user