From b61a63aef4bcce9cb56fe46f10f0dc90b8fd6597 Mon Sep 17 00:00:00 2001 From: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:57:11 +0000 Subject: move errorParsing.ts to core (#6159) --- packages/cli/src/nonInteractiveCli.ts | 2 +- packages/cli/src/ui/hooks/useGeminiStream.test.tsx | 7 +- packages/cli/src/ui/hooks/useGeminiStream.ts | 2 +- packages/cli/src/ui/utils/errorParsing.test.ts | 378 --------------------- packages/cli/src/ui/utils/errorParsing.ts | 164 --------- packages/core/src/index.ts | 1 + packages/core/src/utils/errorParsing.test.ts | 377 ++++++++++++++++++++ packages/core/src/utils/errorParsing.ts | 166 +++++++++ packages/core/src/utils/quotaErrorDetection.ts | 7 +- 9 files changed, 550 insertions(+), 554 deletions(-) delete mode 100644 packages/cli/src/ui/utils/errorParsing.test.ts delete mode 100644 packages/cli/src/ui/utils/errorParsing.ts create mode 100644 packages/core/src/utils/errorParsing.test.ts create mode 100644 packages/core/src/utils/errorParsing.ts (limited to 'packages') diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index c237e56b..f2efe8fc 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -13,10 +13,10 @@ import { isTelemetrySdkInitialized, GeminiEventType, ToolErrorType, + parseAndFormatApiError, } from '@google/gemini-cli-core'; import { Content, Part, FunctionCall } from '@google/genai'; -import { parseAndFormatApiError } from './ui/utils/errorParsing.js'; import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; export async function runNonInteractive( diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 37d63e9a..9eed0912 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -51,6 +51,7 @@ const MockedGeminiClientClass = vi.hoisted(() => const MockedUserPromptEvent = vi.hoisted(() => vi.fn().mockImplementation(() => {}), ); +const mockParseAndFormatApiError = vi.hoisted(() => vi.fn()); vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actualCoreModule = (await importOriginal()) as any; @@ -59,6 +60,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { GitService: vi.fn(), GeminiClient: MockedGeminiClientClass, UserPromptEvent: MockedUserPromptEvent, + parseAndFormatApiError: mockParseAndFormatApiError, }; }); @@ -127,11 +129,6 @@ vi.mock('./slashCommandProcessor.js', () => ({ handleSlashCommand: vi.fn().mockReturnValue(false), })); -const mockParseAndFormatApiError = vi.hoisted(() => vi.fn()); -vi.mock('../utils/errorParsing.js', () => ({ - parseAndFormatApiError: mockParseAndFormatApiError, -})); - // --- END MOCKS --- describe('mergePartListUnions', () => { diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 6f3cb4fd..99b727b6 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -25,6 +25,7 @@ import { UnauthorizedError, UserPromptEvent, DEFAULT_GEMINI_FLASH_MODEL, + parseAndFormatApiError, } from '@google/gemini-cli-core'; import { type Part, type PartListUnion, FinishReason } from '@google/genai'; import { @@ -37,7 +38,6 @@ 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'; diff --git a/packages/cli/src/ui/utils/errorParsing.test.ts b/packages/cli/src/ui/utils/errorParsing.test.ts deleted file mode 100644 index 770dffad..00000000 --- a/packages/cli/src/ui/utils/errorParsing.test.ts +++ /dev/null @@ -1,378 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect } from 'vitest'; -import { parseAndFormatApiError } from './errorParsing.js'; -import { - AuthType, - UserTierId, - DEFAULT_GEMINI_FLASH_MODEL, - isProQuotaExceededError, -} from '@google/gemini-cli-core'; - -describe('parseAndFormatApiError', () => { - const _enterpriseMessage = - 'upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits'; - const vertexMessage = 'request a quota increase through Vertex'; - const geminiMessage = 'request a quota increase through AI Studio'; - - 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 format a 429 API error with the default message', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - undefined, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain( - 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', - ); - }); - - it('should format a 429 API error with the personal message', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain( - 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', - ); - }); - - it('should format a 429 API error with the vertex message', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError(errorMessage, AuthType.USE_VERTEX_AI); - expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain(vertexMessage); - }); - - 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( - `[API Error: ${errorMessage}]`, - ); - }); - - it('should return the original message for malformed JSON', () => { - const errorMessage = '[Stream Error: {"error": "malformed}'; - 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( - `[API Error: ${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', - }, - }); - - const result = parseAndFormatApiError(errorMessage, AuthType.USE_GEMINI); - expect(result).toContain('Gemini 2.5 Pro Preview'); - expect(result).toContain(geminiMessage); - }); - - 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 vertex message', () => { - const error: StructuredError = { - message: 'Rate limit exceeded', - status: 429, - }; - const result = parseAndFormatApiError(error, AuthType.USE_VERTEX_AI); - expect(result).toContain('[API Error: Rate limit exceeded]'); - expect(result).toContain(vertexMessage); - }); - - it('should handle an unknown error type', () => { - const error = 12345; - const expected = '[API Error: An unknown error occurred.]'; - expect(parseAndFormatApiError(error)).toBe(expected); - }); - - it('should format a 429 API error with Pro quota exceeded message for Google auth (Free tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", - ); - expect(result).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(result).toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); - }); - - it('should format a regular 429 API error with standard message for Google auth', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain( - 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', - ); - expect(result).not.toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - }); - - it('should format a 429 API error with generic quota exceeded message for Google auth', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'GenerationRequests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'GenerationRequests'", - ); - expect(result).toContain('You have reached your daily quota limit'); - expect(result).not.toContain( - 'You have reached your daily Gemini 2.5 Pro quota limit', - ); - }); - - it('should prioritize Pro quota message over generic quota message for Google auth', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", - ); - expect(result).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(result).not.toContain('You have reached your daily quota limit'); - }); - - it('should format a 429 API error with Pro quota exceeded message for Google auth (Standard tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - UserTierId.STANDARD, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", - ); - expect(result).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(result).toContain( - 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', - ); - expect(result).not.toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); - }); - - it('should format a 429 API error with Pro quota exceeded message for Google auth (Legacy tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - UserTierId.LEGACY, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", - ); - expect(result).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(result).toContain( - 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', - ); - expect(result).not.toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); - }); - - it('should handle different Gemini 2.5 version strings in Pro quota exceeded errors', () => { - const errorMessage25 = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const errorMessagePreview = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5-preview Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - - const result25 = parseAndFormatApiError( - errorMessage25, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - const resultPreview = parseAndFormatApiError( - errorMessagePreview, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-2.5-preview-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - - expect(result25).toContain( - 'You have reached your daily gemini-2.5-pro quota limit', - ); - expect(resultPreview).toContain( - 'You have reached your daily gemini-2.5-preview-pro quota limit', - ); - expect(result25).toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); - expect(resultPreview).toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); - }); - - it('should not match non-Pro models with similar version strings', () => { - // Test that Flash models with similar version strings don't match - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini 2.5 Flash Requests' and limit", - ), - ).toBe(false); - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini 2.5-preview Flash Requests' and limit", - ), - ).toBe(false); - - // Test other model types - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini 2.5 Ultra Requests' and limit", - ), - ).toBe(false); - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini 2.5 Standard Requests' and limit", - ), - ).toBe(false); - - // Test generic quota messages - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'GenerationRequests' and limit", - ), - ).toBe(false); - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'EmbeddingRequests' and limit", - ), - ).toBe(false); - }); - - it('should format a generic quota exceeded message for Google auth (Standard tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'GenerationRequests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - UserTierId.STANDARD, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain( - "[API Error: Quota exceeded for quota metric 'GenerationRequests'", - ); - expect(result).toContain('You have reached your daily quota limit'); - expect(result).toContain( - 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', - ); - expect(result).not.toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); - }); - - it('should format a regular 429 API error with standard message for Google auth (Standard tier)', () => { - const errorMessage = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; - const result = parseAndFormatApiError( - errorMessage, - AuthType.LOGIN_WITH_GOOGLE, - UserTierId.STANDARD, - 'gemini-2.5-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain( - 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', - ); - expect(result).not.toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); - }); -}); diff --git a/packages/cli/src/ui/utils/errorParsing.ts b/packages/cli/src/ui/utils/errorParsing.ts deleted file mode 100644 index 5031bc0a..00000000 --- a/packages/cli/src/ui/utils/errorParsing.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - AuthType, - UserTierId, - DEFAULT_GEMINI_FLASH_MODEL, - DEFAULT_GEMINI_MODEL, - isProQuotaExceededError, - isGenericQuotaExceededError, - isApiError, - isStructuredError, -} from '@google/gemini-cli-core'; - -// Free Tier message functions -const getRateLimitErrorMessageGoogleFree = ( - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; - -const getRateLimitErrorMessageGoogleProQuotaFree = ( - currentModel: string = DEFAULT_GEMINI_MODEL, - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nYou have reached your daily ${currentModel} quota limit. You will be switched to the ${fallbackModel} model for the rest of this session. To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - -const getRateLimitErrorMessageGoogleGenericQuotaFree = () => - `\nYou have reached your daily quota limit. To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - -// Legacy/Standard Tier message functions -const getRateLimitErrorMessageGooglePaid = ( - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session. We appreciate you for choosing Gemini Code Assist and the Gemini CLI.`; - -const getRateLimitErrorMessageGoogleProQuotaPaid = ( - currentModel: string = DEFAULT_GEMINI_MODEL, - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nYou have reached your daily ${currentModel} quota limit. You will be switched to the ${fallbackModel} model for the rest of this session. We appreciate you for choosing Gemini Code Assist and the Gemini CLI. To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; - -const getRateLimitErrorMessageGoogleGenericQuotaPaid = ( - currentModel: string = DEFAULT_GEMINI_MODEL, -) => - `\nYou have reached your daily quota limit. We appreciate you for choosing Gemini Code Assist and the Gemini CLI. To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; -const RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI = - '\nPlease wait and try again later. To increase your limits, request a quota increase through AI Studio, or switch to another /auth method'; -const RATE_LIMIT_ERROR_MESSAGE_VERTEX = - '\nPlease wait and try again later. To increase your limits, request a quota increase through Vertex, or switch to another /auth method'; -const getRateLimitErrorMessageDefault = ( - fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, -) => - `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; - -function getRateLimitMessage( - authType?: AuthType, - error?: unknown, - userTier?: UserTierId, - currentModel?: string, - fallbackModel?: string, -): string { - switch (authType) { - case AuthType.LOGIN_WITH_GOOGLE: { - // Determine if user is on a paid tier (Legacy or Standard) - default to FREE if not specified - const isPaidTier = - userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD; - - if (isProQuotaExceededError(error)) { - return isPaidTier - ? getRateLimitErrorMessageGoogleProQuotaPaid( - currentModel || DEFAULT_GEMINI_MODEL, - fallbackModel, - ) - : getRateLimitErrorMessageGoogleProQuotaFree( - currentModel || DEFAULT_GEMINI_MODEL, - fallbackModel, - ); - } else if (isGenericQuotaExceededError(error)) { - return isPaidTier - ? getRateLimitErrorMessageGoogleGenericQuotaPaid( - currentModel || DEFAULT_GEMINI_MODEL, - ) - : getRateLimitErrorMessageGoogleGenericQuotaFree(); - } else { - return isPaidTier - ? getRateLimitErrorMessageGooglePaid(fallbackModel) - : getRateLimitErrorMessageGoogleFree(fallbackModel); - } - } - case AuthType.USE_GEMINI: - return RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI; - case AuthType.USE_VERTEX_AI: - return RATE_LIMIT_ERROR_MESSAGE_VERTEX; - default: - return getRateLimitErrorMessageDefault(fallbackModel); - } -} - -export function parseAndFormatApiError( - error: unknown, - authType?: AuthType, - userTier?: UserTierId, - currentModel?: string, - fallbackModel?: string, -): string { - if (isStructuredError(error)) { - let text = `[API Error: ${error.message}]`; - if (error.status === 429) { - text += getRateLimitMessage( - authType, - error, - userTier, - currentModel, - fallbackModel, - ); - } - return text; - } - - // 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 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 += getRateLimitMessage( - authType, - parsedError, - userTier, - currentModel, - fallbackModel, - ); - } - return text; - } - } catch (_e) { - // Not a valid JSON, fall through and return the original message. - } - return `[API Error: ${error}]`; - } - - return '[API Error: An unknown error occurred.]'; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 791446e3..a24cddbe 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -41,6 +41,7 @@ export * from './utils/systemEncoding.js'; export * from './utils/textUtils.js'; export * from './utils/formatters.js'; export * from './utils/filesearch/fileSearch.js'; +export * from './utils/errorParsing.js'; // Export services export * from './services/fileDiscoveryService.js'; diff --git a/packages/core/src/utils/errorParsing.test.ts b/packages/core/src/utils/errorParsing.test.ts new file mode 100644 index 00000000..f2a4709a --- /dev/null +++ b/packages/core/src/utils/errorParsing.test.ts @@ -0,0 +1,377 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { parseAndFormatApiError } from './errorParsing.js'; +import { isProQuotaExceededError } from './quotaErrorDetection.js'; +import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; +import { UserTierId } from '../code_assist/types.js'; +import { AuthType } from '../core/contentGenerator.js'; +import { StructuredError } from '../core/turn.js'; + +describe('parseAndFormatApiError', () => { + const _enterpriseMessage = + 'upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits'; + const vertexMessage = 'request a quota increase through Vertex'; + const geminiMessage = 'request a quota increase through AI Studio'; + + 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 format a 429 API error with the default message', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + undefined, + undefined, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain('[API Error: Rate limit exceeded'); + expect(result).toContain( + 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', + ); + }); + + it('should format a 429 API error with the personal message', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain('[API Error: Rate limit exceeded'); + expect(result).toContain( + 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', + ); + }); + + it('should format a 429 API error with the vertex message', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError(errorMessage, AuthType.USE_VERTEX_AI); + expect(result).toContain('[API Error: Rate limit exceeded'); + expect(result).toContain(vertexMessage); + }); + + 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( + `[API Error: ${errorMessage}]`, + ); + }); + + it('should return the original message for malformed JSON', () => { + const errorMessage = '[Stream Error: {"error": "malformed}'; + 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( + `[API Error: ${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', + }, + }); + + const result = parseAndFormatApiError(errorMessage, AuthType.USE_GEMINI); + expect(result).toContain('Gemini 2.5 Pro Preview'); + expect(result).toContain(geminiMessage); + }); + + 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 vertex message', () => { + const error: StructuredError = { + message: 'Rate limit exceeded', + status: 429, + }; + const result = parseAndFormatApiError(error, AuthType.USE_VERTEX_AI); + expect(result).toContain('[API Error: Rate limit exceeded]'); + expect(result).toContain(vertexMessage); + }); + + it('should handle an unknown error type', () => { + const error = 12345; + const expected = '[API Error: An unknown error occurred.]'; + expect(parseAndFormatApiError(error)).toBe(expected); + }); + + it('should format a 429 API error with Pro quota exceeded message for Google auth (Free tier)', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain( + "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", + ); + expect(result).toContain( + 'You have reached your daily gemini-2.5-pro quota limit', + ); + expect(result).toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + }); + + it('should format a regular 429 API error with standard message for Google auth', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain('[API Error: Rate limit exceeded'); + expect(result).toContain( + 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', + ); + expect(result).not.toContain( + 'You have reached your daily gemini-2.5-pro quota limit', + ); + }); + + it('should format a 429 API error with generic quota exceeded message for Google auth', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'GenerationRequests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain( + "[API Error: Quota exceeded for quota metric 'GenerationRequests'", + ); + expect(result).toContain('You have reached your daily quota limit'); + expect(result).not.toContain( + 'You have reached your daily Gemini 2.5 Pro quota limit', + ); + }); + + it('should prioritize Pro quota message over generic quota message for Google auth', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain( + "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", + ); + expect(result).toContain( + 'You have reached your daily gemini-2.5-pro quota limit', + ); + expect(result).not.toContain('You have reached your daily quota limit'); + }); + + it('should format a 429 API error with Pro quota exceeded message for Google auth (Standard tier)', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + UserTierId.STANDARD, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain( + "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", + ); + expect(result).toContain( + 'You have reached your daily gemini-2.5-pro quota limit', + ); + expect(result).toContain( + 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', + ); + expect(result).not.toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + }); + + it('should format a 429 API error with Pro quota exceeded message for Google auth (Legacy tier)', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + UserTierId.LEGACY, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain( + "[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'", + ); + expect(result).toContain( + 'You have reached your daily gemini-2.5-pro quota limit', + ); + expect(result).toContain( + 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', + ); + expect(result).not.toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + }); + + it('should handle different Gemini 2.5 version strings in Pro quota exceeded errors', () => { + const errorMessage25 = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + const errorMessagePreview = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5-preview Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + + const result25 = parseAndFormatApiError( + errorMessage25, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + const resultPreview = parseAndFormatApiError( + errorMessagePreview, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-2.5-preview-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + + expect(result25).toContain( + 'You have reached your daily gemini-2.5-pro quota limit', + ); + expect(resultPreview).toContain( + 'You have reached your daily gemini-2.5-preview-pro quota limit', + ); + expect(result25).toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + expect(resultPreview).toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + }); + + it('should not match non-Pro models with similar version strings', () => { + // Test that Flash models with similar version strings don't match + expect( + isProQuotaExceededError( + "Quota exceeded for quota metric 'Gemini 2.5 Flash Requests' and limit", + ), + ).toBe(false); + expect( + isProQuotaExceededError( + "Quota exceeded for quota metric 'Gemini 2.5-preview Flash Requests' and limit", + ), + ).toBe(false); + + // Test other model types + expect( + isProQuotaExceededError( + "Quota exceeded for quota metric 'Gemini 2.5 Ultra Requests' and limit", + ), + ).toBe(false); + expect( + isProQuotaExceededError( + "Quota exceeded for quota metric 'Gemini 2.5 Standard Requests' and limit", + ), + ).toBe(false); + + // Test generic quota messages + expect( + isProQuotaExceededError( + "Quota exceeded for quota metric 'GenerationRequests' and limit", + ), + ).toBe(false); + expect( + isProQuotaExceededError( + "Quota exceeded for quota metric 'EmbeddingRequests' and limit", + ), + ).toBe(false); + }); + + it('should format a generic quota exceeded message for Google auth (Standard tier)', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'GenerationRequests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + UserTierId.STANDARD, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain( + "[API Error: Quota exceeded for quota metric 'GenerationRequests'", + ); + expect(result).toContain('You have reached your daily quota limit'); + expect(result).toContain( + 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', + ); + expect(result).not.toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + }); + + it('should format a regular 429 API error with standard message for Google auth (Standard tier)', () => { + const errorMessage = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}'; + const result = parseAndFormatApiError( + errorMessage, + AuthType.LOGIN_WITH_GOOGLE, + UserTierId.STANDARD, + 'gemini-2.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + expect(result).toContain('[API Error: Rate limit exceeded'); + expect(result).toContain( + 'We appreciate you for choosing Gemini Code Assist and the Gemini CLI', + ); + expect(result).not.toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + }); +}); diff --git a/packages/core/src/utils/errorParsing.ts b/packages/core/src/utils/errorParsing.ts new file mode 100644 index 00000000..aa15a652 --- /dev/null +++ b/packages/core/src/utils/errorParsing.ts @@ -0,0 +1,166 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + isProQuotaExceededError, + isGenericQuotaExceededError, + isApiError, + isStructuredError, +} from './quotaErrorDetection.js'; +import { + DEFAULT_GEMINI_MODEL, + DEFAULT_GEMINI_FLASH_MODEL, +} from '../config/models.js'; +import { UserTierId } from '../code_assist/types.js'; +import { AuthType } from '../core/contentGenerator.js'; + +// Free Tier message functions +const getRateLimitErrorMessageGoogleFree = ( + fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, +) => + `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; + +const getRateLimitErrorMessageGoogleProQuotaFree = ( + currentModel: string = DEFAULT_GEMINI_MODEL, + fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, +) => + `\nYou have reached your daily ${currentModel} quota limit. You will be switched to the ${fallbackModel} model for the rest of this session. To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; + +const getRateLimitErrorMessageGoogleGenericQuotaFree = () => + `\nYou have reached your daily quota limit. To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; + +// Legacy/Standard Tier message functions +const getRateLimitErrorMessageGooglePaid = ( + fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, +) => + `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session. We appreciate you for choosing Gemini Code Assist and the Gemini CLI.`; + +const getRateLimitErrorMessageGoogleProQuotaPaid = ( + currentModel: string = DEFAULT_GEMINI_MODEL, + fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, +) => + `\nYou have reached your daily ${currentModel} quota limit. You will be switched to the ${fallbackModel} model for the rest of this session. We appreciate you for choosing Gemini Code Assist and the Gemini CLI. To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; + +const getRateLimitErrorMessageGoogleGenericQuotaPaid = ( + currentModel: string = DEFAULT_GEMINI_MODEL, +) => + `\nYou have reached your daily quota limit. We appreciate you for choosing Gemini Code Assist and the Gemini CLI. To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; +const RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI = + '\nPlease wait and try again later. To increase your limits, request a quota increase through AI Studio, or switch to another /auth method'; +const RATE_LIMIT_ERROR_MESSAGE_VERTEX = + '\nPlease wait and try again later. To increase your limits, request a quota increase through Vertex, or switch to another /auth method'; +const getRateLimitErrorMessageDefault = ( + fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, +) => + `\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; + +function getRateLimitMessage( + authType?: AuthType, + error?: unknown, + userTier?: UserTierId, + currentModel?: string, + fallbackModel?: string, +): string { + switch (authType) { + case AuthType.LOGIN_WITH_GOOGLE: { + // Determine if user is on a paid tier (Legacy or Standard) - default to FREE if not specified + const isPaidTier = + userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD; + + if (isProQuotaExceededError(error)) { + return isPaidTier + ? getRateLimitErrorMessageGoogleProQuotaPaid( + currentModel || DEFAULT_GEMINI_MODEL, + fallbackModel, + ) + : getRateLimitErrorMessageGoogleProQuotaFree( + currentModel || DEFAULT_GEMINI_MODEL, + fallbackModel, + ); + } else if (isGenericQuotaExceededError(error)) { + return isPaidTier + ? getRateLimitErrorMessageGoogleGenericQuotaPaid( + currentModel || DEFAULT_GEMINI_MODEL, + ) + : getRateLimitErrorMessageGoogleGenericQuotaFree(); + } else { + return isPaidTier + ? getRateLimitErrorMessageGooglePaid(fallbackModel) + : getRateLimitErrorMessageGoogleFree(fallbackModel); + } + } + case AuthType.USE_GEMINI: + return RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI; + case AuthType.USE_VERTEX_AI: + return RATE_LIMIT_ERROR_MESSAGE_VERTEX; + default: + return getRateLimitErrorMessageDefault(fallbackModel); + } +} + +export function parseAndFormatApiError( + error: unknown, + authType?: AuthType, + userTier?: UserTierId, + currentModel?: string, + fallbackModel?: string, +): string { + if (isStructuredError(error)) { + let text = `[API Error: ${error.message}]`; + if (error.status === 429) { + text += getRateLimitMessage( + authType, + error, + userTier, + currentModel, + fallbackModel, + ); + } + return text; + } + + // 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 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 += getRateLimitMessage( + authType, + parsedError, + userTier, + currentModel, + fallbackModel, + ); + } + return text; + } + } catch (_e) { + // Not a valid JSON, fall through and return the original message. + } + return `[API Error: ${error}]`; + } + + return '[API Error: An unknown error occurred.]'; +} diff --git a/packages/core/src/utils/quotaErrorDetection.ts b/packages/core/src/utils/quotaErrorDetection.ts index 6fe9b312..1377b4fa 100644 --- a/packages/core/src/utils/quotaErrorDetection.ts +++ b/packages/core/src/utils/quotaErrorDetection.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { StructuredError } from '../core/turn.js'; + export interface ApiError { error: { code: number; @@ -13,11 +15,6 @@ export interface ApiError { }; } -interface StructuredError { - message: string; - status?: number; -} - export function isApiError(error: unknown): error is ApiError { return ( typeof error === 'object' && -- cgit v1.2.3