mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat: attempt more error parsing (#14899)
This commit is contained in:
@@ -94,7 +94,12 @@ describe('parseGoogleApiError', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(parseGoogleApiError(mockError)).toBeNull();
|
||||
|
||||
expect(parseGoogleApiError(mockError)).toEqual({
|
||||
code: 400,
|
||||
message: 'Bad Request',
|
||||
details: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if there are no valid details', () => {
|
||||
@@ -115,7 +120,11 @@ describe('parseGoogleApiError', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(parseGoogleApiError(mockError)).toBeNull();
|
||||
expect(parseGoogleApiError(mockError)).toEqual({
|
||||
code: 400,
|
||||
message: 'Bad Request',
|
||||
details: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse a doubly nested error in the message', () => {
|
||||
|
||||
@@ -190,32 +190,32 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null {
|
||||
const message = currentError.message;
|
||||
const errorDetails = currentError.details;
|
||||
|
||||
if (Array.isArray(errorDetails) && code && message) {
|
||||
if (code && message) {
|
||||
const details: GoogleApiErrorDetail[] = [];
|
||||
for (const detail of errorDetails) {
|
||||
if (detail && typeof detail === 'object') {
|
||||
const detailObj = detail as Record<string, unknown>;
|
||||
const typeKey = Object.keys(detailObj).find(
|
||||
(key) => key.trim() === '@type',
|
||||
);
|
||||
if (typeKey) {
|
||||
if (typeKey !== '@type') {
|
||||
detailObj['@type'] = detailObj[typeKey];
|
||||
delete detailObj[typeKey];
|
||||
if (Array.isArray(errorDetails)) {
|
||||
for (const detail of errorDetails) {
|
||||
if (detail && typeof detail === 'object') {
|
||||
const detailObj = detail as Record<string, unknown>;
|
||||
const typeKey = Object.keys(detailObj).find(
|
||||
(key) => key.trim() === '@type',
|
||||
);
|
||||
if (typeKey) {
|
||||
if (typeKey !== '@type') {
|
||||
detailObj['@type'] = detailObj[typeKey];
|
||||
delete detailObj[typeKey];
|
||||
}
|
||||
// We can just cast it; the consumer will have to switch on @type
|
||||
details.push(detailObj as unknown as GoogleApiErrorDetail);
|
||||
}
|
||||
// We can just cast it; the consumer will have to switch on @type
|
||||
details.push(detailObj as unknown as GoogleApiErrorDetail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
details,
|
||||
};
|
||||
}
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -288,6 +288,18 @@ function fromApiError(errorObj: object): ErrorShape | undefined {
|
||||
data = JSON.parse(data);
|
||||
} catch (_) {
|
||||
// Not a JSON string, can't parse.
|
||||
// Try one more fallback: look for the first '{' and last '}'
|
||||
if (typeof data === 'string') {
|
||||
const firstBrace = data.indexOf('{');
|
||||
const lastBrace = data.lastIndexOf('}');
|
||||
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
||||
try {
|
||||
data = JSON.parse(data.substring(firstBrace, lastBrace + 1));
|
||||
} catch (__) {
|
||||
// Still failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from './googleQuotaErrors.js';
|
||||
import * as errorParser from './googleErrors.js';
|
||||
import type { GoogleApiError } from './googleErrors.js';
|
||||
import { ModelNotFoundError } from './httpErrors.js';
|
||||
|
||||
describe('classifyGoogleError', () => {
|
||||
afterEach(() => {
|
||||
@@ -341,4 +342,51 @@ describe('classifyGoogleError', () => {
|
||||
const result = classifyGoogleError(originalError);
|
||||
expect(result).toBe(originalError);
|
||||
});
|
||||
|
||||
it('should classify nested JSON string 404 error as ModelNotFoundError', () => {
|
||||
// Mimic the double-wrapped JSON structure seen in the user report
|
||||
const innerError = {
|
||||
error: {
|
||||
code: 404,
|
||||
message:
|
||||
'models/NOT_FOUND is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.',
|
||||
status: 'NOT_FOUND',
|
||||
},
|
||||
};
|
||||
const errorString = JSON.stringify(innerError);
|
||||
|
||||
const outerErrorString = JSON.stringify({
|
||||
error: {
|
||||
message: errorString,
|
||||
},
|
||||
});
|
||||
const error = new Error(`[API Error: ${outerErrorString}]`);
|
||||
|
||||
const classified = classifyGoogleError(error);
|
||||
expect(classified).toBeInstanceOf(ModelNotFoundError);
|
||||
expect((classified as ModelNotFoundError).code).toBe(404);
|
||||
});
|
||||
|
||||
it('should fallback to string parsing for retry delays when details array is empty', () => {
|
||||
const errorWithEmptyDetails = {
|
||||
error: {
|
||||
code: 429,
|
||||
message: 'Resource exhausted. Please retry in 5s',
|
||||
details: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = classifyGoogleError(errorWithEmptyDetails);
|
||||
|
||||
expect(result).toBeInstanceOf(RetryableQuotaError);
|
||||
if (result instanceof RetryableQuotaError) {
|
||||
expect(result.retryDelayMs).toBe(5000);
|
||||
// The cause should be the parsed GoogleApiError
|
||||
expect(result.cause).toEqual({
|
||||
code: 429,
|
||||
message: 'Resource exhausted. Please retry in 5s',
|
||||
details: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,9 +89,15 @@ export function classifyGoogleError(error: unknown): unknown {
|
||||
return new ModelNotFoundError(message, status);
|
||||
}
|
||||
|
||||
if (!googleApiError || googleApiError.code !== 429) {
|
||||
if (
|
||||
!googleApiError ||
|
||||
googleApiError.code !== 429 ||
|
||||
googleApiError.details.length === 0
|
||||
) {
|
||||
// Fallback: try to parse the error message for a retry delay
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage =
|
||||
googleApiError?.message ||
|
||||
(error instanceof Error ? error.message : String(error));
|
||||
const match = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);
|
||||
if (match?.[1]) {
|
||||
const retryDelaySeconds = parseDurationInSeconds(match[1]);
|
||||
@@ -108,7 +114,7 @@ export function classifyGoogleError(error: unknown): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
return error; // Not a 429 error we can handle.
|
||||
return error; // Not a 429 error we can handle with structured details or a parsable retry message.
|
||||
}
|
||||
|
||||
const quotaFailure = googleApiError.details.find(
|
||||
|
||||
Reference in New Issue
Block a user