From fd885a3e50e3c88bba6b5b2ee03a76b7c514ff29 Mon Sep 17 00:00:00 2001 From: gemini-cli-robot Date: Wed, 5 Nov 2025 09:04:36 -0800 Subject: [PATCH] fix(patch): cherry-pick f51d745 to release/v0.13.0-preview.0-pr-12586 to patch version v0.13.0-preview.0 and create version 0.13.0-preview.1 (#12595) Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com> --- .../core/src/utils/googleQuotaErrors.test.ts | 38 +++++++++++++++++++ packages/core/src/utils/googleQuotaErrors.ts | 33 +++++++++++++--- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/core/src/utils/googleQuotaErrors.test.ts b/packages/core/src/utils/googleQuotaErrors.test.ts index cc5e5de43a..34836119a8 100644 --- a/packages/core/src/utils/googleQuotaErrors.test.ts +++ b/packages/core/src/utils/googleQuotaErrors.test.ts @@ -25,6 +25,44 @@ describe('classifyGoogleError', () => { expect(result).toBe(regularError); }); + it('should return RetryableQuotaError when message contains "Please retry in Xs"', () => { + const complexError = { + error: { + message: + '{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s."}]}}', + code: 429, + status: 'Too Many Requests', + }, + }; + const rawError = new Error(JSON.stringify(complexError)); + vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null); + + const result = classifyGoogleError(rawError); + + expect(result).toBeInstanceOf(RetryableQuotaError); + expect((result as RetryableQuotaError).retryDelayMs).toBe(44097.740004); + expect((result as RetryableQuotaError).message).toBe(rawError.message); + }); + + it('should return RetryableQuotaError when error is a string and message contains "Please retry in Xms"', () => { + const complexErrorString = JSON.stringify({ + error: { + message: + '{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms."}]}}', + code: 429, + status: 'Too Many Requests', + }, + }); + const rawError = new Error(complexErrorString); + vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null); + + const result = classifyGoogleError(rawError); + + expect(result).toBeInstanceOf(RetryableQuotaError); + expect((result as RetryableQuotaError).retryDelayMs).toBeCloseTo(900.2); + expect((result as RetryableQuotaError).message).toBe(rawError.message); + }); + it('should return original error if code is not 429', () => { const apiError: GoogleApiError = { code: 500, diff --git a/packages/core/src/utils/googleQuotaErrors.ts b/packages/core/src/utils/googleQuotaErrors.ts index 55c5d29a8a..f09d8b2474 100644 --- a/packages/core/src/utils/googleQuotaErrors.ts +++ b/packages/core/src/utils/googleQuotaErrors.ts @@ -43,16 +43,20 @@ export class RetryableQuotaError extends Error { } /** - * Parses a duration string (e.g., "34.074824224s", "60s") and returns the time in seconds. + * Parses a duration string (e.g., "34.074824224s", "60s", "900ms") and returns the time in seconds. * @param duration The duration string to parse. * @returns The duration in seconds, or null if parsing fails. */ function parseDurationInSeconds(duration: string): number | null { - if (!duration.endsWith('s')) { - return null; + if (duration.endsWith('ms')) { + const milliseconds = parseFloat(duration.slice(0, -2)); + return isNaN(milliseconds) ? null : milliseconds / 1000; } - const seconds = parseFloat(duration.slice(0, -1)); - return isNaN(seconds) ? null : seconds; + if (duration.endsWith('s')) { + const seconds = parseFloat(duration.slice(0, -1)); + return isNaN(seconds) ? null : seconds; + } + return null; } /** @@ -64,6 +68,7 @@ function parseDurationInSeconds(duration: string): number | null { * - If the error suggests a retry delay of more than 2 minutes, it's a `TerminalQuotaError`. * - If the error suggests a retry delay of 2 minutes or less, it's a `RetryableQuotaError`. * - If the error indicates a per-minute limit, it's a `RetryableQuotaError`. + * - If the error message contains the phrase "Please retry in X[s|ms]", it's a `RetryableQuotaError`. * * @param error The error to classify. * @returns A `TerminalQuotaError`, `RetryableQuotaError`, or the original `unknown` error. @@ -72,6 +77,24 @@ export function classifyGoogleError(error: unknown): unknown { const googleApiError = parseGoogleApiError(error); if (!googleApiError || googleApiError.code !== 429) { + // Fallback: try to parse the error message for a retry delay + const errorMessage = error instanceof Error ? error.message : String(error); + const match = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/); + if (match?.[1]) { + const retryDelaySeconds = parseDurationInSeconds(match[1]); + if (retryDelaySeconds !== null) { + return new RetryableQuotaError( + errorMessage, + googleApiError ?? { + code: 429, + message: errorMessage, + details: [], + }, + retryDelaySeconds, + ); + } + } + return error; // Not a 429 error we can handle. }