From a06c8fe300cddd508085c803740d103c268e1607 Mon Sep 17 00:00:00 2001 From: Bryan Morgan Date: Wed, 9 Jul 2025 02:32:09 -0400 Subject: [PATCH 1/7] Added quota limit detection as well as Code Assist Tier-specific messaging for quotas and Flash failover --- packages/cli/src/ui/App.tsx | 28 ++- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 4 + packages/cli/src/ui/hooks/useGeminiStream.ts | 7 + .../cli/src/ui/utils/errorParsing.test.ts | 201 +++++++++++++++++- packages/cli/src/ui/utils/errorParsing.ts | 91 ++++---- packages/core/src/config/config.ts | 1 + packages/core/src/core/client.test.ts | 1 + packages/core/src/core/client.ts | 12 +- packages/core/src/core/geminiChat.ts | 12 +- packages/core/src/index.ts | 1 + .../utils/flashFallback.integration.test.ts | 1 + .../core/src/utils/quotaErrorDetection.ts | 79 +++++++ packages/core/src/utils/retry.test.ts | 2 +- packages/core/src/utils/retry.ts | 52 ++++- 14 files changed, 430 insertions(+), 62 deletions(-) create mode 100644 packages/core/src/utils/quotaErrorDetection.ts diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index feb132aeff6..82ddf354cec 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -67,6 +67,7 @@ import { useBracketedPaste } from './hooks/useBracketedPaste.js'; import { useTextBuffer } from './components/shared/text-buffer.js'; import * as fs from 'fs'; import { UpdateNotification } from './components/UpdateNotification.js'; +import { isProQuotaExceededError, isGenericQuotaExceededError } from '@google/gemini-cli-core'; import { checkForUpdates } from './utils/updateCheck.js'; import ansiEscapes from 'ansi-escapes'; import { OverflowProvider } from './contexts/OverflowContext.js'; @@ -243,15 +244,34 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { const flashFallbackHandler = async ( currentModel: string, fallbackModel: string, + error?: unknown, ): Promise => { + let message: string; + + // Check if this is a Pro quota exceeded error + if (error && isProQuotaExceededError(error)) { + message = `⚡ You have reached your daily ${currentModel} quota limit. +⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder 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 you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key +⚡ You can switch authentication methods by typing /auth`; + } else if (error && isGenericQuotaExceededError(error)) { + message = `⚡ You have reached your daily quota limit. +⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder 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 you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key +⚡ You can switch authentication methods by typing /auth`; + } else { + // Default fallback message for other cases (like consecutive 429s) + message = `⚡ Slow response times detected. +⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.`; + } + // Add message to UI history addItem( { type: MessageType.INFO, - text: `⚡ Slow response times detected. Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session. -⚡ To avoid this you can either upgrade to Standard tier. See: https://goo.gle/set-up-gemini-code-assist -⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key -⚡ You can switch authentication methods by typing /auth`, + text: message, }, Date.now(), ); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 3a0029196a0..fc6f93c57e2 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -1097,6 +1097,7 @@ describe('useGeminiStream', () => { getContentGeneratorConfig: vi.fn(() => ({ authType: mockAuthType, })), + getModel: vi.fn(() => 'gemini-2.5-pro'), } as unknown as Config; const { result } = renderHook(() => @@ -1125,6 +1126,9 @@ describe('useGeminiStream', () => { expect(mockParseAndFormatApiError).toHaveBeenCalledWith( 'Rate limit exceeded', mockAuthType, + undefined, + 'gemini-2.5-pro', + 'gemini-2.5-flash', ); }); }); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index b4acdb9a894..550cab86078 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -24,6 +24,7 @@ import { ThoughtSummary, UnauthorizedError, UserPromptEvent, + DEFAULT_GEMINI_FLASH_MODEL, } from '@google/gemini-cli-core'; import { type Part, type PartListUnion } from '@google/genai'; import { @@ -397,6 +398,9 @@ export const useGeminiStream = ( text: parseAndFormatApiError( eventValue.error, config.getContentGeneratorConfig().authType, + undefined, + config.getModel(), + DEFAULT_GEMINI_FLASH_MODEL, ), }, userMessageTimestamp, @@ -533,6 +537,9 @@ export const useGeminiStream = ( text: parseAndFormatApiError( getErrorMessage(error) || 'Unknown error', config.getContentGeneratorConfig().authType, + undefined, + config.getModel(), + DEFAULT_GEMINI_FLASH_MODEL, ), }, userMessageTimestamp, diff --git a/packages/cli/src/ui/utils/errorParsing.test.ts b/packages/cli/src/ui/utils/errorParsing.test.ts index 4bbaabf1881..fa167534c4f 100644 --- a/packages/cli/src/ui/utils/errorParsing.test.ts +++ b/packages/cli/src/ui/utils/errorParsing.test.ts @@ -6,10 +6,10 @@ import { describe, it, expect } from 'vitest'; import { parseAndFormatApiError } from './errorParsing.js'; -import { AuthType, StructuredError } from '@google/gemini-cli-core'; +import { AuthType, UserTierId, DEFAULT_GEMINI_FLASH_MODEL, isProQuotaExceededError } from '@google/gemini-cli-core'; describe('parseAndFormatApiError', () => { - const enterpriseMessage = 'upgrade to a plan with higher limits'; + 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'; @@ -24,9 +24,9 @@ describe('parseAndFormatApiError', () => { 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); + 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('Your request has been rate limited'); + expect(result).toContain('Slow response times detected. Switching to the gemini-2.5-flash model'); }); it('should format a 429 API error with the personal message', () => { @@ -35,9 +35,12 @@ describe('parseAndFormatApiError', () => { 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(enterpriseMessage); + expect(result).toContain('Slow response times detected. Switching to the gemini-2.5-flash model'); }); it('should format a 429 API error with the vertex message', () => { @@ -116,4 +119,192 @@ describe('parseAndFormatApiError', () => { 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('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 version strings in Pro quota exceeded errors', () => { + const errorMessage15 = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 1.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 errorMessageBeta = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini beta-3.0 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + const errorMessageExperimental = + 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini experimental-v2 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + + const result15 = parseAndFormatApiError( + errorMessage15, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-1.5-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + const resultPreview = parseAndFormatApiError( + errorMessagePreview, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-2.5-preview-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + const resultBeta = parseAndFormatApiError( + errorMessageBeta, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-beta-3.0-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + const resultExperimental = parseAndFormatApiError( + errorMessageExperimental, + AuthType.LOGIN_WITH_GOOGLE, + undefined, + 'gemini-experimental-v2-pro', + DEFAULT_GEMINI_FLASH_MODEL, + ); + + expect(result15).toContain('You have reached your daily gemini-1.5-pro quota limit'); + expect(resultPreview).toContain('You have reached your daily gemini-2.5-preview-pro quota limit'); + expect(resultBeta).toContain('You have reached your daily gemini-beta-3.0-pro quota limit'); + expect(resultExperimental).toContain('You have reached your daily gemini-experimental-v2-pro quota limit'); + expect(result15).toContain('upgrade to a Gemini Code Assist Standard or Enterprise plan'); + expect(resultPreview).toContain('upgrade to a Gemini Code Assist Standard or Enterprise plan'); + expect(resultBeta).toContain('upgrade to a Gemini Code Assist Standard or Enterprise plan'); + expect(resultExperimental).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); + expect(isProQuotaExceededError('Quota exceeded for quota metric \'Gemini beta-3.0 Flash Requests\' and limit')).toBe(false); + expect(isProQuotaExceededError('Quota exceeded for quota metric \'Gemini experimental-v2 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 index 3301481280c..2034d70ba64 100644 --- a/packages/cli/src/ui/utils/errorParsing.ts +++ b/packages/cli/src/ui/utils/errorParsing.ts @@ -4,66 +4,81 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AuthType, StructuredError } from '@google/gemini-cli-core'; +import { AuthType, UserTierId, DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_MODEL, isProQuotaExceededError, isGenericQuotaExceededError, isApiError, isStructuredError } from '@google/gemini-cli-core'; -const RATE_LIMIT_ERROR_MESSAGE_GOOGLE = - '\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'; +// Free Tier message functions +const getRateLimitErrorMessageGoogleFree = (fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL) => + `\nSlow 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) => + `\nSlow 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 RATE_LIMIT_ERROR_MESSAGE_DEFAULT = - 'Your request has been rate limited. Please wait and try again later.'; +const getRateLimitErrorMessageDefault = (fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL) => + `\nSlow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; -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 - ); -} - -function isStructuredError(error: unknown): error is StructuredError { - return ( - typeof error === 'object' && - error !== null && - 'message' in error && - typeof (error as StructuredError).message === 'string' - ); -} - -function getRateLimitMessage(authType?: AuthType): string { +function getRateLimitMessage( + authType?: AuthType, + error?: unknown, + userTier?: UserTierId, + currentModel?: string, + fallbackModel?: string, +): string { switch (authType) { - case AuthType.LOGIN_WITH_GOOGLE: - return RATE_LIMIT_ERROR_MESSAGE_GOOGLE; + 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 RATE_LIMIT_ERROR_MESSAGE_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); + text += getRateLimitMessage(authType, error, userTier, currentModel, fallbackModel); } return text; } @@ -92,7 +107,7 @@ export function parseAndFormatApiError( } let text = `[API Error: ${finalMessage} (Status: ${parsedError.error.status})]`; if (parsedError.error.code === 429) { - text += getRateLimitMessage(authType); + text += getRateLimitMessage(authType, parsedError, userTier, currentModel, fallbackModel); } return text; } diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 2cea70caf7e..b0659a9d8ac 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -103,6 +103,7 @@ export interface SandboxConfig { export type FlashFallbackHandler = ( currentModel: string, fallbackModel: string, + error?: unknown, ) => Promise; export interface ConfigParameters { diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 9d3791fdcfc..80680aca6f4 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -845,6 +845,7 @@ describe('Gemini Client (client.ts)', () => { expect(mockFallbackHandler).toHaveBeenCalledWith( currentModel, fallbackModel, + undefined, ); }); }); diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 6cfcd4074a7..bbf2250e75f 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -323,8 +323,8 @@ export class GeminiClient { }); const result = await retryWithBackoff(apiCall, { - onPersistent429: async (authType?: string) => - await this.handleFlashFallback(authType), + onPersistent429: async (authType?: string, error?: unknown) => + await this.handleFlashFallback(authType, error), authType: this.config.getContentGeneratorConfig()?.authType, }); @@ -411,8 +411,8 @@ export class GeminiClient { }); const result = await retryWithBackoff(apiCall, { - onPersistent429: async (authType?: string) => - await this.handleFlashFallback(authType), + onPersistent429: async (authType?: string, error?: unknown) => + await this.handleFlashFallback(authType, error), authType: this.config.getContentGeneratorConfig()?.authType, }); return result; @@ -559,7 +559,7 @@ export class GeminiClient { * Handles fallback to Flash model when persistent 429 errors occur for OAuth users. * Uses a fallback handler if provided by the config, otherwise returns null. */ - private async handleFlashFallback(authType?: string): Promise { + private async handleFlashFallback(authType?: string, error?: unknown): Promise { // Only handle fallback for OAuth users if (authType !== AuthType.LOGIN_WITH_GOOGLE) { return null; @@ -577,7 +577,7 @@ export class GeminiClient { const fallbackHandler = this.config.flashFallbackHandler; if (typeof fallbackHandler === 'function') { try { - const accepted = await fallbackHandler(currentModel, fallbackModel); + const accepted = await fallbackHandler(currentModel, fallbackModel, error); if (accepted) { this.config.setModel(fallbackModel); return fallbackModel; diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 537d55a07e8..9439505d334 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -191,7 +191,7 @@ export class GeminiChat { * Handles fallback to Flash model when persistent 429 errors occur for OAuth users. * Uses a fallback handler if provided by the config, otherwise returns null. */ - private async handleFlashFallback(authType?: string): Promise { + private async handleFlashFallback(authType?: string, error?: unknown): Promise { // Only handle fallback for OAuth users if (authType !== AuthType.LOGIN_WITH_GOOGLE) { return null; @@ -209,7 +209,7 @@ export class GeminiChat { const fallbackHandler = this.config.flashFallbackHandler; if (typeof fallbackHandler === 'function') { try { - const accepted = await fallbackHandler(currentModel, fallbackModel); + const accepted = await fallbackHandler(currentModel, fallbackModel, error); if (accepted) { this.config.setModel(fallbackModel); return fallbackModel; @@ -270,8 +270,8 @@ export class GeminiChat { } return false; }, - onPersistent429: async (authType?: string) => - await this.handleFlashFallback(authType), + onPersistent429: async (authType?: string, error?: unknown) => + await this.handleFlashFallback(authType, error), authType: this.config.getContentGeneratorConfig()?.authType, }); const durationMs = Date.now() - startTime; @@ -367,8 +367,8 @@ export class GeminiChat { } return false; // Don't retry other errors by default }, - onPersistent429: async (authType?: string) => - await this.handleFlashFallback(authType), + onPersistent429: async (authType?: string, error?: unknown) => + await this.handleFlashFallback(authType, error), authType: this.config.getContentGeneratorConfig()?.authType, }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index aff37f50699..df7db12c1a9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -32,6 +32,7 @@ export * from './utils/getFolderStructure.js'; export * from './utils/memoryDiscovery.js'; export * from './utils/gitIgnoreParser.js'; export * from './utils/editor.js'; +export * from './utils/quotaErrorDetection.js'; // Export services export * from './services/fileDiscoveryService.js'; diff --git a/packages/core/src/utils/flashFallback.integration.test.ts b/packages/core/src/utils/flashFallback.integration.test.ts index 6554425ff90..f5e354a0185 100644 --- a/packages/core/src/utils/flashFallback.integration.test.ts +++ b/packages/core/src/utils/flashFallback.integration.test.ts @@ -86,6 +86,7 @@ describe('Flash Fallback Integration', () => { expect(fallbackModel).toBe(DEFAULT_GEMINI_FLASH_MODEL); expect(mockFallbackHandler).toHaveBeenCalledWith( AuthType.LOGIN_WITH_GOOGLE, + expect.any(Error), ); expect(result).toBe('success after fallback'); // Should have: 2 failures, then fallback triggered, then 1 success after retry reset diff --git a/packages/core/src/utils/quotaErrorDetection.ts b/packages/core/src/utils/quotaErrorDetection.ts new file mode 100644 index 00000000000..090e7664b79 --- /dev/null +++ b/packages/core/src/utils/quotaErrorDetection.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface ApiError { + error: { + code: number; + message: string; + status: string; + details: unknown[]; + }; +} + +interface StructuredError { + message: string; + status?: number; +} + +export 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 isStructuredError(error: unknown): error is StructuredError { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as StructuredError).message === 'string' + ); +} + +export function isProQuotaExceededError(error: unknown): boolean { + // Regular expression to match "Quota exceeded for quota metric 'Gemini" followed by any version string and "Pro Requests'" + // This will match patterns like: + // - "Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'" + // - "Quota exceeded for quota metric 'Gemini 1.5-preview Pro Requests'" + // - "Quota exceeded for quota metric 'Gemini beta-3.0 Pro Requests'" + // - "Quota exceeded for quota metric 'Gemini experimental-v2 Pro Requests'" + // The pattern matches: "Gemini" + whitespace + any characters (non-greedy) + whitespace + "Pro Requests'" + const proQuotaRegex = /Quota exceeded for quota metric 'Gemini\s+.*?\s+Pro Requests'/; + + if (typeof error === 'string') { + return proQuotaRegex.test(error); + } + + if (isStructuredError(error)) { + return proQuotaRegex.test(error.message); + } + + if (isApiError(error)) { + return proQuotaRegex.test(error.error.message); + } + + return false; +} + +export function isGenericQuotaExceededError(error: unknown): boolean { + if (typeof error === 'string') { + return error.includes("Quota exceeded for quota metric"); + } + + if (isStructuredError(error)) { + return error.message.includes("Quota exceeded for quota metric"); + } + + if (isApiError(error)) { + return error.error.message.includes("Quota exceeded for quota metric"); + } + + return false; +} \ No newline at end of file diff --git a/packages/core/src/utils/retry.test.ts b/packages/core/src/utils/retry.test.ts index a0294c313bd..75cf8e635cc 100644 --- a/packages/core/src/utils/retry.test.ts +++ b/packages/core/src/utils/retry.test.ts @@ -357,7 +357,7 @@ describe('retryWithBackoff', () => { // Should fail with original error when fallback is rejected expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Rate limit exceeded'); - expect(fallbackCallback).toHaveBeenCalledWith('oauth-personal'); + expect(fallbackCallback).toHaveBeenCalledWith('oauth-personal', expect.any(Error)); }); it('should handle mixed error types (only count consecutive 429s)', async () => { diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index f3f5f2d2631..4523f949b46 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -5,13 +5,15 @@ */ import { AuthType } from '../core/contentGenerator.js'; +import { isProQuotaExceededError, isGenericQuotaExceededError } from './quotaErrorDetection.js'; + export interface RetryOptions { maxAttempts: number; initialDelayMs: number; maxDelayMs: number; shouldRetry: (error: Error) => boolean; - onPersistent429?: (authType?: string) => Promise; + onPersistent429?: (authType?: string, error?: unknown) => Promise; authType?: string; } @@ -86,6 +88,52 @@ export async function retryWithBackoff( } catch (error) { const errorStatus = getErrorStatus(error); + // Check for Pro quota exceeded error first - immediate fallback for OAuth users + if ( + errorStatus === 429 && + authType === AuthType.LOGIN_WITH_GOOGLE && + isProQuotaExceededError(error) && + onPersistent429 + ) { + try { + const fallbackModel = await onPersistent429(authType, error); + if (fallbackModel) { + // Reset attempt counter and try with new model + attempt = 0; + consecutive429Count = 0; + currentDelay = initialDelayMs; + // With the model updated, we continue to the next attempt + continue; + } + } catch (fallbackError) { + // If fallback fails, continue with original error + console.warn('Fallback to Flash model failed:', fallbackError); + } + } + + // Check for generic quota exceeded error - immediate fallback for OAuth users + if ( + errorStatus === 429 && + authType === AuthType.LOGIN_WITH_GOOGLE && + isGenericQuotaExceededError(error) && + onPersistent429 + ) { + try { + const fallbackModel = await onPersistent429(authType, error); + if (fallbackModel) { + // Reset attempt counter and try with new model + attempt = 0; + consecutive429Count = 0; + currentDelay = initialDelayMs; + // With the model updated, we continue to the next attempt + continue; + } + } catch (fallbackError) { + // If fallback fails, continue with original error + console.warn('Fallback to Flash model failed:', fallbackError); + } + } + // Track consecutive 429 errors if (errorStatus === 429) { consecutive429Count++; @@ -100,7 +148,7 @@ export async function retryWithBackoff( authType === AuthType.LOGIN_WITH_GOOGLE ) { try { - const fallbackModel = await onPersistent429(authType); + const fallbackModel = await onPersistent429(authType, error); if (fallbackModel) { // Reset attempt counter and try with new model attempt = 0; From 34a34ed91bf7b43fe349b24e6664d11ac9d531ff Mon Sep 17 00:00:00 2001 From: Bryan Morgan Date: Wed, 9 Jul 2025 02:32:40 -0400 Subject: [PATCH 2/7] Added quota limit detection as well as Code Assist Tier-specific messaging for quotas and Flash failover --- packages/cli/src/ui/App.tsx | 9 +- .../cli/src/ui/utils/errorParsing.test.ts | 202 ++++++++++++++---- packages/cli/src/ui/utils/errorParsing.ts | 83 +++++-- packages/core/src/core/client.ts | 11 +- packages/core/src/core/geminiChat.ts | 11 +- .../core/src/utils/quotaErrorDetection.ts | 25 +-- packages/core/src/utils/retry.test.ts | 5 +- packages/core/src/utils/retry.ts | 11 +- 8 files changed, 267 insertions(+), 90 deletions(-) diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 82ddf354cec..833cc2b5b66 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -67,7 +67,10 @@ import { useBracketedPaste } from './hooks/useBracketedPaste.js'; import { useTextBuffer } from './components/shared/text-buffer.js'; import * as fs from 'fs'; import { UpdateNotification } from './components/UpdateNotification.js'; -import { isProQuotaExceededError, isGenericQuotaExceededError } from '@google/gemini-cli-core'; +import { + isProQuotaExceededError, + isGenericQuotaExceededError, +} from '@google/gemini-cli-core'; import { checkForUpdates } from './utils/updateCheck.js'; import ansiEscapes from 'ansi-escapes'; import { OverflowProvider } from './contexts/OverflowContext.js'; @@ -247,7 +250,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { error?: unknown, ): Promise => { let message: string; - + // Check if this is a Pro quota exceeded error if (error && isProQuotaExceededError(error)) { message = `⚡ You have reached your daily ${currentModel} quota limit. @@ -266,7 +269,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { message = `⚡ Slow response times detected. ⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.`; } - + // Add message to UI history addItem( { diff --git a/packages/cli/src/ui/utils/errorParsing.test.ts b/packages/cli/src/ui/utils/errorParsing.test.ts index fa167534c4f..3d228efb423 100644 --- a/packages/cli/src/ui/utils/errorParsing.test.ts +++ b/packages/cli/src/ui/utils/errorParsing.test.ts @@ -6,10 +6,16 @@ import { describe, it, expect } from 'vitest'; import { parseAndFormatApiError } from './errorParsing.js'; -import { AuthType, UserTierId, DEFAULT_GEMINI_FLASH_MODEL, isProQuotaExceededError } from '@google/gemini-cli-core'; +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 _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'; @@ -24,9 +30,17 @@ describe('parseAndFormatApiError', () => { 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); + 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('Slow response times detected. Switching to the gemini-2.5-flash model'); + expect(result).toContain( + 'Slow response times detected. Switching to the gemini-2.5-flash model', + ); }); it('should format a 429 API error with the personal message', () => { @@ -40,7 +54,9 @@ describe('parseAndFormatApiError', () => { DEFAULT_GEMINI_FLASH_MODEL, ); expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain('Slow response times detected. Switching to the gemini-2.5-flash model'); + expect(result).toContain( + 'Slow response times detected. Switching to the gemini-2.5-flash model', + ); }); it('should format a 429 API error with the vertex message', () => { @@ -130,9 +146,15 @@ describe('parseAndFormatApiError', () => { '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'); + 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', () => { @@ -146,8 +168,12 @@ describe('parseAndFormatApiError', () => { DEFAULT_GEMINI_FLASH_MODEL, ); expect(result).toContain('[API Error: Rate limit exceeded'); - expect(result).toContain('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'); + expect(result).toContain( + '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', () => { @@ -160,9 +186,13 @@ describe('parseAndFormatApiError', () => { 'gemini-2.5-pro', DEFAULT_GEMINI_FLASH_MODEL, ); - expect(result).toContain('[API Error: Quota exceeded for quota metric \'GenerationRequests\''); + 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'); + 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', () => { @@ -175,8 +205,12 @@ describe('parseAndFormatApiError', () => { '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( + "[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'); }); @@ -190,10 +224,18 @@ describe('parseAndFormatApiError', () => { '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'); + 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)', () => { @@ -206,10 +248,18 @@ describe('parseAndFormatApiError', () => { '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'); + 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 version strings in Pro quota exceeded errors', () => { @@ -221,7 +271,7 @@ describe('parseAndFormatApiError', () => { 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini beta-3.0 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; const errorMessageExperimental = 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini experimental-v2 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - + const result15 = parseAndFormatApiError( errorMessage15, AuthType.LOGIN_WITH_GOOGLE, @@ -250,31 +300,79 @@ describe('parseAndFormatApiError', () => { 'gemini-experimental-v2-pro', DEFAULT_GEMINI_FLASH_MODEL, ); - - expect(result15).toContain('You have reached your daily gemini-1.5-pro quota limit'); - expect(resultPreview).toContain('You have reached your daily gemini-2.5-preview-pro quota limit'); - expect(resultBeta).toContain('You have reached your daily gemini-beta-3.0-pro quota limit'); - expect(resultExperimental).toContain('You have reached your daily gemini-experimental-v2-pro quota limit'); - expect(result15).toContain('upgrade to a Gemini Code Assist Standard or Enterprise plan'); - expect(resultPreview).toContain('upgrade to a Gemini Code Assist Standard or Enterprise plan'); - expect(resultBeta).toContain('upgrade to a Gemini Code Assist Standard or Enterprise plan'); - expect(resultExperimental).toContain('upgrade to a Gemini Code Assist Standard or Enterprise plan'); + + expect(result15).toContain( + 'You have reached your daily gemini-1.5-pro quota limit', + ); + expect(resultPreview).toContain( + 'You have reached your daily gemini-2.5-preview-pro quota limit', + ); + expect(resultBeta).toContain( + 'You have reached your daily gemini-beta-3.0-pro quota limit', + ); + expect(resultExperimental).toContain( + 'You have reached your daily gemini-experimental-v2-pro quota limit', + ); + expect(result15).toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + expect(resultPreview).toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + expect(resultBeta).toContain( + 'upgrade to a Gemini Code Assist Standard or Enterprise plan', + ); + expect(resultExperimental).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); - expect(isProQuotaExceededError('Quota exceeded for quota metric \'Gemini beta-3.0 Flash Requests\' and limit')).toBe(false); - expect(isProQuotaExceededError('Quota exceeded for quota metric \'Gemini experimental-v2 Flash Requests\' and limit')).toBe(false); - + 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); + expect( + isProQuotaExceededError( + "Quota exceeded for quota metric 'Gemini beta-3.0 Flash Requests' and limit", + ), + ).toBe(false); + expect( + isProQuotaExceededError( + "Quota exceeded for quota metric 'Gemini experimental-v2 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); - + 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); + 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)', () => { @@ -287,10 +385,16 @@ describe('parseAndFormatApiError', () => { 'gemini-2.5-pro', DEFAULT_GEMINI_FLASH_MODEL, ); - expect(result).toContain('[API Error: Quota exceeded for quota metric \'GenerationRequests\''); + 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'); + 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)', () => { @@ -304,7 +408,11 @@ describe('parseAndFormatApiError', () => { 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'); + 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 index 2034d70ba64..555d5e4ef9d 100644 --- a/packages/cli/src/ui/utils/errorParsing.ts +++ b/packages/cli/src/ui/utils/errorParsing.ts @@ -4,38 +4,60 @@ * 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'; +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) => +const getRateLimitErrorMessageGoogleFree = ( + fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, +) => `\nSlow 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) => +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) => +const getRateLimitErrorMessageGooglePaid = ( + fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, +) => `\nSlow 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) => +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) => +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) => +const getRateLimitErrorMessageDefault = ( + fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, +) => `\nSlow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; - function getRateLimitMessage( - authType?: AuthType, - error?: unknown, + authType?: AuthType, + error?: unknown, userTier?: UserTierId, currentModel?: string, fallbackModel?: string, @@ -43,18 +65,27 @@ function getRateLimitMessage( 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; - + 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); + 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) + return isPaidTier + ? getRateLimitErrorMessageGoogleGenericQuotaPaid( + currentModel || DEFAULT_GEMINI_MODEL, + ) : getRateLimitErrorMessageGoogleGenericQuotaFree(); } else { - return isPaidTier + return isPaidTier ? getRateLimitErrorMessageGooglePaid(fallbackModel) : getRateLimitErrorMessageGoogleFree(fallbackModel); } @@ -78,7 +109,13 @@ export function parseAndFormatApiError( if (isStructuredError(error)) { let text = `[API Error: ${error.message}]`; if (error.status === 429) { - text += getRateLimitMessage(authType, error, userTier, currentModel, fallbackModel); + text += getRateLimitMessage( + authType, + error, + userTier, + currentModel, + fallbackModel, + ); } return text; } @@ -107,7 +144,13 @@ export function parseAndFormatApiError( } let text = `[API Error: ${finalMessage} (Status: ${parsedError.error.status})]`; if (parsedError.error.code === 429) { - text += getRateLimitMessage(authType, parsedError, userTier, currentModel, fallbackModel); + text += getRateLimitMessage( + authType, + parsedError, + userTier, + currentModel, + fallbackModel, + ); } return text; } diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index bbf2250e75f..b8996cbf364 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -559,7 +559,10 @@ export class GeminiClient { * Handles fallback to Flash model when persistent 429 errors occur for OAuth users. * Uses a fallback handler if provided by the config, otherwise returns null. */ - private async handleFlashFallback(authType?: string, error?: unknown): Promise { + private async handleFlashFallback( + authType?: string, + error?: unknown, + ): Promise { // Only handle fallback for OAuth users if (authType !== AuthType.LOGIN_WITH_GOOGLE) { return null; @@ -577,7 +580,11 @@ export class GeminiClient { const fallbackHandler = this.config.flashFallbackHandler; if (typeof fallbackHandler === 'function') { try { - const accepted = await fallbackHandler(currentModel, fallbackModel, error); + const accepted = await fallbackHandler( + currentModel, + fallbackModel, + error, + ); if (accepted) { this.config.setModel(fallbackModel); return fallbackModel; diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 9439505d334..1be84f2e8a3 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -191,7 +191,10 @@ export class GeminiChat { * Handles fallback to Flash model when persistent 429 errors occur for OAuth users. * Uses a fallback handler if provided by the config, otherwise returns null. */ - private async handleFlashFallback(authType?: string, error?: unknown): Promise { + private async handleFlashFallback( + authType?: string, + error?: unknown, + ): Promise { // Only handle fallback for OAuth users if (authType !== AuthType.LOGIN_WITH_GOOGLE) { return null; @@ -209,7 +212,11 @@ export class GeminiChat { const fallbackHandler = this.config.flashFallbackHandler; if (typeof fallbackHandler === 'function') { try { - const accepted = await fallbackHandler(currentModel, fallbackModel, error); + const accepted = await fallbackHandler( + currentModel, + fallbackModel, + error, + ); if (accepted) { this.config.setModel(fallbackModel); return fallbackModel; diff --git a/packages/core/src/utils/quotaErrorDetection.ts b/packages/core/src/utils/quotaErrorDetection.ts index 090e7664b79..b2ca35e92eb 100644 --- a/packages/core/src/utils/quotaErrorDetection.ts +++ b/packages/core/src/utils/quotaErrorDetection.ts @@ -45,35 +45,36 @@ export function isProQuotaExceededError(error: unknown): boolean { // - "Quota exceeded for quota metric 'Gemini beta-3.0 Pro Requests'" // - "Quota exceeded for quota metric 'Gemini experimental-v2 Pro Requests'" // The pattern matches: "Gemini" + whitespace + any characters (non-greedy) + whitespace + "Pro Requests'" - const proQuotaRegex = /Quota exceeded for quota metric 'Gemini\s+.*?\s+Pro Requests'/; - + const proQuotaRegex = + /Quota exceeded for quota metric 'Gemini\s+.*?\s+Pro Requests'/; + if (typeof error === 'string') { return proQuotaRegex.test(error); } - + if (isStructuredError(error)) { return proQuotaRegex.test(error.message); } - + if (isApiError(error)) { return proQuotaRegex.test(error.error.message); } - + return false; } export function isGenericQuotaExceededError(error: unknown): boolean { if (typeof error === 'string') { - return error.includes("Quota exceeded for quota metric"); + return error.includes('Quota exceeded for quota metric'); } - + if (isStructuredError(error)) { - return error.message.includes("Quota exceeded for quota metric"); + return error.message.includes('Quota exceeded for quota metric'); } - + if (isApiError(error)) { - return error.error.message.includes("Quota exceeded for quota metric"); + return error.error.message.includes('Quota exceeded for quota metric'); } - + return false; -} \ No newline at end of file +} diff --git a/packages/core/src/utils/retry.test.ts b/packages/core/src/utils/retry.test.ts index 75cf8e635cc..f84d2004460 100644 --- a/packages/core/src/utils/retry.test.ts +++ b/packages/core/src/utils/retry.test.ts @@ -357,7 +357,10 @@ describe('retryWithBackoff', () => { // Should fail with original error when fallback is rejected expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Rate limit exceeded'); - expect(fallbackCallback).toHaveBeenCalledWith('oauth-personal', expect.any(Error)); + expect(fallbackCallback).toHaveBeenCalledWith( + 'oauth-personal', + expect.any(Error), + ); }); it('should handle mixed error types (only count consecutive 429s)', async () => { diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index 4523f949b46..c671e7f2856 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -5,15 +5,20 @@ */ import { AuthType } from '../core/contentGenerator.js'; -import { isProQuotaExceededError, isGenericQuotaExceededError } from './quotaErrorDetection.js'; - +import { + isProQuotaExceededError, + isGenericQuotaExceededError, +} from './quotaErrorDetection.js'; export interface RetryOptions { maxAttempts: number; initialDelayMs: number; maxDelayMs: number; shouldRetry: (error: Error) => boolean; - onPersistent429?: (authType?: string, error?: unknown) => Promise; + onPersistent429?: ( + authType?: string, + error?: unknown, + ) => Promise; authType?: string; } From 077eaae0c0ad5635885471f9d6427781a2c409af Mon Sep 17 00:00:00 2001 From: Bryan Morgan Date: Wed, 9 Jul 2025 02:50:14 -0400 Subject: [PATCH 3/7] addressed code review comment --- packages/core/src/utils/retry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index c671e7f2856..01651950b56 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -116,10 +116,11 @@ export async function retryWithBackoff( } } - // Check for generic quota exceeded error - immediate fallback for OAuth users + // Check for generic quota exceeded error (but not Pro, which was handled above) - immediate fallback for OAuth users if ( errorStatus === 429 && authType === AuthType.LOGIN_WITH_GOOGLE && + !isProQuotaExceededError(error) && isGenericQuotaExceededError(error) && onPersistent429 ) { From 7aa4c305f5aad4e961e16b6348e5c67eacac2394 Mon Sep 17 00:00:00 2001 From: Bryan Morgan Date: Wed, 9 Jul 2025 02:58:22 -0400 Subject: [PATCH 4/7] fixed code review scan issues --- packages/core/src/utils/quotaErrorDetection.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/core/src/utils/quotaErrorDetection.ts b/packages/core/src/utils/quotaErrorDetection.ts index b2ca35e92eb..ec77f5ee8cb 100644 --- a/packages/core/src/utils/quotaErrorDetection.ts +++ b/packages/core/src/utils/quotaErrorDetection.ts @@ -38,26 +38,28 @@ export function isStructuredError(error: unknown): error is StructuredError { } export function isProQuotaExceededError(error: unknown): boolean { - // Regular expression to match "Quota exceeded for quota metric 'Gemini" followed by any version string and "Pro Requests'" + // Check for Pro quota exceeded errors by looking for the specific pattern // This will match patterns like: // - "Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'" // - "Quota exceeded for quota metric 'Gemini 1.5-preview Pro Requests'" // - "Quota exceeded for quota metric 'Gemini beta-3.0 Pro Requests'" // - "Quota exceeded for quota metric 'Gemini experimental-v2 Pro Requests'" - // The pattern matches: "Gemini" + whitespace + any characters (non-greedy) + whitespace + "Pro Requests'" - const proQuotaRegex = - /Quota exceeded for quota metric 'Gemini\s+.*?\s+Pro Requests'/; + // We use string methods instead of regex to avoid ReDoS vulnerabilities + + const checkMessage = (message: string): boolean => + message.includes("Quota exceeded for quota metric 'Gemini") && + message.includes("Pro Requests'"); if (typeof error === 'string') { - return proQuotaRegex.test(error); + return checkMessage(error); } if (isStructuredError(error)) { - return proQuotaRegex.test(error.message); + return checkMessage(error.message); } if (isApiError(error)) { - return proQuotaRegex.test(error.error.message); + return checkMessage(error.error.message); } return false; From b524f8081040d9581e63f236cc9fef6d398c145c Mon Sep 17 00:00:00 2001 From: Bryan Morgan Date: Wed, 9 Jul 2025 11:41:26 -0400 Subject: [PATCH 5/7] Removed auto-execution on Flash after 429 failover --- packages/cli/src/ui/App.tsx | 13 +++++- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 14 ++++++ packages/cli/src/ui/hooks/useGeminiStream.ts | 16 +++++++ packages/core/src/config/config.ts | 11 ++++- packages/core/src/core/client.test.ts | 4 +- packages/core/src/core/client.ts | 32 ++++++++++++-- packages/core/src/core/geminiChat.test.ts | 2 + packages/core/src/core/geminiChat.ts | 44 ++++++++++++++++--- packages/core/src/utils/editCorrector.test.ts | 4 ++ packages/core/src/utils/retry.ts | 17 +++++-- 10 files changed, 139 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 833cc2b5b66..009c7e49cdb 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -136,6 +136,8 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { const ctrlDTimerRef = useRef(null); const [constrainHeight, setConstrainHeight] = useState(true); const [showPrivacyNotice, setShowPrivacyNotice] = useState(false); + const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] = + useState(false); const openPrivacyNotice = useCallback(() => { setShowPrivacyNotice(true); @@ -278,7 +280,14 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { }, Date.now(), ); - return true; // Always accept the fallback + + // Set the flag to prevent tool continuation + setModelSwitchedFromQuotaError(true); + // Set global quota error flag to prevent Flash model calls + config.setQuotaErrorOccurred(true); + // Switch model for future use but return false to stop current retry + config.setModel(fallbackModel); + return false; // Don't continue with current prompt }; config.setFlashFallbackHandler(flashFallbackHandler); @@ -445,6 +454,8 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { getPreferredEditor, onAuthError, performMemoryRefresh, + modelSwitchedFromQuotaError, + setModelSwitchedFromQuotaError, ); pendingHistoryItems.push(...pendingGeminiHistoryItems); const { elapsedTime, currentLoadingPhrase } = diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index fc6f93c57e2..6f6e2e2695b 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -386,6 +386,8 @@ describe('useGeminiStream', () => { () => 'vscode' as EditorType, () => {}, () => Promise.resolve(), + false, + () => {}, ); }, { @@ -518,6 +520,8 @@ describe('useGeminiStream', () => { () => 'vscode' as EditorType, () => {}, () => Promise.resolve(), + false, + () => {}, ), ); @@ -582,6 +586,8 @@ describe('useGeminiStream', () => { () => 'vscode' as EditorType, () => {}, () => Promise.resolve(), + false, + () => {}, ), ); @@ -675,6 +681,8 @@ describe('useGeminiStream', () => { () => 'vscode' as EditorType, () => {}, () => Promise.resolve(), + false, + () => {}, ), ); @@ -775,6 +783,8 @@ describe('useGeminiStream', () => { () => 'vscode' as EditorType, () => {}, () => Promise.resolve(), + false, + () => {}, ), ); @@ -1063,6 +1073,8 @@ describe('useGeminiStream', () => { () => 'vscode' as EditorType, () => {}, mockPerformMemoryRefresh, + false, + () => {}, ), ); @@ -1113,6 +1125,8 @@ describe('useGeminiStream', () => { () => 'vscode' as EditorType, () => {}, () => Promise.resolve(), + false, + () => {}, ), ); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 550cab86078..d32c9ffa5e7 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -90,6 +90,8 @@ export const useGeminiStream = ( getPreferredEditor: () => EditorType | undefined, onAuthError: () => void, performMemoryRefresh: () => Promise, + modelSwitchedFromQuotaError: boolean, + setModelSwitchedFromQuotaError: React.Dispatch>, ) => { const [initError, setInitError] = useState(null); const abortControllerRef = useRef(null); @@ -494,6 +496,12 @@ export const useGeminiStream = ( const userMessageTimestamp = Date.now(); setShowHelp(false); + // Reset quota error flag when starting a new query (not a continuation) + if (!options?.isContinuation) { + setModelSwitchedFromQuotaError(false); + config.setQuotaErrorOccurred(false); + } + abortControllerRef.current = new AbortController(); const abortSignal = abortControllerRef.current.signal; turnCancelledRef.current = false; @@ -552,6 +560,7 @@ export const useGeminiStream = ( [ streamingState, setShowHelp, + setModelSwitchedFromQuotaError, prepareQueryForGemini, processGeminiStreamEvents, pendingHistoryItemRef, @@ -668,6 +677,12 @@ export const useGeminiStream = ( ); markToolsAsSubmitted(callIdsToMarkAsSubmitted); + + // Don't continue if model was switched due to quota error + if (modelSwitchedFromQuotaError) { + return; + } + submitQuery(mergePartListUnions(responsesToSend), { isContinuation: true, }); @@ -678,6 +693,7 @@ export const useGeminiStream = ( markToolsAsSubmitted, geminiClient, performMemoryRefresh, + modelSwitchedFromQuotaError, ], ); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index b0659a9d8ac..51915fc867e 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -104,7 +104,7 @@ export type FlashFallbackHandler = ( currentModel: string, fallbackModel: string, error?: unknown, -) => Promise; +) => Promise; export interface ConfigParameters { sessionId: string; @@ -183,6 +183,7 @@ export class Config { private readonly listExtensions: boolean; private readonly _activeExtensions: ActiveExtension[]; flashFallbackHandler?: FlashFallbackHandler; + private quotaErrorOccurred: boolean = false; constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -304,6 +305,14 @@ export class Config { this.flashFallbackHandler = handler; } + setQuotaErrorOccurred(value: boolean): void { + this.quotaErrorOccurred = value; + } + + getQuotaErrorOccurred(): boolean { + return this.quotaErrorOccurred; + } + getEmbeddingModel(): string { return this.embeddingModel; } diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 80680aca6f4..cd77a3f7bbc 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -178,6 +178,8 @@ describe('Gemini Client (client.ts)', () => { getProxy: vi.fn().mockReturnValue(undefined), getWorkingDir: vi.fn().mockReturnValue('/test/dir'), getFileService: vi.fn().mockReturnValue(fileService), + getQuotaErrorOccurred: vi.fn().mockReturnValue(false), + setQuotaErrorOccurred: vi.fn(), }; return mock as unknown as Config; }); @@ -351,7 +353,7 @@ describe('Gemini Client (client.ts)', () => { await client.generateJson(contents, schema, abortSignal); expect(mockGenerateContentFn).toHaveBeenCalledWith({ - model: DEFAULT_GEMINI_FLASH_MODEL, + model: 'test-model', // Should use current model from config config: { abortSignal, systemInstruction: getCoreSystemPrompt(''), diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index b8996cbf364..51aab961bb3 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -262,6 +262,7 @@ export class GeminiClient { request: PartListUnion, signal: AbortSignal, turns: number = this.MAX_TURNS, + originalModel?: string, ): AsyncGenerator { // Ensure turns never exceeds MAX_TURNS to prevent infinite loops const boundedTurns = Math.min(turns, this.MAX_TURNS); @@ -269,6 +270,9 @@ export class GeminiClient { return new Turn(this.getChat()); } + // Track the original model from the first call to detect model switching + const initialModel = originalModel || this.config.getModel(); + const compressed = await this.tryCompressChat(); if (compressed) { yield { type: GeminiEventType.ChatCompressed, value: compressed }; @@ -279,6 +283,14 @@ export class GeminiClient { yield event; } if (!turn.pendingToolCalls.length && signal && !signal.aborted) { + // Check if model was switched during the call (likely due to quota error) + const currentModel = this.config.getModel(); + if (currentModel !== initialModel) { + // Model was switched (likely due to quota error fallback) + // Don't continue with recursive call to prevent unwanted Flash execution + return turn; + } + const nextSpeakerCheck = await checkNextSpeaker( this.getChat(), this, @@ -288,7 +300,12 @@ export class GeminiClient { const nextRequest = [{ text: 'Please continue.' }]; // This recursive call's events will be yielded out, but the final // turn object will be from the top-level call. - yield* this.sendMessageStream(nextRequest, signal, boundedTurns - 1); + yield* this.sendMessageStream( + nextRequest, + signal, + boundedTurns - 1, + initialModel, + ); } } return turn; @@ -298,9 +315,12 @@ export class GeminiClient { contents: Content[], schema: SchemaUnion, abortSignal: AbortSignal, - model: string = DEFAULT_GEMINI_FLASH_MODEL, + model?: string, config: GenerateContentConfig = {}, ): Promise> { + // Use current model from config instead of hardcoded Flash model + const modelToUse = + model || this.config.getModel() || DEFAULT_GEMINI_FLASH_MODEL; try { const userMemory = this.config.getUserMemory(); const systemInstruction = getCoreSystemPrompt(userMemory); @@ -312,7 +332,7 @@ export class GeminiClient { const apiCall = () => this.getContentGenerator().generateContent({ - model, + model: modelToUse, config: { ...requestConfig, systemInstruction, @@ -585,10 +605,14 @@ export class GeminiClient { fallbackModel, error, ); - if (accepted) { + if (accepted !== false && accepted !== null) { this.config.setModel(fallbackModel); return fallbackModel; } + // Check if the model was switched manually in the handler + if (this.config.getModel() === fallbackModel) { + return null; // Model was switched but don't continue with current prompt + } } catch (error) { console.warn('Flash fallback handler failed:', error); } diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index bfaeb8f628f..35e6bf6c5b5 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -43,6 +43,8 @@ describe('GeminiChat', () => { }), getModel: vi.fn().mockReturnValue('gemini-pro'), setModel: vi.fn(), + getQuotaErrorOccurred: vi.fn().mockReturnValue(false), + setQuotaErrorOccurred: vi.fn(), flashFallbackHandler: undefined, } as unknown as Config; diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 1be84f2e8a3..2c149e9380f 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -217,10 +217,14 @@ export class GeminiChat { fallbackModel, error, ); - if (accepted) { + if (accepted !== false && accepted !== null) { this.config.setModel(fallbackModel); return fallbackModel; } + // Check if the model was switched manually in the handler + if (this.config.getModel() === fallbackModel) { + return null; // Model was switched but don't continue with current prompt + } } catch (error) { console.warn('Flash fallback handler failed:', error); } @@ -262,12 +266,25 @@ export class GeminiChat { let response: GenerateContentResponse; try { - const apiCall = () => - this.contentGenerator.generateContent({ - model: this.config.getModel() || DEFAULT_GEMINI_FLASH_MODEL, + const apiCall = () => { + const modelToUse = this.config.getModel() || DEFAULT_GEMINI_FLASH_MODEL; + + // Prevent Flash model calls immediately after quota error + if ( + this.config.getQuotaErrorOccurred() && + modelToUse === DEFAULT_GEMINI_FLASH_MODEL + ) { + throw new Error( + 'Please submit a new query to continue with the Flash model.', + ); + } + + return this.contentGenerator.generateContent({ + model: modelToUse, contents: requestContents, config: { ...this.generationConfig, ...params.config }, }); + }; response = await retryWithBackoff(apiCall, { shouldRetry: (error: Error) => { @@ -354,12 +371,25 @@ export class GeminiChat { const startTime = Date.now(); try { - const apiCall = () => - this.contentGenerator.generateContentStream({ - model: this.config.getModel(), + const apiCall = () => { + const modelToUse = this.config.getModel(); + + // Prevent Flash model calls immediately after quota error + if ( + this.config.getQuotaErrorOccurred() && + modelToUse === DEFAULT_GEMINI_FLASH_MODEL + ) { + throw new Error( + 'Please submit a new query to continue with the Flash model.', + ); + } + + return this.contentGenerator.generateContentStream({ + model: modelToUse, contents: requestContents, config: { ...this.generationConfig, ...params.config }, }); + }; // Note: Retrying streams can be complex. If generateContentStream itself doesn't handle retries // for transient issues internally before yielding the async generator, this retry will re-initiate diff --git a/packages/core/src/utils/editCorrector.test.ts b/packages/core/src/utils/editCorrector.test.ts index bcf75dfe57b..cf9008ef815 100644 --- a/packages/core/src/utils/editCorrector.test.ts +++ b/packages/core/src/utils/editCorrector.test.ts @@ -214,6 +214,8 @@ describe('editCorrector', () => { setAlwaysSkipModificationConfirmation: vi.fn((skip: boolean) => { configParams.alwaysSkipModificationConfirmation = skip; }), + getQuotaErrorOccurred: vi.fn().mockReturnValue(false), + setQuotaErrorOccurred: vi.fn(), } as unknown as Config; callCount = 0; @@ -654,6 +656,8 @@ describe('editCorrector', () => { setAlwaysSkipModificationConfirmation: vi.fn((skip: boolean) => { configParams.alwaysSkipModificationConfirmation = skip; }), + getQuotaErrorOccurred: vi.fn().mockReturnValue(false), + setQuotaErrorOccurred: vi.fn(), } as unknown as Config; callCount = 0; diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index 01651950b56..e5d65751925 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -18,7 +18,7 @@ export interface RetryOptions { onPersistent429?: ( authType?: string, error?: unknown, - ) => Promise; + ) => Promise; authType?: string; } @@ -102,13 +102,16 @@ export async function retryWithBackoff( ) { try { const fallbackModel = await onPersistent429(authType, error); - if (fallbackModel) { + if (fallbackModel !== false && fallbackModel !== null) { // Reset attempt counter and try with new model attempt = 0; consecutive429Count = 0; currentDelay = initialDelayMs; // With the model updated, we continue to the next attempt continue; + } else { + // Fallback handler returned null/false, meaning don't continue - stop retry process + throw error; } } catch (fallbackError) { // If fallback fails, continue with original error @@ -126,13 +129,16 @@ export async function retryWithBackoff( ) { try { const fallbackModel = await onPersistent429(authType, error); - if (fallbackModel) { + if (fallbackModel !== false && fallbackModel !== null) { // Reset attempt counter and try with new model attempt = 0; consecutive429Count = 0; currentDelay = initialDelayMs; // With the model updated, we continue to the next attempt continue; + } else { + // Fallback handler returned null/false, meaning don't continue - stop retry process + throw error; } } catch (fallbackError) { // If fallback fails, continue with original error @@ -155,13 +161,16 @@ export async function retryWithBackoff( ) { try { const fallbackModel = await onPersistent429(authType, error); - if (fallbackModel) { + if (fallbackModel !== false && fallbackModel !== null) { // Reset attempt counter and try with new model attempt = 0; consecutive429Count = 0; currentDelay = initialDelayMs; // With the model updated, we continue to the next attempt continue; + } else { + // Fallback handler returned null/false, meaning don't continue - stop retry process + throw error; } } catch (fallbackError) { // If fallback fails, continue with original error From a90f3a1b2437a7b69564b43e39a27ad0075b7933 Mon Sep 17 00:00:00 2001 From: Jenna Inouye Date: Wed, 9 Jul 2025 09:23:58 -0700 Subject: [PATCH 6/7] Update Terms of Service and Privacy Notice for clarity. (#3036) --- docs/tos-privacy.md | 89 ++++++++++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/docs/tos-privacy.md b/docs/tos-privacy.md index 200a4a71cba..b2cbbc2908b 100644 --- a/docs/tos-privacy.md +++ b/docs/tos-privacy.md @@ -1,58 +1,87 @@ # Gemini CLI: Terms of Service and Privacy Notice -Gemini CLI is an open-source tool that lets you interact with Google's powerful language models directly from your command-line interface. The Terms of Service and Privacy notices that apply to your usage of Gemini CLI depend on the type of account you use to authenticate with Google. See [quota and pricing](./quota-and-pricing.md) for details on the quota and pricing details that apply to your usage of Gemini CLI. +Gemini CLI is an open-source tool that lets you interact with Google's powerful language models directly from your command-line interface. The Terms of Service and Privacy Notices that apply to your usage of the Gemini CLI depend on the type of account you use to authenticate with Google. -This article outlines the specific terms and privacy policies applicable for different auth methods. +This article outlines the specific terms and privacy policies applicable for different account types and authentication methods. Note: See [quotas and pricing](./quota-and-pricing.md) for the quota and pricing details that apply to your usage of the Gemini CLI. -## 1. Login with Google (Gemini Code Assist for [individuals](https://developers.google.com/gemini-code-assist/docs/overview#supported-features-gca)) +## How to determine your authentication method -For users who authenticate using their Google account to access Gemini Code Assist for individuals: +Your authentication method refers to the method you use to log into and access the Gemini CLI. There are four ways to authenticate: -- Terms of Service: Your use of Gemini CLI is governed by the general [Google Terms of Service](https://policies.google.com/terms?hl=en-US). -- Privacy Notice: The collection and use of your data are described in the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals). +- Logging in with your Google account to Gemini Code Assist for Individuals +- Logging in with your Google account to Gemini Code Assist for Workspace, Standard, or Enterprise Users +- Using an API key with Gemini Developer +- Using an API key with Vertex AI GenAI API -## 2. Gemini API Key (Using Gemini Developer [API](https://ai.google.dev/gemini-api/docs) a: Unpaid Service, b: Paid Service) +For each of these four methods of authentication, different Terms of Service and Privacy Notices may apply. -If you are using a Gemini API key for authentication, the following terms apply: +| Authentication | Account | Terms of Service | Privacy Notice | +| :---------------------------- | :------------------ | :------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Gemini Code Assist via Google | Individual | [Google Terms of Service](https://policies.google.com/terms?hl=en-US) | [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals) | +| Gemini Code Assist via Google | Standard/Enterprise | [Google Cloud Platform Terms of Service](https://cloud.google.com/terms) | [Gemini Code Assist Privacy Notice for Standard and Enterprise](https://cloud.google.com/gemini/docs/codeassist/security-privacy-compliance#standard_and_enterprise_data_protection_and_privacy) | +| Gemini Developer API | Unpaid | [Gemini API Terms of Service - Unpaid Services](https://ai.google.dev/gemini-api/terms#unpaid-services) | [Google Privacy Policy](https://policies.google.com/privacy) | +| Gemini Developer API | Paid | [Gemini API Terms of Service - Paid Services](https://ai.google.dev/gemini-api/terms#paid-services) | [Google Privacy Policy](https://policies.google.com/privacy) | +| Vertex AI Gen API | | [Google Cloud Platform Service Terms](https://cloud.google.com/terms/service-terms/) | [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) | -- Terms of Service: Your use is subject to the [Gemini API Terms of Service](https://ai.google.dev/gemini-api/terms). For a. [Unpaid Service](https://ai.google.dev/gemini-api/terms#unpaid-services) or b. [Paid Service](https://ai.google.dev/gemini-api/terms#paid-services) -- Privacy Notice: Information regarding data handling and privacy is detailed in the general [Google Privacy Policy](https://policies.google.com/privacy). +## 1. If you have logged in with your Google account to Gemini Code Assist for Individuals -## 3. Login with Google (for Workspace or Licensed Code Assist users) +For users who use their Google account to access [Gemini Code Assist for Individuals](https://developers.google.com/gemini-code-assist/docs/overview#supported-features-gca), these Terms of Service and Privacy Notice documents apply: -For users of Standard or Enterprise [edition](https://cloud.google.com/gemini/docs/codeassist/overview#editions-overview) of Gemini Code Assist: +- **Terms of Service:** Your use of the Gemini CLI is governed by the [Google Terms of Service](https://policies.google.com/terms?hl=en-US). +- **Privacy Notice:** The collection and use of your data is described in the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals). -- Terms of Service: The [Google Cloud Platform Terms of Service](https://cloud.google.com/terms) govern your use of the service. -- Privacy Notice: The handling of your data is outlined in the [Gemini Code Assist Privacy Notices](https://developers.google.com/gemini-code-assist/resources/privacy-notices). +## 2. If you have logged in with your Google account to Gemini Code Assist for Workspace, Standard, or Enterprise Users -## 4. Vertex AI (Using Vertex AI Gen [API](https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest)) +For users who use their Google account to access the [Standard or Enterprise edition](https://cloud.google.com/gemini/docs/codeassist/overview#editions-overview) of Gemini Code Assist, these Terms of Service and Privacy Notice documents apply: -If you are using an API key with a Vertex AI Gen API backend: +- **Terms of Service:** Your use of the Gemini CLI is governed by the [Google Cloud Platform Terms of Service](https://cloud.google.com/terms). +- **Privacy Notice:** The collection and use of your data is described in the [Gemini Code Assist Privacy Notices for Standard and Enterprise Users](https://cloud.google.com/gemini/docs/codeassist/security-privacy-compliance#standard_and_enterprise_data_protection_and_privacy). -- Terms of Service: Your usage is governed by the [Google Cloud Platform Service Terms](https://cloud.google.com/terms/service-terms/). -- Privacy Notice: The [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) describes how your data is collected and managed. +## 3. If you have logged in with a Gemini API key to the Gemini Developer API + +If you are using a Gemini API key for authentication with the [Gemini Developer API](https://ai.google.dev/gemini-api/docs), these Terms of Service and Privacy Notice documents apply: + +- **Terms of Service:** Your use of the Gemini CLI is governed by the [Gemini API Terms of Service](https://ai.google.dev/gemini-api/terms). These terms may differ depending on whether you are using an unpaid or paid service: + - For unpaid services, refer to the [Gemini API Terms of Service - Unpaid Services](https://ai.google.dev/gemini-api/terms#unpaid-services). + - For paid services, refer to the [Gemini API Terms of Service - Paid Services](https://ai.google.dev/gemini-api/terms#paid-services). +- **Privacy Notice:** The collection and use of your data is described in the [Google Privacy Policy](https://policies.google.com/privacy). + +## 4. If you have logged in with a Gemini API key to the Vertex AI GenAI API + +If you are using a Gemini API key for authentication with a [Vertex AI GenAI API](https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest) backend, these Terms of Service and Privacy Notice documents apply: + +- **Terms of Service:** Your use of the Gemini CLI is governed by the [Google Cloud Platform Service Terms](https://cloud.google.com/terms/service-terms/). +- **Privacy Notice:** The collection and use of your data is described in the [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice). ### Usage Statistics Opt-Out -You may opt-out from sending Usage Statistics to Google data by following the instructions available here: [Usage Statistics Configuration](./cli/configuration.md#usage-statistics). +You may opt-out from sending Usage Statistics to Google by following the instructions available here: [Usage Statistics Configuration](./cli/configuration.md#usage-statistics). -## Frequently Asked Questions (FAQ) for Gemini CLI +## Frequently Asked Questions (FAQ) for the Gemini CLI ### 1. Is my code, including prompts and answers, used to train Google's models? -This depends entirely on the type of auth method you use. +Whether your code, including prompts and answers, is used to train Google's models depends on the type of authentication method you use and your account type. + +- **Google account with Gemini Code Assist for Individuals**: Yes. When you use your personal Google account, the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals) applies. Under this notice, + your **prompts, answers, and related code are collected** and may be used to improve Google's products, including for model training. +- **Google account with Gemini Code Assist for Workspace, Standard, or Enterprise**: No. For these accounts, your data is governed by the [Gemini Code Assist Privacy Notices](https://cloud.google.com/gemini/docs/codeassist/security-privacy-compliance#standard_and_enterprise_data_protection_and_privacy) terms, which treat your inputs as confidential. Your **prompts, answers, and related code are not collected** and are not used to train models. +- **Gemini API key via the Gemini Developer API**: Whether your code is collected or used depends on whether you are using an unpaid or paid service. + - **Unpaid services**: Yes. When you use the Gemini API key via the Gemini Developer API with an unpaid service, the [Gemini API Terms of Service - Unpaid Services](https://ai.google.dev/gemini-api/terms#unpaid-services) terms apply. Under this notice, your **prompts, answers, and related code are collected** and may be used to improve Google's products, including for model training. + - **Paid services**: No. When you use the Gemini API key via the Gemini Developer API with a paid service, the [Gemini API Terms of Service - Paid Services](https://ai.google.dev/gemini-api/terms#paid-services) terms apply, which treats your inputs as confidential. Your **prompts, answers, and related code are not collected** and are not used to train models. +- **Gemini API key via the Vertex AI GenAI API**: No. For these accounts, your data is governed by the [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) terms, which treat your inputs as confidential. Your **prompts, answers, and related code are not collected** and are not used to train models. -- **Auth method 1:** Yes. When you use your personal Google account, the Gemini Code Assist Privacy Notice for Individuals applies. Under this notice, your **prompts, answers, and related code are collected** and may be used to improve Google's products, which includes model training. -- **Auth method 2a:** Yes. When you use the Gemini API key Gemini API (Unpaid Service) terms apply. Under this notice , your **prompts, answers, and related code are collected** and may be used to improve Google's products, which includes model training. -- **Auth method 2b, 3 & 4:** No. For these accounts, your data is governed by the Google Cloud or Gemini API (Paid Service) terms, which treat your inputs as confidential. Your code, prompts, and other inputs are **not** used to train models. +### 2. What are Usage Statistics and what does the opt-out control? -### 2. What are "Usage Statistics" and what does the opt-out control? +The **Usage Statistics** setting is the single control for all optional data collection in the Gemini CLI. -The "Usage Statistics" setting is the single control for all optional data collection in the Gemini CLI. The data it collects depends on your account type: +The data it collects depends on your account and authentication type: -- **Auth method 1:** When enabled, this setting allows Google to collect both anonymous telemetry (like commands run and performance metrics) and **your prompts and answers** for model improvement. -- **Auth method 2a:** When enabled, this setting allows Google to collect both anonymous telemetry (like commands run and performance metrics) and **your prompts and answers** for model improvement. When disabled we will use your data as described in the [How Google Uses Your Data](https://ai.google.dev/gemini-api/terms#data-use-unpaid). -- **Auth method 2b:** This setting only controls the collection of anonymous telemetry. Google logs prompts and responses for a limited period of time, solely for the purpose of detecting violations of the Prohibited Use Policy and any required legal or regulatory disclosures -- **Auth methods 3 & 4:** This setting only controls the collection of anonymous telemetry. Your prompts and answers are never collected, regardless of this setting. +- **Google account with Gemini Code Assist for Individuals**: When enabled, this setting allows Google to collect both anonymous telemetry (for example, commands run and performance metrics) and **your prompts and answers** for model improvement. +- **Google account with Gemini Code Assist for Workspace, Standard, or Enterprise**: This setting only controls the collection of anonymous telemetry. Your prompts and answers are never collected, regardless of this setting. +- **Gemini API key via the Gemini Developer API**: + **Unpaid services**: When enabled, this setting allows Google to collect both anonymous telemetry (like commands run and performance metrics) and **your prompts and answers** for model improvement. When disabled we will use your data as described in [How Google Uses Your Data](https://ai.google.dev/gemini-api/terms#data-use-unpaid). + **Paid services**: This setting only controls the collection of anonymous telemetry. Google logs prompts and responses for a limited period of time, solely for the purpose of detecting violations of the Prohibited Use Policy and any required legal or regulatory disclosures. +- **Gemini API key via the Vertex AI GenAI API:** This setting only controls the collection of anonymous telemetry. Your prompts and answers are never collected, regardless of this setting. You can disable Usage Statistics for any account type by following the instructions in the [Usage Statistics Configuration](./cli/configuration.md#usage-statistics) documentation. From 43295dd1af97f15021235ab40aa01657d22ac1b6 Mon Sep 17 00:00:00 2001 From: Bryan Morgan Date: Wed, 9 Jul 2025 13:45:03 -0400 Subject: [PATCH 7/7] fixed additional messaging --- packages/cli/src/ui/App.tsx | 39 +++++++++-- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 2 + .../cli/src/ui/utils/errorParsing.test.ts | 64 ++++--------------- packages/cli/src/ui/utils/errorParsing.ts | 6 +- packages/core/src/code_assist/server.ts | 57 ++++++++++++++++- .../core/src/utils/quotaErrorDetection.ts | 53 +++++++++++++-- 6 files changed, 153 insertions(+), 68 deletions(-) diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 009c7e49cdb..e3a5eb55bc3 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -70,6 +70,7 @@ import { UpdateNotification } from './components/UpdateNotification.js'; import { isProQuotaExceededError, isGenericQuotaExceededError, + UserTierId, } from '@google/gemini-cli-core'; import { checkForUpdates } from './utils/updateCheck.js'; import ansiEscapes from 'ansi-escapes'; @@ -253,23 +254,51 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { ): Promise => { let message: string; + // For quota errors, assume FREE tier (safe default) - only show upgrade messaging to free tier users + // TODO: Get actual user tier from config when available + const userTier = undefined; // Defaults to FREE tier behavior + const isPaidTier = + userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD; + // Check if this is a Pro quota exceeded error if (error && isProQuotaExceededError(error)) { - message = `⚡ You have reached your daily ${currentModel} quota limit. + if (isPaidTier) { + message = `⚡ You have reached your daily ${currentModel} quota limit. +⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session. +⚡ 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`; + } else { + message = `⚡ You have reached your daily ${currentModel} quota limit. ⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder 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 you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key ⚡ You can switch authentication methods by typing /auth`; + } } else if (error && isGenericQuotaExceededError(error)) { - message = `⚡ You have reached your daily quota limit. + if (isPaidTier) { + message = `⚡ You have reached your daily quota limit. +⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session. +⚡ 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`; + } else { + message = `⚡ You have reached your daily quota limit. ⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder 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 you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key ⚡ You can switch authentication methods by typing /auth`; + } } else { - // Default fallback message for other cases (like consecutive 429s) - message = `⚡ Slow response times detected. -⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.`; + if (isPaidTier) { + // Default fallback message for other cases (like consecutive 429s) + message = `⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session. +⚡ Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${currentModel} quota limit +⚡ 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`; + } else { + // Default fallback message for other cases (like consecutive 429s) + message = `⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session. +⚡ Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${currentModel} 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 you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key +⚡ You can switch authentication methods by typing /auth`; + } } // Add message to UI history diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 6f6e2e2695b..62ade50f904 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -301,6 +301,8 @@ describe('useGeminiStream', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, addHistory: vi.fn(), + setQuotaErrorOccurred: vi.fn(), + getQuotaErrorOccurred: vi.fn(() => false), } as unknown as Config; mockOnDebugMessage = vi.fn(); mockHandleSlashCommand = vi.fn().mockResolvedValue(false); diff --git a/packages/cli/src/ui/utils/errorParsing.test.ts b/packages/cli/src/ui/utils/errorParsing.test.ts index 3d228efb423..770dffad830 100644 --- a/packages/cli/src/ui/utils/errorParsing.test.ts +++ b/packages/cli/src/ui/utils/errorParsing.test.ts @@ -39,7 +39,7 @@ describe('parseAndFormatApiError', () => { ); expect(result).toContain('[API Error: Rate limit exceeded'); expect(result).toContain( - 'Slow response times detected. Switching to the gemini-2.5-flash model', + 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', ); }); @@ -55,7 +55,7 @@ describe('parseAndFormatApiError', () => { ); expect(result).toContain('[API Error: Rate limit exceeded'); expect(result).toContain( - 'Slow response times detected. Switching to the gemini-2.5-flash model', + 'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model', ); }); @@ -169,7 +169,7 @@ describe('parseAndFormatApiError', () => { ); expect(result).toContain('[API Error: Rate limit exceeded'); expect(result).toContain( - 'Slow response times detected. Switching to the gemini-2.5-flash model', + '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', @@ -262,21 +262,17 @@ describe('parseAndFormatApiError', () => { ); }); - it('should handle different Gemini version strings in Pro quota exceeded errors', () => { - const errorMessage15 = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 1.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; + 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 errorMessageBeta = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini beta-3.0 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const errorMessageExperimental = - 'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini experimental-v2 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}'; - const result15 = parseAndFormatApiError( - errorMessage15, + const result25 = parseAndFormatApiError( + errorMessage25, AuthType.LOGIN_WITH_GOOGLE, undefined, - 'gemini-1.5-pro', + 'gemini-2.5-pro', DEFAULT_GEMINI_FLASH_MODEL, ); const resultPreview = parseAndFormatApiError( @@ -286,45 +282,19 @@ describe('parseAndFormatApiError', () => { 'gemini-2.5-preview-pro', DEFAULT_GEMINI_FLASH_MODEL, ); - const resultBeta = parseAndFormatApiError( - errorMessageBeta, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-beta-3.0-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - const resultExperimental = parseAndFormatApiError( - errorMessageExperimental, - AuthType.LOGIN_WITH_GOOGLE, - undefined, - 'gemini-experimental-v2-pro', - DEFAULT_GEMINI_FLASH_MODEL, - ); - expect(result15).toContain( - 'You have reached your daily gemini-1.5-pro quota limit', + 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(resultBeta).toContain( - 'You have reached your daily gemini-beta-3.0-pro quota limit', - ); - expect(resultExperimental).toContain( - 'You have reached your daily gemini-experimental-v2-pro quota limit', - ); - expect(result15).toContain( + 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', ); - expect(resultBeta).toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); - expect(resultExperimental).toContain( - 'upgrade to a Gemini Code Assist Standard or Enterprise plan', - ); }); it('should not match non-Pro models with similar version strings', () => { @@ -339,16 +309,6 @@ describe('parseAndFormatApiError', () => { "Quota exceeded for quota metric 'Gemini 2.5-preview Flash Requests' and limit", ), ).toBe(false); - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini beta-3.0 Flash Requests' and limit", - ), - ).toBe(false); - expect( - isProQuotaExceededError( - "Quota exceeded for quota metric 'Gemini experimental-v2 Flash Requests' and limit", - ), - ).toBe(false); // Test other model types expect( diff --git a/packages/cli/src/ui/utils/errorParsing.ts b/packages/cli/src/ui/utils/errorParsing.ts index 555d5e4ef9d..5031bc0ac1f 100644 --- a/packages/cli/src/ui/utils/errorParsing.ts +++ b/packages/cli/src/ui/utils/errorParsing.ts @@ -19,7 +19,7 @@ import { const getRateLimitErrorMessageGoogleFree = ( fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, ) => - `\nSlow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; + `\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, @@ -34,7 +34,7 @@ const getRateLimitErrorMessageGoogleGenericQuotaFree = () => const getRateLimitErrorMessageGooglePaid = ( fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, ) => - `\nSlow 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.`; + `\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, @@ -53,7 +53,7 @@ const RATE_LIMIT_ERROR_MESSAGE_VERTEX = const getRateLimitErrorMessageDefault = ( fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL, ) => - `\nSlow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`; + `\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, diff --git a/packages/core/src/code_assist/server.ts b/packages/core/src/code_assist/server.ts index 06ce0341640..01fd2462cbc 100644 --- a/packages/core/src/code_assist/server.ts +++ b/packages/core/src/code_assist/server.ts @@ -31,7 +31,23 @@ import { toCountTokenRequest, toGenerateContentRequest, } from './converter.js'; -import { PassThrough } from 'node:stream'; +import { Readable } from 'node:stream'; + +interface ErrorData { + error?: { + message?: string; + }; +} + +interface GaxiosResponse { + status: number; + data: unknown; +} + +interface StreamError extends Error { + status?: number; + response?: GaxiosResponse; +} /** HTTP options to be used in each of the requests. */ export interface HttpOptions { @@ -177,8 +193,45 @@ export class CodeAssistServer implements ContentGenerator { }); return (async function* (): AsyncGenerator { + // Convert ReadableStream to Node.js stream if needed + let nodeStream: NodeJS.ReadableStream; + + if (res.data instanceof ReadableStream) { + // Convert Web ReadableStream to Node.js Readable stream + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nodeStream = Readable.fromWeb(res.data as any); + } else if ( + res.data && + typeof (res.data as NodeJS.ReadableStream).on === 'function' + ) { + // Already a Node.js stream + nodeStream = res.data as NodeJS.ReadableStream; + } else { + // If res.data is not a stream, it might be an error response + // Try to extract error information from the response + let errorMessage = + 'Response data is not a readable stream. This may indicate a server error or quota issue.'; + + if (res.data && typeof res.data === 'object') { + // Check if this is an error response with error details + const errorData = res.data as ErrorData; + if (errorData.error?.message) { + errorMessage = errorData.error.message; + } else if (typeof errorData === 'string') { + errorMessage = errorData; + } + } + + // Create an error that looks like a quota error if it contains quota information + const error: StreamError = new Error(errorMessage); + // Add status and response properties so it can be properly handled by retry logic + error.status = res.status; + error.response = res; + throw error; + } + const rl = readline.createInterface({ - input: res.data as PassThrough, + input: nodeStream, crlfDelay: Infinity, // Recognizes '\r\n' and '\n' as line breaks }); diff --git a/packages/core/src/utils/quotaErrorDetection.ts b/packages/core/src/utils/quotaErrorDetection.ts index ec77f5ee8cb..a8e87a5de28 100644 --- a/packages/core/src/utils/quotaErrorDetection.ts +++ b/packages/core/src/utils/quotaErrorDetection.ts @@ -41,14 +41,23 @@ export function isProQuotaExceededError(error: unknown): boolean { // Check for Pro quota exceeded errors by looking for the specific pattern // This will match patterns like: // - "Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'" - // - "Quota exceeded for quota metric 'Gemini 1.5-preview Pro Requests'" - // - "Quota exceeded for quota metric 'Gemini beta-3.0 Pro Requests'" - // - "Quota exceeded for quota metric 'Gemini experimental-v2 Pro Requests'" + // - "Quota exceeded for quota metric 'Gemini 2.5-preview Pro Requests'" // We use string methods instead of regex to avoid ReDoS vulnerabilities - const checkMessage = (message: string): boolean => - message.includes("Quota exceeded for quota metric 'Gemini") && - message.includes("Pro Requests'"); + const checkMessage = (message: string): boolean => { + console.log('[DEBUG] isProQuotaExceededError checking message:', message); + const result = + message.includes("Quota exceeded for quota metric 'Gemini") && + message.includes("Pro Requests'"); + console.log('[DEBUG] isProQuotaExceededError result:', result); + return result; + }; + + // Log the full error object to understand its structure + console.log( + '[DEBUG] isProQuotaExceededError - full error object:', + JSON.stringify(error, null, 2), + ); if (typeof error === 'string') { return checkMessage(error); @@ -62,6 +71,38 @@ export function isProQuotaExceededError(error: unknown): boolean { return checkMessage(error.error.message); } + // Check if it's a Gaxios error with response data + if (error && typeof error === 'object' && 'response' in error) { + const gaxiosError = error as { + response?: { + data?: unknown; + }; + }; + if (gaxiosError.response && gaxiosError.response.data) { + console.log( + '[DEBUG] isProQuotaExceededError - checking response data:', + gaxiosError.response.data, + ); + if (typeof gaxiosError.response.data === 'string') { + return checkMessage(gaxiosError.response.data); + } + if ( + typeof gaxiosError.response.data === 'object' && + gaxiosError.response.data !== null && + 'error' in gaxiosError.response.data + ) { + const errorData = gaxiosError.response.data as { + error?: { message?: string }; + }; + return checkMessage(errorData.error?.message || ''); + } + } + } + + console.log( + '[DEBUG] isProQuotaExceededError - no matching error format for:', + error, + ); return false; }