From a456a76f189a2d4a09ecc5fd67b33a40ee424bee Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Mon, 29 Dec 2025 17:23:23 -0500 Subject: [PATCH 1/2] =?UTF-8?q?Exponential=20back-off=20retries=20for=20re?= =?UTF-8?q?tryable=20error=20without=20a=20specified=20=E2=80=A6=20(#15684?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # packages/core/src/utils/retry.ts --- .../core/src/utils/googleQuotaErrors.test.ts | 14 +++++++------- packages/core/src/utils/googleQuotaErrors.ts | 18 +++++++++--------- packages/core/src/utils/retry.ts | 13 +++++++++++++ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/core/src/utils/googleQuotaErrors.test.ts b/packages/core/src/utils/googleQuotaErrors.test.ts index 791584e2809..5aaaf16b768 100644 --- a/packages/core/src/utils/googleQuotaErrors.test.ts +++ b/packages/core/src/utils/googleQuotaErrors.test.ts @@ -342,7 +342,7 @@ describe('classifyGoogleError', () => { const result = classifyGoogleError(originalError); expect(result).toBeInstanceOf(RetryableQuotaError); if (result instanceof RetryableQuotaError) { - expect(result.retryDelayMs).toBe(5000); + expect(result.retryDelayMs).toBeUndefined(); } }); @@ -393,7 +393,7 @@ describe('classifyGoogleError', () => { } }); - it('should return RetryableQuotaError with 5s fallback for generic 429 without specific message', () => { + it('should return RetryableQuotaError without delay time for generic 429 without specific message', () => { const generic429 = { status: 429, message: 'Resource exhausted. No specific retry info.', @@ -403,11 +403,11 @@ describe('classifyGoogleError', () => { expect(result).toBeInstanceOf(RetryableQuotaError); if (result instanceof RetryableQuotaError) { - expect(result.retryDelayMs).toBe(5000); + expect(result.retryDelayMs).toBeUndefined(); } }); - it('should return RetryableQuotaError with 5s fallback for 429 with empty details and no regex match', () => { + it('should return RetryableQuotaError without delay time for 429 with empty details and no regex match', () => { const errorWithEmptyDetails = { error: { code: 429, @@ -420,11 +420,11 @@ describe('classifyGoogleError', () => { expect(result).toBeInstanceOf(RetryableQuotaError); if (result instanceof RetryableQuotaError) { - expect(result.retryDelayMs).toBe(5000); + expect(result.retryDelayMs).toBeUndefined(); } }); - it('should return RetryableQuotaError with 5s fallback for 429 with some detail', () => { + it('should return RetryableQuotaError without delay time for 429 with some detail', () => { const errorWithEmptyDetails = { error: { code: 429, @@ -446,7 +446,7 @@ describe('classifyGoogleError', () => { expect(result).toBeInstanceOf(RetryableQuotaError); if (result instanceof RetryableQuotaError) { - expect(result.retryDelayMs).toBe(5000); + expect(result.retryDelayMs).toBeUndefined(); } }); }); diff --git a/packages/core/src/utils/googleQuotaErrors.ts b/packages/core/src/utils/googleQuotaErrors.ts index 3878874c502..4c1234010f4 100644 --- a/packages/core/src/utils/googleQuotaErrors.ts +++ b/packages/core/src/utils/googleQuotaErrors.ts @@ -13,8 +13,6 @@ 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). */ @@ -24,11 +22,13 @@ export class TerminalQuotaError extends Error { constructor( message: string, override readonly cause: GoogleApiError, - retryDelayMs?: number, + retryDelaySeconds?: number, ) { super(message); this.name = 'TerminalQuotaError'; - this.retryDelayMs = retryDelayMs ? retryDelayMs * 1000 : undefined; + this.retryDelayMs = retryDelaySeconds + ? retryDelaySeconds * 1000 + : undefined; } } @@ -36,16 +36,18 @@ export class TerminalQuotaError extends Error { * A retryable error indicating a temporary quota issue (e.g., per-minute limit). */ export class RetryableQuotaError extends Error { - retryDelayMs: number; + retryDelayMs?: number; constructor( message: string, override readonly cause: GoogleApiError, - retryDelaySeconds: number, + retryDelaySeconds?: number, ) { super(message); this.name = 'RetryableQuotaError'; - this.retryDelayMs = retryDelaySeconds * 1000; + this.retryDelayMs = retryDelaySeconds + ? retryDelaySeconds * 1000 + : undefined; } } @@ -124,7 +126,6 @@ export function classifyGoogleError(error: unknown): unknown { message: errorMessage, details: [], }, - DEFAULT_RETRYABLE_DELAY_SECOND, ); } @@ -259,7 +260,6 @@ export function classifyGoogleError(error: unknown): unknown { message: errorMessage, details: [], }, - DEFAULT_RETRYABLE_DELAY_SECOND, ); } return error; // Fallback to original error if no specific classification fits. diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index fd91cbd2ff9..d9eb3aad8e7 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -220,6 +220,11 @@ export async function retryWithBackoff( if (classifiedError instanceof RetryableQuotaError || is500) { if (attempt >= maxAttempts) { + const errorMessage = + classifiedError instanceof Error ? classifiedError.message : ''; + debugLogger.warn( + `Attempt ${attempt} failed${errorMessage ? `: ${errorMessage}` : ''}. Max attempts reached`, + ); if (onPersistent429) { try { const fallbackModel = await onPersistent429( @@ -240,8 +245,16 @@ export async function retryWithBackoff( : error; } +<<<<<<< HEAD if (classifiedError instanceof RetryableQuotaError) { console.warn( +======= + if ( + classifiedError instanceof RetryableQuotaError && + classifiedError.retryDelayMs !== undefined + ) { + debugLogger.warn( +>>>>>>> 07e597de4 (Exponential back-off retries for retryable error without a specified … (#15684)) `Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`, ); await delay(classifiedError.retryDelayMs, signal); From 405b942fefbe6a6f0c429a120d1f2a559fadbd42 Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Tue, 30 Dec 2025 16:11:33 -0500 Subject: [PATCH 2/2] resolve conflict --- packages/core/src/utils/retry.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index d9eb3aad8e7..c6296f2d95b 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -245,16 +245,11 @@ export async function retryWithBackoff( : error; } -<<<<<<< HEAD - if (classifiedError instanceof RetryableQuotaError) { - console.warn( -======= if ( classifiedError instanceof RetryableQuotaError && classifiedError.retryDelayMs !== undefined ) { debugLogger.warn( ->>>>>>> 07e597de4 (Exponential back-off retries for retryable error without a specified … (#15684)) `Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`, ); await delay(classifiedError.retryDelayMs, signal);