summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/ui/hooks/useGeminiStream.ts5
-rw-r--r--packages/cli/src/ui/utils/errorParsing.test.ts56
-rw-r--r--packages/cli/src/ui/utils/errorParsing.ts56
3 files changed, 116 insertions, 1 deletions
diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts
index caf82a47..5e741547 100644
--- a/packages/cli/src/ui/hooks/useGeminiStream.ts
+++ b/packages/cli/src/ui/hooks/useGeminiStream.ts
@@ -28,6 +28,7 @@ import {
ToolCallStatus,
} from '../types.js';
import { isAtCommand } from '../utils/commandUtils.js';
+import { parseAndFormatApiError } from '../utils/errorParsing.js';
import { useShellCommandProcessor } from './shellCommandProcessor.js';
import { handleAtCommand } from './atCommandProcessor.js';
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
@@ -467,7 +468,9 @@ export const useGeminiStream = (
addItem(
{
type: MessageType.ERROR,
- text: `[Stream Error: ${getErrorMessage(error) || 'Unknown error'}]`,
+ text: parseAndFormatApiError(
+ getErrorMessage(error) || 'Unknown error',
+ ),
},
userMessageTimestamp,
);
diff --git a/packages/cli/src/ui/utils/errorParsing.test.ts b/packages/cli/src/ui/utils/errorParsing.test.ts
new file mode 100644
index 00000000..afee5793
--- /dev/null
+++ b/packages/cli/src/ui/utils/errorParsing.test.ts
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import { parseAndFormatApiError } from './errorParsing.js';
+
+describe('parseAndFormatApiError', () => {
+ 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)';
+ 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);
+ });
+
+ it('should return the original message for malformed JSON', () => {
+ const errorMessage = '[Stream Error: {"error": "malformed}';
+ expect(parseAndFormatApiError(errorMessage)).toBe(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);
+ });
+
+ it('should format a nested API error', () => {
+ const nestedErrorMessage = JSON.stringify({
+ error: {
+ code: 429,
+ message:
+ "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: 'RESOURCE_EXHAUSTED',
+ },
+ });
+
+ const errorMessage = JSON.stringify({
+ error: {
+ code: 429,
+ message: nestedErrorMessage,
+ status: 'Too Many Requests',
+ },
+ });
+
+ 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)",
+ );
+ });
+});
diff --git a/packages/cli/src/ui/utils/errorParsing.ts b/packages/cli/src/ui/utils/errorParsing.ts
new file mode 100644
index 00000000..aec337b6
--- /dev/null
+++ b/packages/cli/src/ui/utils/errorParsing.ts
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export interface ApiError {
+ error: {
+ code: number;
+ message: string;
+ status: string;
+ details: unknown[];
+ };
+}
+
+function isApiError(error: unknown): error is ApiError {
+ return (
+ typeof error === 'object' &&
+ error !== null &&
+ 'error' in error &&
+ typeof (error as ApiError).error === 'object' &&
+ 'message' in (error as ApiError).error
+ );
+}
+
+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.
+ }
+
+ const jsonString = errorMessage.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;
+ }
+ } catch (_e) {
+ // It's not a nested JSON error, so we just use the message as is.
+ }
+ return `API Error: ${finalMessage} (Status: ${error.error.status})`;
+ }
+ } catch (_e) {
+ // Not a valid JSON, fall through and return the original message.
+ }
+
+ return errorMessage;
+}