mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-14 08:01:02 -07:00
fix(cli): handle flash model errors gracefully (#12667)
This commit is contained in:
@@ -103,15 +103,6 @@ describe('useQuotaAndFallback', () => {
|
||||
return setFallbackHandlerSpy.mock.calls[0][0] as FallbackModelHandler;
|
||||
};
|
||||
|
||||
it('should return null and take no action if already in fallback mode', async () => {
|
||||
vi.spyOn(mockConfig, 'isInFallbackMode').mockReturnValue(true);
|
||||
const handler = getRegisteredHandler();
|
||||
const result = await handler('gemini-pro', 'gemini-flash', new Error());
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null and take no action if authType is not LOGIN_WITH_GOOGLE', async () => {
|
||||
// Override the default mock from beforeEach for this specific test
|
||||
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
@@ -125,6 +116,62 @@ describe('useQuotaAndFallback', () => {
|
||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Flash Model Fallback', () => {
|
||||
it('should show a terminal quota message and stop, without offering a fallback', async () => {
|
||||
const handler = getRegisteredHandler();
|
||||
const result = await handler(
|
||||
'gemini-2.5-flash',
|
||||
'gemini-2.5-flash',
|
||||
new TerminalQuotaError('flash quota', mockGoogleApiError),
|
||||
);
|
||||
|
||||
expect(result).toBe('stop');
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0]
|
||||
.text;
|
||||
expect(message).toContain(
|
||||
'You have reached your daily gemini-2.5-flash',
|
||||
);
|
||||
expect(message).not.toContain('continue with the fallback model');
|
||||
});
|
||||
|
||||
it('should show a capacity message and stop', async () => {
|
||||
const handler = getRegisteredHandler();
|
||||
// let result: FallbackIntent | null = null;
|
||||
const result = await handler(
|
||||
'gemini-2.5-flash',
|
||||
'gemini-2.5-flash',
|
||||
new Error('capacity'),
|
||||
);
|
||||
|
||||
expect(result).toBe('stop');
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0]
|
||||
.text;
|
||||
expect(message).toContain(
|
||||
'Pardon Our Congestion! It looks like gemini-2.5-flash is very popular',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show a capacity message and stop, even when already in fallback mode', async () => {
|
||||
vi.spyOn(mockConfig, 'isInFallbackMode').mockReturnValue(true);
|
||||
const handler = getRegisteredHandler();
|
||||
const result = await handler(
|
||||
'gemini-2.5-flash',
|
||||
'gemini-2.5-flash',
|
||||
new Error('capacity'),
|
||||
);
|
||||
|
||||
expect(result).toBe('stop');
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0]
|
||||
.text;
|
||||
expect(message).toContain(
|
||||
'Pardon Our Congestion! It looks like gemini-2.5-flash is very popular',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interactive Fallback', () => {
|
||||
// Pro Quota Errors
|
||||
it('should set an interactive request and wait for user choice', async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type FallbackIntent,
|
||||
TerminalQuotaError,
|
||||
UserTierId,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -41,10 +42,6 @@ export function useQuotaAndFallback({
|
||||
fallbackModel,
|
||||
error,
|
||||
): Promise<FallbackIntent | null> => {
|
||||
if (config.isInFallbackMode()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fallbacks are currently only handled for OAuth users.
|
||||
const contentGeneratorConfig = config.getContentGeneratorConfig();
|
||||
if (
|
||||
@@ -58,28 +55,35 @@ export function useQuotaAndFallback({
|
||||
const isPaidTier =
|
||||
userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
|
||||
|
||||
const isFallbackModel = failedModel === DEFAULT_GEMINI_FLASH_MODEL;
|
||||
let message: string;
|
||||
|
||||
if (error instanceof TerminalQuotaError) {
|
||||
// Pro Quota specific messages (Interactive)
|
||||
// Common part of the message for both tiers
|
||||
const messageLines = [
|
||||
`⚡ You have reached your daily ${failedModel} quota limit.`,
|
||||
`⚡ You can choose to authenticate with a paid API key${
|
||||
isFallbackModel ? '.' : ' or continue with the fallback model.'
|
||||
}`,
|
||||
];
|
||||
|
||||
// Tier-specific part
|
||||
if (isPaidTier) {
|
||||
message = [
|
||||
`⚡ You have reached your daily ${failedModel} quota limit.`,
|
||||
`⚡ You can choose to authenticate with a paid API key or continue with the fallback model.`,
|
||||
messageLines.push(
|
||||
`⚡ Increase your limits by using a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key`,
|
||||
`⚡ You can switch authentication methods by typing /auth`,
|
||||
].join('\n');
|
||||
);
|
||||
} else {
|
||||
message = [
|
||||
`⚡ You have reached your daily ${failedModel} quota limit.`,
|
||||
`⚡ You can choose to authenticate with a paid API key or continue with the fallback model.`,
|
||||
messageLines.push(
|
||||
`⚡ Increase your limits by `,
|
||||
`⚡ - signing up for a plan with higher limits at https://goo.gle/set-up-gemini-code-assist`,
|
||||
`⚡ - or using a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key`,
|
||||
`⚡ You can switch authentication methods by typing /auth`,
|
||||
].join('\n');
|
||||
);
|
||||
}
|
||||
message = messageLines.join('\n');
|
||||
} else {
|
||||
// Capacity error
|
||||
message = [
|
||||
`🚦Pardon Our Congestion! It looks like ${failedModel} is very popular at the moment.`,
|
||||
`Please retry again later.`,
|
||||
@@ -95,6 +99,10 @@ export function useQuotaAndFallback({
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
if (isFallbackModel) {
|
||||
return 'stop';
|
||||
}
|
||||
|
||||
setModelSwitchedFromQuotaError(true);
|
||||
config.setQuotaErrorOccurred(true);
|
||||
|
||||
|
||||
@@ -73,14 +73,15 @@ describe('handleFallback', () => {
|
||||
expect(mockConfig.setFallbackMode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null if the failed model is already the fallback model', async () => {
|
||||
it('should still consult the handler if the failed model is the fallback model', async () => {
|
||||
mockHandler.mockResolvedValue('stop');
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
FALLBACK_MODEL, // Failed model is Flash
|
||||
AUTH_OAUTH,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
expect(mockHandler).not.toHaveBeenCalled();
|
||||
expect(result).toBe(false);
|
||||
expect(mockHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null if no fallbackHandler is injected in config', async () => {
|
||||
|
||||
@@ -21,8 +21,6 @@ export async function handleFallback(
|
||||
|
||||
const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL;
|
||||
|
||||
if (failedModel === fallbackModel) return null;
|
||||
|
||||
// Consult UI Handler for Intent
|
||||
const fallbackModelHandler = config.fallbackModelHandler;
|
||||
if (typeof fallbackModelHandler !== 'function') return null;
|
||||
|
||||
Reference in New Issue
Block a user