Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
57 changes: 57 additions & 0 deletions packages/core/src/utils/googleQuotaErrors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,4 +389,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);
}
});
});
30 changes: 30 additions & 0 deletions packages/core/src/utils/googleQuotaErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
}
Loading