mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(core): treat RESOURCE_EXHAUSTED 429 errors without details as TerminalQuotaError
This commit is contained in:
@@ -128,12 +128,14 @@ export interface GoogleApiError {
|
|||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
details: GoogleApiErrorDetail[];
|
details: GoogleApiErrorDetail[];
|
||||||
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorShape = {
|
type ErrorShape = {
|
||||||
message?: string;
|
message?: string;
|
||||||
details?: unknown[];
|
details?: unknown[];
|
||||||
code?: number;
|
code?: number;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,6 +215,7 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null {
|
|||||||
const code = currentError.code;
|
const code = currentError.code;
|
||||||
const message = currentError.message;
|
const message = currentError.message;
|
||||||
const errorDetails = currentError.details;
|
const errorDetails = currentError.details;
|
||||||
|
const status = currentError.status;
|
||||||
|
|
||||||
if (code && message) {
|
if (code && message) {
|
||||||
const details: GoogleApiErrorDetail[] = [];
|
const details: GoogleApiErrorDetail[] = [];
|
||||||
@@ -231,7 +234,7 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null {
|
|||||||
}
|
}
|
||||||
// Basic structural check before casting.
|
// Basic structural check before casting.
|
||||||
// Since the proto definitions are loose, we primarily rely on @type presence.
|
// Since the proto definitions are loose, we primarily rely on @type presence.
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
if (typeof detailObj['@type'] === 'string') {
|
if (typeof detailObj['@type'] === 'string') {
|
||||||
// We can just cast it; the consumer will have to switch on @type
|
// We can just cast it; the consumer will have to switch on @type
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
@@ -246,6 +249,7 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null {
|
|||||||
code,
|
code,
|
||||||
message,
|
message,
|
||||||
details,
|
details,
|
||||||
|
status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,23 @@ describe('classifyGoogleError', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return TerminalQuotaError for 429 when status is RESOURCE_EXHAUSTED and details are empty', () => {
|
||||||
|
const apiError: GoogleApiError = {
|
||||||
|
code: 429,
|
||||||
|
status: 'RESOURCE_EXHAUSTED',
|
||||||
|
message: 'Your prepayment funds are depleted.',
|
||||||
|
details: [],
|
||||||
|
};
|
||||||
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
|
||||||
|
const originalError = new Error('Your prepayment funds are depleted.');
|
||||||
|
const result = classifyGoogleError(originalError);
|
||||||
|
expect(result).toBeInstanceOf(TerminalQuotaError);
|
||||||
|
if (result instanceof TerminalQuotaError) {
|
||||||
|
expect(result.cause).toBe(apiError);
|
||||||
|
expect(result.message).toBe('Your prepayment funds are depleted.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should return original error if code is not 429, 499 or 503', () => {
|
it('should return original error if code is not 429, 499 or 503', () => {
|
||||||
const apiError: GoogleApiError = {
|
const apiError: GoogleApiError = {
|
||||||
code: 500,
|
code: 500,
|
||||||
|
|||||||
@@ -268,6 +268,13 @@ export function classifyGoogleError(error: unknown): unknown {
|
|||||||
} else if (status === 429 || status === 499) {
|
} else if (status === 429 || status === 499) {
|
||||||
// Fallback: If it is a 429 or 499 but doesn't have a specific "retry in" message,
|
// Fallback: If it is a 429 or 499 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).
|
// assume it is a temporary rate limit and retry after 5 sec (same as DEFAULT_RETRY_OPTIONS).
|
||||||
|
|
||||||
|
// However, if the API explicitly returns RESOURCE_EXHAUSTED without details,
|
||||||
|
// it indicates a hard quota exhaustion rather than a transient rate limit.
|
||||||
|
if (googleApiError?.status === 'RESOURCE_EXHAUSTED') {
|
||||||
|
return new TerminalQuotaError(errorMessage, googleApiError);
|
||||||
|
}
|
||||||
|
|
||||||
return new RetryableQuotaError(
|
return new RetryableQuotaError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
googleApiError ?? {
|
googleApiError ?? {
|
||||||
@@ -405,6 +412,11 @@ export function classifyGoogleError(error: unknown): unknown {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
googleApiError?.message ||
|
googleApiError?.message ||
|
||||||
(error instanceof Error ? error.message : String(error));
|
(error instanceof Error ? error.message : String(error));
|
||||||
|
|
||||||
|
if (googleApiError?.status === 'RESOURCE_EXHAUSTED') {
|
||||||
|
return new TerminalQuotaError(errorMessage, googleApiError);
|
||||||
|
}
|
||||||
|
|
||||||
return new RetryableQuotaError(
|
return new RetryableQuotaError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
googleApiError ?? {
|
googleApiError ?? {
|
||||||
|
|||||||
Reference in New Issue
Block a user