mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-12 07:01:09 -07:00
Exponential back-off retries for retryable error without a specified … (#15684)
This commit is contained in:
@@ -342,7 +342,7 @@ describe('classifyGoogleError', () => {
|
|||||||
const result = classifyGoogleError(originalError);
|
const result = classifyGoogleError(originalError);
|
||||||
expect(result).toBeInstanceOf(RetryableQuotaError);
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
||||||
if (result instanceof 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 = {
|
const generic429 = {
|
||||||
status: 429,
|
status: 429,
|
||||||
message: 'Resource exhausted. No specific retry info.',
|
message: 'Resource exhausted. No specific retry info.',
|
||||||
@@ -403,11 +403,11 @@ describe('classifyGoogleError', () => {
|
|||||||
|
|
||||||
expect(result).toBeInstanceOf(RetryableQuotaError);
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
||||||
if (result instanceof 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 = {
|
const errorWithEmptyDetails = {
|
||||||
error: {
|
error: {
|
||||||
code: 429,
|
code: 429,
|
||||||
@@ -420,11 +420,11 @@ describe('classifyGoogleError', () => {
|
|||||||
|
|
||||||
expect(result).toBeInstanceOf(RetryableQuotaError);
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
||||||
if (result instanceof 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 = {
|
const errorWithEmptyDetails = {
|
||||||
error: {
|
error: {
|
||||||
code: 429,
|
code: 429,
|
||||||
@@ -446,7 +446,7 @@ describe('classifyGoogleError', () => {
|
|||||||
|
|
||||||
expect(result).toBeInstanceOf(RetryableQuotaError);
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
||||||
if (result instanceof RetryableQuotaError) {
|
if (result instanceof RetryableQuotaError) {
|
||||||
expect(result.retryDelayMs).toBe(5000);
|
expect(result.retryDelayMs).toBeUndefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import type {
|
|||||||
import { parseGoogleApiError } from './googleErrors.js';
|
import { parseGoogleApiError } from './googleErrors.js';
|
||||||
import { getErrorStatus, ModelNotFoundError } from './httpErrors.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).
|
* 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(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
override readonly cause: GoogleApiError,
|
override readonly cause: GoogleApiError,
|
||||||
retryDelayMs?: number,
|
retryDelaySeconds?: number,
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'TerminalQuotaError';
|
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).
|
* A retryable error indicating a temporary quota issue (e.g., per-minute limit).
|
||||||
*/
|
*/
|
||||||
export class RetryableQuotaError extends Error {
|
export class RetryableQuotaError extends Error {
|
||||||
retryDelayMs: number;
|
retryDelayMs?: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
override readonly cause: GoogleApiError,
|
override readonly cause: GoogleApiError,
|
||||||
retryDelaySeconds: number,
|
retryDelaySeconds?: number,
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'RetryableQuotaError';
|
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,
|
message: errorMessage,
|
||||||
details: [],
|
details: [],
|
||||||
},
|
},
|
||||||
DEFAULT_RETRYABLE_DELAY_SECOND,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +260,6 @@ export function classifyGoogleError(error: unknown): unknown {
|
|||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
details: [],
|
details: [],
|
||||||
},
|
},
|
||||||
DEFAULT_RETRYABLE_DELAY_SECOND,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return error; // Fallback to original error if no specific classification fits.
|
return error; // Fallback to original error if no specific classification fits.
|
||||||
|
|||||||
@@ -220,6 +220,11 @@ export async function retryWithBackoff<T>(
|
|||||||
|
|
||||||
if (classifiedError instanceof RetryableQuotaError || is500) {
|
if (classifiedError instanceof RetryableQuotaError || is500) {
|
||||||
if (attempt >= maxAttempts) {
|
if (attempt >= maxAttempts) {
|
||||||
|
const errorMessage =
|
||||||
|
classifiedError instanceof Error ? classifiedError.message : '';
|
||||||
|
debugLogger.warn(
|
||||||
|
`Attempt ${attempt} failed${errorMessage ? `: ${errorMessage}` : ''}. Max attempts reached`,
|
||||||
|
);
|
||||||
if (onPersistent429) {
|
if (onPersistent429) {
|
||||||
try {
|
try {
|
||||||
const fallbackModel = await onPersistent429(
|
const fallbackModel = await onPersistent429(
|
||||||
@@ -240,7 +245,10 @@ export async function retryWithBackoff<T>(
|
|||||||
: error;
|
: error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (classifiedError instanceof RetryableQuotaError) {
|
if (
|
||||||
|
classifiedError instanceof RetryableQuotaError &&
|
||||||
|
classifiedError.retryDelayMs !== undefined
|
||||||
|
) {
|
||||||
debugLogger.warn(
|
debugLogger.warn(
|
||||||
`Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`,
|
`Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user