summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
authorAbhi <[email protected]>2025-06-23 17:30:13 -0400
committerGitHub <[email protected]>2025-06-23 21:30:13 +0000
commitdc76bcc433d58d879f8850ac777d2cd239dad611 (patch)
tree89458926149f83721f00f09a5b5b3130d571bda5 /packages/cli/src
parent21e6a36cf1b17ff126b3d0253e68a6f3ebfc7c36 (diff)
Add error messaging for 429 errors (#1316)
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/nonInteractiveCli.test.ts3
-rw-r--r--packages/cli/src/nonInteractiveCli.ts4
-rw-r--r--packages/cli/src/ui/hooks/useGeminiStream.ts9
-rw-r--r--packages/cli/src/ui/utils/errorParsing.test.ts55
-rw-r--r--packages/cli/src/ui/utils/errorParsing.ts72
5 files changed, 110 insertions, 33 deletions
diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts
index 959cf03f..2bee1f24 100644
--- a/packages/cli/src/nonInteractiveCli.test.ts
+++ b/packages/cli/src/nonInteractiveCli.test.ts
@@ -215,8 +215,7 @@ describe('runNonInteractive', () => {
await runNonInteractive(mockConfig, 'Initial fail');
expect(consoleErrorSpy).toHaveBeenCalledWith(
- 'Error processing input:',
- apiError,
+ '[API Error: API connection failed]',
);
});
});
diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts
index 01ec62c8..02bbfd52 100644
--- a/packages/cli/src/nonInteractiveCli.ts
+++ b/packages/cli/src/nonInteractiveCli.ts
@@ -19,6 +19,8 @@ import {
GenerateContentResponse,
} from '@google/genai';
+import { parseAndFormatApiError } from './ui/utils/errorParsing.js';
+
function getResponseText(response: GenerateContentResponse): string | null {
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0];
@@ -126,7 +128,7 @@ export async function runNonInteractive(
}
}
} catch (error) {
- console.error('Error processing input:', error);
+ console.error(parseAndFormatApiError(error));
process.exit(1);
} finally {
if (isTelemetrySdkInitialized()) {
diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts
index 921fbdb1..e045fdeb 100644
--- a/packages/cli/src/ui/hooks/useGeminiStream.ts
+++ b/packages/cli/src/ui/hooks/useGeminiStream.ts
@@ -396,7 +396,10 @@ export const useGeminiStream = (
setPendingHistoryItem(null);
}
addItem(
- { type: MessageType.ERROR, text: `[API Error: ${eventValue.message}]` },
+ {
+ type: MessageType.ERROR,
+ text: parseAndFormatApiError(eventValue.error),
+ },
userMessageTimestamp,
);
},
@@ -530,6 +533,10 @@ export const useGeminiStream = (
setPendingHistoryItem(null);
}
} catch (error: unknown) {
+ console.log(
+ 'GEMINI_DEBUG: Caught error in useGeminiStream.ts:',
+ JSON.stringify(error),
+ );
if (isAuthError(error)) {
onAuthError();
} else if (!isNodeError(error) || error.name !== 'AbortError') {
diff --git a/packages/cli/src/ui/utils/errorParsing.test.ts b/packages/cli/src/ui/utils/errorParsing.test.ts
index afee5793..0dbd75c8 100644
--- a/packages/cli/src/ui/utils/errorParsing.test.ts
+++ b/packages/cli/src/ui/utils/errorParsing.test.ts
@@ -6,29 +6,46 @@
import { describe, it, expect } from 'vitest';
import { parseAndFormatApiError } from './errorParsing.js';
+import { StructuredError } from '@gemini-cli/core';
describe('parseAndFormatApiError', () => {
+ const rateLimitMessage =
+ 'Please wait and try again later. To increase your limits, upgrade to a plan with higher limits, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey';
+
it('should format a valid API error JSON', () => {
const errorMessage =
'got status: 400 Bad Request. {"error":{"code":400,"message":"API key not valid. Please pass a valid API key.","status":"INVALID_ARGUMENT"}}';
const expected =
- 'API Error: API key not valid. Please pass a valid API key. (Status: INVALID_ARGUMENT)';
+ '[API Error: API key not valid. Please pass a valid API key. (Status: INVALID_ARGUMENT)]';
+ expect(parseAndFormatApiError(errorMessage)).toBe(expected);
+ });
+
+ it('should format a 429 API error JSON with the custom message', () => {
+ const errorMessage =
+ 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}';
+ const expected = `[API Error: Rate limit exceeded (Status: RESOURCE_EXHAUSTED)]\n${rateLimitMessage}`;
expect(parseAndFormatApiError(errorMessage)).toBe(expected);
});
it('should return the original message if it is not a JSON error', () => {
const errorMessage = 'This is a plain old error message';
- expect(parseAndFormatApiError(errorMessage)).toBe(errorMessage);
+ expect(parseAndFormatApiError(errorMessage)).toBe(
+ `[API Error: ${errorMessage}]`,
+ );
});
it('should return the original message for malformed JSON', () => {
const errorMessage = '[Stream Error: {"error": "malformed}';
- expect(parseAndFormatApiError(errorMessage)).toBe(errorMessage);
+ expect(parseAndFormatApiError(errorMessage)).toBe(
+ `[API Error: ${errorMessage}]`,
+ );
});
it('should handle JSON that does not match the ApiError structure', () => {
const errorMessage = '[Stream Error: {"not_an_error": "some other json"}]';
- expect(parseAndFormatApiError(errorMessage)).toBe(errorMessage);
+ expect(parseAndFormatApiError(errorMessage)).toBe(
+ `[API Error: ${errorMessage}]`,
+ );
});
it('should format a nested API error', () => {
@@ -49,8 +66,32 @@ describe('parseAndFormatApiError', () => {
},
});
- expect(parseAndFormatApiError(errorMessage)).toBe(
- "API Error: Gemini 2.5 Pro Preview doesn't have a free quota tier. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. (Status: Too Many Requests)",
- );
+ const expected = `[API Error: Gemini 2.5 Pro Preview doesn't have a free quota tier. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. (Status: Too Many Requests)]\n${rateLimitMessage}`;
+
+ expect(parseAndFormatApiError(errorMessage)).toBe(expected);
+ });
+
+ it('should format a StructuredError', () => {
+ const error: StructuredError = {
+ message: 'A structured error occurred',
+ status: 500,
+ };
+ const expected = '[API Error: A structured error occurred]';
+ expect(parseAndFormatApiError(error)).toBe(expected);
+ });
+
+ it('should format a 429 StructuredError with the custom message', () => {
+ const error: StructuredError = {
+ message: 'Rate limit exceeded',
+ status: 429,
+ };
+ const expected = `[API Error: Rate limit exceeded]\n${rateLimitMessage}`;
+ expect(parseAndFormatApiError(error)).toBe(expected);
+ });
+
+ it('should handle an unknown error type', () => {
+ const error = 12345;
+ const expected = '[API Error: An unknown error occurred.]';
+ expect(parseAndFormatApiError(error)).toBe(expected);
});
});
diff --git a/packages/cli/src/ui/utils/errorParsing.ts b/packages/cli/src/ui/utils/errorParsing.ts
index aec337b6..1aca15ae 100644
--- a/packages/cli/src/ui/utils/errorParsing.ts
+++ b/packages/cli/src/ui/utils/errorParsing.ts
@@ -4,6 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import { StructuredError } from '@gemini-cli/core';
+
+const RATE_LIMIT_ERROR_MESSAGE =
+ '\nPlease wait and try again later. To increase your limits, upgrade to a plan with higher limits, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey';
+
export interface ApiError {
error: {
code: number;
@@ -23,34 +28,57 @@ function isApiError(error: unknown): error is ApiError {
);
}
-export function parseAndFormatApiError(errorMessage: string): string {
- // The error message might be prefixed with some text, like "[Stream Error: ...]".
- // We want to find the start of the JSON object.
- const jsonStart = errorMessage.indexOf('{');
- if (jsonStart === -1) {
- return errorMessage; // Not a JSON error, return as is.
+function isStructuredError(error: unknown): error is StructuredError {
+ return (
+ typeof error === 'object' &&
+ error !== null &&
+ 'message' in error &&
+ typeof (error as StructuredError).message === 'string'
+ );
+}
+
+export function parseAndFormatApiError(error: unknown): string {
+ if (isStructuredError(error)) {
+ let text = `[API Error: ${error.message}]`;
+ if (error.status === 429) {
+ text += RATE_LIMIT_ERROR_MESSAGE;
+ }
+ return text;
}
- const jsonString = errorMessage.substring(jsonStart);
+ // The error message might be a string containing a JSON object.
+ if (typeof error === 'string') {
+ const jsonStart = error.indexOf('{');
+ if (jsonStart === -1) {
+ return `[API Error: ${error}]`; // Not a JSON error, return as is.
+ }
+
+ const jsonString = error.substring(jsonStart);
- try {
- const error = JSON.parse(jsonString) as unknown;
- if (isApiError(error)) {
- let finalMessage = error.error.message;
- try {
- // See if the message is a stringified JSON with another error
- const nestedError = JSON.parse(finalMessage) as unknown;
- if (isApiError(nestedError)) {
- finalMessage = nestedError.error.message;
+ try {
+ const parsedError = JSON.parse(jsonString) as unknown;
+ if (isApiError(parsedError)) {
+ let finalMessage = parsedError.error.message;
+ try {
+ // See if the message is a stringified JSON with another error
+ const nestedError = JSON.parse(finalMessage) as unknown;
+ if (isApiError(nestedError)) {
+ finalMessage = nestedError.error.message;
+ }
+ } catch (_e) {
+ // It's not a nested JSON error, so we just use the message as is.
+ }
+ let text = `[API Error: ${finalMessage} (Status: ${parsedError.error.status})]`;
+ if (parsedError.error.code === 429) {
+ text += RATE_LIMIT_ERROR_MESSAGE;
}
- } catch (_e) {
- // It's not a nested JSON error, so we just use the message as is.
+ return text;
}
- return `API Error: ${finalMessage} (Status: ${error.error.status})`;
+ } catch (_e) {
+ // Not a valid JSON, fall through and return the original message.
}
- } catch (_e) {
- // Not a valid JSON, fall through and return the original message.
+ return `[API Error: ${error}]`;
}
- return errorMessage;
+ return '[API Error: An unknown error occurred.]';
}