From ed4b440ba00d235fdaf4cd6b31d9bcfd69c5deb1 Mon Sep 17 00:00:00 2001 From: gemini-cli-robot Date: Thu, 18 Dec 2025 16:07:18 -0800 Subject: [PATCH] fix(patch): cherry-pick 9e6914d to release/v0.22.0-preview.2-pr-15288 to patch version v0.22.0-preview.2 and create version 0.22.0-preview.3 (#15294) Co-authored-by: Sehoon Shon --- .../core/src/utils/googleQuotaErrors.test.ts | 64 ++++++++++++++++++- packages/core/src/utils/googleQuotaErrors.ts | 30 +++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/googleQuotaErrors.test.ts b/packages/core/src/utils/googleQuotaErrors.test.ts index f6fd9f474c..791584e280 100644 --- a/packages/core/src/utils/googleQuotaErrors.test.ts +++ b/packages/core/src/utils/googleQuotaErrors.test.ts @@ -325,7 +325,7 @@ describe('classifyGoogleError', () => { expect(result).toBeInstanceOf(TerminalQuotaError); }); - it('should return original error for 429 without specific details', () => { + it('should return RetryableQuotaError for any 429', () => { const apiError: GoogleApiError = { code: 429, message: 'Too many requests', @@ -340,7 +340,10 @@ describe('classifyGoogleError', () => { vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError); const originalError = new Error(); const result = classifyGoogleError(originalError); - expect(result).toBe(originalError); + expect(result).toBeInstanceOf(RetryableQuotaError); + if (result instanceof RetryableQuotaError) { + expect(result.retryDelayMs).toBe(5000); + } }); it('should classify nested JSON string 404 error as ModelNotFoundError', () => { @@ -389,4 +392,61 @@ describe('classifyGoogleError', () => { }); } }); + + it('should return RetryableQuotaError with 5s fallback for generic 429 without specific message', () => { + const generic429 = { + status: 429, + message: 'Resource exhausted. No specific retry info.', + }; + + const result = classifyGoogleError(generic429); + + expect(result).toBeInstanceOf(RetryableQuotaError); + if (result instanceof RetryableQuotaError) { + expect(result.retryDelayMs).toBe(5000); + } + }); + + it('should return RetryableQuotaError with 5s fallback for 429 with empty details and no regex match', () => { + const errorWithEmptyDetails = { + error: { + code: 429, + message: 'A generic 429 error with no retry message.', + details: [], + }, + }; + + const result = classifyGoogleError(errorWithEmptyDetails); + + expect(result).toBeInstanceOf(RetryableQuotaError); + if (result instanceof RetryableQuotaError) { + expect(result.retryDelayMs).toBe(5000); + } + }); + + it('should return RetryableQuotaError with 5s fallback for 429 with some detail', () => { + const errorWithEmptyDetails = { + error: { + code: 429, + message: 'A generic 429 error with no retry message.', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.ErrorInfo', + reason: 'QUOTA_EXCEEDED', + domain: 'googleapis.com', + metadata: { + quota_limit: '', + }, + }, + ], + }, + }; + + const result = classifyGoogleError(errorWithEmptyDetails); + + expect(result).toBeInstanceOf(RetryableQuotaError); + if (result instanceof RetryableQuotaError) { + expect(result.retryDelayMs).toBe(5000); + } + }); }); diff --git a/packages/core/src/utils/googleQuotaErrors.ts b/packages/core/src/utils/googleQuotaErrors.ts index 6f315f33e1..3878874c50 100644 --- a/packages/core/src/utils/googleQuotaErrors.ts +++ b/packages/core/src/utils/googleQuotaErrors.ts @@ -13,6 +13,8 @@ import type { import { parseGoogleApiError } from './googleErrors.js'; import { getErrorStatus, ModelNotFoundError } from './httpErrors.js'; +const DEFAULT_RETRYABLE_DELAY_SECOND = 5; + /** * A non-retryable error indicating a hard quota limit has been reached (e.g., daily limit). */ @@ -112,6 +114,18 @@ export function classifyGoogleError(error: unknown): unknown { retryDelaySeconds, ); } + } else if (status === 429) { + // Fallback: If it is a 429 but doesn't have a specific "retry in" message, + // assume it is a temporary rate limit and retry after 5 sec (same as DEFAULT_RETRY_OPTIONS). + return new RetryableQuotaError( + errorMessage, + googleApiError ?? { + code: 429, + message: errorMessage, + details: [], + }, + DEFAULT_RETRYABLE_DELAY_SECOND, + ); } return error; // Not a 429 error we can handle with structured details or a parsable retry message. @@ -232,5 +246,21 @@ export function classifyGoogleError(error: unknown): unknown { ); } } + + // If we reached this point and the status is still 429, we return retryable. + if (status === 429) { + const errorMessage = + googleApiError?.message || + (error instanceof Error ? error.message : String(error)); + return new RetryableQuotaError( + errorMessage, + googleApiError ?? { + code: 429, + message: errorMessage, + details: [], + }, + DEFAULT_RETRYABLE_DELAY_SECOND, + ); + } return error; // Fallback to original error if no specific classification fits. }