Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/core/src/utils/googleQuotaErrors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});

Expand Down Expand Up @@ -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.',
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -446,7 +446,7 @@ describe('classifyGoogleError', () => {

expect(result).toBeInstanceOf(RetryableQuotaError);
if (result instanceof RetryableQuotaError) {
expect(result.retryDelayMs).toBe(5000);
expect(result.retryDelayMs).toBeUndefined();
}
});
});
18 changes: 9 additions & 9 deletions packages/core/src/utils/googleQuotaErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand All @@ -24,28 +22,32 @@ 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;
}
}

/**
* 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;
}
}

Expand Down Expand Up @@ -124,7 +126,6 @@ export function classifyGoogleError(error: unknown): unknown {
message: errorMessage,
details: [],
},
DEFAULT_RETRYABLE_DELAY_SECOND,
);
}

Expand Down Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/utils/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ export async function retryWithBackoff<T>(

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(
Expand All @@ -240,8 +245,11 @@ export async function retryWithBackoff<T>(
: error;
}

if (classifiedError instanceof RetryableQuotaError) {
console.warn(
if (
classifiedError instanceof RetryableQuotaError &&
classifiedError.retryDelayMs !== undefined
) {
debugLogger.warn(
`Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`,
);
await delay(classifiedError.retryDelayMs, signal);
Expand Down