mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-14 05:17:18 -07:00
feat: launch Gemini 3 in Gemini CLI 🚀🚀🚀 (in main) (#13287)
Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Co-authored-by: Sehoon Shon <sshon@google.com> Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com> Co-authored-by: Sandy Tao <sandytao520@icloud.com> Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com> Co-authored-by: Aishanee Shah <aishaneeshah@gmail.com> Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com> Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> Co-authored-by: Jacob Richman <jacob314@gmail.com> Co-authored-by: joshualitt <joshualitt@google.com> Co-authored-by: Jenna Inouye <jinouye@google.com>
This commit is contained in:
@@ -201,6 +201,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
},
|
||||
new Map(), // extensionsUpdateState
|
||||
true, // isConfigInitialized
|
||||
vi.fn(), // setBannerVisible
|
||||
vi.fn(), // setCustomDialog
|
||||
),
|
||||
);
|
||||
|
||||
@@ -77,6 +77,7 @@ export const useSlashCommandProcessor = (
|
||||
actions: SlashCommandProcessorActions,
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateStatus>,
|
||||
isConfigInitialized: boolean,
|
||||
setBannerVisible: (visible: boolean) => void,
|
||||
setCustomDialog: (dialog: React.ReactNode | null) => void,
|
||||
) => {
|
||||
const session = useSessionStats();
|
||||
@@ -203,6 +204,7 @@ export const useSlashCommandProcessor = (
|
||||
console.clear();
|
||||
}
|
||||
refreshStatic();
|
||||
setBannerVisible(false);
|
||||
},
|
||||
loadHistory,
|
||||
setDebugMessage: actions.setDebugMessage,
|
||||
@@ -241,6 +243,7 @@ export const useSlashCommandProcessor = (
|
||||
sessionShellAllowlist,
|
||||
reloadCommands,
|
||||
extensionsUpdateState,
|
||||
setBannerVisible,
|
||||
setCustomDialog,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('useEditorSettings', () => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Editor preference set to "vscode" in User settings.',
|
||||
text: 'Editor preference set to "VS Code" in User settings.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
@@ -164,6 +164,11 @@ describe('useEditorSettings', () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
const editorTypes: EditorType[] = ['cursor', 'windsurf', 'vim'];
|
||||
const displayNames: Record<string, string> = {
|
||||
cursor: 'Cursor',
|
||||
windsurf: 'Windsurf',
|
||||
vim: 'Vim',
|
||||
};
|
||||
const scope = SettingScope.User;
|
||||
|
||||
editorTypes.forEach((editorType) => {
|
||||
@@ -180,7 +185,7 @@ describe('useEditorSettings', () => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Editor preference set to "${editorType}" in User settings.`,
|
||||
text: `Editor preference set to "${displayNames[editorType]}" in User settings.`,
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
@@ -210,7 +215,7 @@ describe('useEditorSettings', () => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Editor preference set to "vscode" in ${scope} settings.`,
|
||||
text: `Editor preference set to "VS Code" in ${scope} settings.`,
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { EditorType } from '@google/gemini-cli-core';
|
||||
import {
|
||||
allowEditorTypeInSandbox,
|
||||
checkHasEditorType,
|
||||
getEditorDisplayName,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { SettingPaths } from '../../config/settingPaths.js';
|
||||
@@ -58,7 +59,7 @@ export const useEditorSettings = (
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Editor preference ${editorType ? `set to "${editorType}"` : 'cleared'} in ${scope} settings.`,
|
||||
text: `Editor preference ${editorType ? `set to "${getEditorDisplayName(editorType)}"` : 'cleared'} in ${scope} settings.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
makeFakeConfig,
|
||||
type GoogleApiError,
|
||||
RetryableQuotaError,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
ModelNotFoundError,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useQuotaAndFallback } from './useQuotaAndFallback.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -87,18 +89,14 @@ describe('useQuotaAndFallback', () => {
|
||||
|
||||
describe('Fallback Handler Logic', () => {
|
||||
// Helper function to render the hook and extract the registered handler
|
||||
const getRegisteredHandler = (
|
||||
userTier: UserTierId = UserTierId.FREE,
|
||||
): FallbackModelHandler => {
|
||||
renderHook(
|
||||
(props) =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: props.userTier,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
}),
|
||||
{ initialProps: { userTier } },
|
||||
const getRegisteredHandler = (): FallbackModelHandler => {
|
||||
renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
}),
|
||||
);
|
||||
return setFallbackHandlerSpy.mock.calls[0][0] as FallbackModelHandler;
|
||||
};
|
||||
@@ -116,65 +114,8 @@ 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 () => {
|
||||
it('should set an interactive request for a terminal quota error', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
@@ -187,31 +128,42 @@ describe('useQuotaAndFallback', () => {
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
// Call the handler but do not await it, to check the intermediate state
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
const error = new TerminalQuotaError(
|
||||
'pro quota',
|
||||
mockGoogleApiError,
|
||||
1000 * 60 * 5,
|
||||
); // 5 minutes
|
||||
await act(() => {
|
||||
promise = handler(
|
||||
'gemini-pro',
|
||||
'gemini-flash',
|
||||
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
||||
);
|
||||
promise = handler('gemini-pro', 'gemini-flash', error);
|
||||
});
|
||||
|
||||
// The hook should now have a pending request for the UI to handle
|
||||
expect(result.current.proQuotaRequest).not.toBeNull();
|
||||
expect(result.current.proQuotaRequest?.failedModel).toBe('gemini-pro');
|
||||
const request = result.current.proQuotaRequest;
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.failedModel).toBe('gemini-pro');
|
||||
expect(request?.isTerminalQuotaError).toBe(true);
|
||||
|
||||
const message = request!.message;
|
||||
expect(message).toContain('Usage limit reached for gemini-pro.');
|
||||
expect(message).toContain('Access resets at'); // From getResetTimeMessage
|
||||
expect(message).toContain('/stats for usage details');
|
||||
expect(message).toContain('/auth to switch to API key.');
|
||||
|
||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||
|
||||
// Simulate the user choosing to continue with the fallback model
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('retry');
|
||||
result.current.handleProQuotaChoice('retry_always');
|
||||
});
|
||||
|
||||
// The original promise from the handler should now resolve
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry');
|
||||
expect(intent).toBe('retry_always');
|
||||
|
||||
// The pending request should be cleared from the state
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle race conditions by stopping subsequent requests', async () => {
|
||||
@@ -253,120 +205,129 @@ describe('useQuotaAndFallback', () => {
|
||||
expect(result.current.proQuotaRequest).toBe(firstRequest);
|
||||
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('retry');
|
||||
result.current.handleProQuotaChoice('retry_always');
|
||||
});
|
||||
|
||||
const intent1 = await promise1!;
|
||||
expect(intent1).toBe('retry');
|
||||
expect(intent1).toBe('retry_always');
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
});
|
||||
|
||||
// Non-Quota error test cases
|
||||
// Non-TerminalQuotaError test cases
|
||||
const testCases = [
|
||||
{
|
||||
description: 'other error for FREE tier',
|
||||
tier: UserTierId.FREE,
|
||||
description: 'generic error',
|
||||
error: new Error('some error'),
|
||||
expectedMessageSnippets: [
|
||||
'🚦Pardon Our Congestion! It looks like model-A is very popular at the moment.',
|
||||
'Please retry again later.',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'other error for LEGACY tier',
|
||||
tier: UserTierId.LEGACY, // Paid tier
|
||||
error: new Error('some error'),
|
||||
expectedMessageSnippets: [
|
||||
'🚦Pardon Our Congestion! It looks like model-A is very popular at the moment.',
|
||||
'Please retry again later.',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'retryable quota error for FREE tier',
|
||||
tier: UserTierId.FREE,
|
||||
description: 'retryable quota error',
|
||||
error: new RetryableQuotaError(
|
||||
'retryable quota',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
),
|
||||
expectedMessageSnippets: [
|
||||
'🚦Pardon Our Congestion! It looks like model-A is very popular at the moment.',
|
||||
'Please retry again later.',
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'retryable quota error for LEGACY tier',
|
||||
tier: UserTierId.LEGACY, // Paid tier
|
||||
error: new RetryableQuotaError(
|
||||
'retryable quota',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
),
|
||||
expectedMessageSnippets: [
|
||||
'🚦Pardon Our Congestion! It looks like model-A is very popular at the moment.',
|
||||
'Please retry again later.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const {
|
||||
description,
|
||||
tier,
|
||||
error,
|
||||
expectedMessageSnippets,
|
||||
} of testCases) {
|
||||
for (const { description, error } of testCases) {
|
||||
it(`should handle ${description} correctly`, async () => {
|
||||
const { result } = renderHook(
|
||||
(props) =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: props.tier,
|
||||
setModelSwitchedFromQuotaError:
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
}),
|
||||
{ initialProps: { tier } },
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError:
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
// Call the handler but do not await it, to check the intermediate state
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
await act(() => {
|
||||
promise = handler('model-A', 'model-B', error);
|
||||
});
|
||||
|
||||
// The hook should now have a pending request for the UI to handle
|
||||
expect(result.current.proQuotaRequest).not.toBeNull();
|
||||
expect(result.current.proQuotaRequest?.failedModel).toBe('model-A');
|
||||
const request = result.current.proQuotaRequest;
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.failedModel).toBe('model-A');
|
||||
expect(request?.isTerminalQuotaError).toBe(false);
|
||||
|
||||
// Check that the correct initial message was added
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: MessageType.INFO }),
|
||||
expect.any(Number),
|
||||
// Check that the correct initial message was generated
|
||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||
const message = request!.message;
|
||||
expect(message).toContain(
|
||||
'model-A is currently experiencing high demand. We apologize and appreciate your patience.',
|
||||
);
|
||||
const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0]
|
||||
.text;
|
||||
for (const snippet of expectedMessageSnippets) {
|
||||
expect(message).toContain(snippet);
|
||||
}
|
||||
|
||||
// Simulate the user choosing to continue with the fallback model
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('retry');
|
||||
result.current.handleProQuotaChoice('retry_always');
|
||||
});
|
||||
|
||||
expect(mockSetModelSwitchedFromQuotaError).toHaveBeenCalledWith(true);
|
||||
// The original promise from the handler should now resolve
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry');
|
||||
expect(intent).toBe('retry_always');
|
||||
|
||||
// The pending request should be cleared from the state
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
expect(mockConfig.setQuotaErrorOccurred).toHaveBeenCalledWith(true);
|
||||
|
||||
// Check for the "Switched to fallback model" message
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock
|
||||
.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain('Switched to fallback model.');
|
||||
});
|
||||
}
|
||||
|
||||
it('should handle ModelNotFoundError correctly', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
const error = new ModelNotFoundError('model not found', 404);
|
||||
|
||||
await act(() => {
|
||||
promise = handler('gemini-3-pro-preview', 'gemini-2.5-pro', error);
|
||||
});
|
||||
|
||||
// The hook should now have a pending request for the UI to handle
|
||||
const request = result.current.proQuotaRequest;
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.failedModel).toBe('gemini-3-pro-preview');
|
||||
expect(request?.isTerminalQuotaError).toBe(false);
|
||||
expect(request?.isModelNotFoundError).toBe(true);
|
||||
|
||||
const message = request!.message;
|
||||
expect(message).toBe(
|
||||
`It seems like you don't have access to Gemini 3.
|
||||
Learn more at https://goo.gle/enable-preview-features
|
||||
To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
);
|
||||
|
||||
// Simulate the user choosing to switch
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('retry_always');
|
||||
});
|
||||
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry_always');
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -418,7 +379,7 @@ describe('useQuotaAndFallback', () => {
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
});
|
||||
|
||||
it('should resolve intent to "retry" and add info message on continue', async () => {
|
||||
it('should resolve intent to "retry_always" and add info message on continue', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
@@ -430,7 +391,7 @@ describe('useQuotaAndFallback', () => {
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
// The first `addItem` call is for the initial quota error message
|
||||
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
await act(() => {
|
||||
promise = handler(
|
||||
@@ -441,18 +402,53 @@ describe('useQuotaAndFallback', () => {
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('retry');
|
||||
result.current.handleProQuotaChoice('retry_always');
|
||||
});
|
||||
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry');
|
||||
expect(intent).toBe('retry_always');
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
|
||||
// Check for the second "Switched to fallback model" message
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(2);
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[1][0];
|
||||
// Check for the "Switched to fallback model" message
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain('Switched to fallback model.');
|
||||
});
|
||||
|
||||
it('should show a special message when falling back from the preview model', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
await act(() => {
|
||||
promise = handler(
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
'gemini-flash',
|
||||
new Error('preview model failed'),
|
||||
);
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('retry_always');
|
||||
});
|
||||
|
||||
await promise!;
|
||||
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain(
|
||||
`Switched to fallback model gemini-flash. We will periodically check if ${PREVIEW_GEMINI_MODEL} is available again.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
type FallbackModelHandler,
|
||||
type FallbackIntent,
|
||||
TerminalQuotaError,
|
||||
UserTierId,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
ModelNotFoundError,
|
||||
type UserTierId,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -51,56 +52,29 @@ export function useQuotaAndFallback({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use actual user tier if available; otherwise, default to FREE tier behavior (safe default)
|
||||
const isPaidTier =
|
||||
userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
|
||||
|
||||
const isFallbackModel = failedModel === DEFAULT_GEMINI_FLASH_MODEL;
|
||||
let message: string;
|
||||
|
||||
let isTerminalQuotaError = false;
|
||||
let isModelNotFoundError = false;
|
||||
if (error instanceof TerminalQuotaError) {
|
||||
isTerminalQuotaError = true;
|
||||
// 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.'
|
||||
}`,
|
||||
`Usage limit reached for ${failedModel}.`,
|
||||
error.retryDelayMs ? getResetTimeMessage(error.retryDelayMs) : null,
|
||||
`/stats for usage details`,
|
||||
`/auth to switch to API key.`,
|
||||
].filter(Boolean);
|
||||
message = messageLines.join('\n');
|
||||
} else if (error instanceof ModelNotFoundError) {
|
||||
isModelNotFoundError = true;
|
||||
const messageLines = [
|
||||
`It seems like you don't have access to Gemini 3.`,
|
||||
`Learn more at https://goo.gle/enable-preview-features`,
|
||||
`To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
];
|
||||
|
||||
// Tier-specific part
|
||||
if (isPaidTier) {
|
||||
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`,
|
||||
);
|
||||
} else {
|
||||
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`,
|
||||
);
|
||||
}
|
||||
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.`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
// Add message to UI history
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: message,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
if (isFallbackModel) {
|
||||
return 'stop';
|
||||
message = `${failedModel} is currently experiencing high demand. We apologize and appreciate your patience.`;
|
||||
}
|
||||
|
||||
setModelSwitchedFromQuotaError(true);
|
||||
@@ -117,6 +91,9 @@ export function useQuotaAndFallback({
|
||||
failedModel,
|
||||
fallbackModel,
|
||||
resolve,
|
||||
message,
|
||||
isTerminalQuotaError,
|
||||
isModelNotFoundError,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -136,14 +113,25 @@ export function useQuotaAndFallback({
|
||||
setProQuotaRequest(null);
|
||||
isDialogPending.current = false; // Reset the flag here
|
||||
|
||||
if (choice === 'retry') {
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Switched to fallback model. Tip: Press Ctrl+P (or Up Arrow) to recall your previous prompt and submit it again if you wish.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
if (choice === 'retry_always') {
|
||||
// If we were recovering from a Preview Model failure, show a specific message.
|
||||
if (proQuotaRequest.failedModel === PREVIEW_GEMINI_MODEL) {
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Switched to fallback model ${proQuotaRequest.fallbackModel}. ${!proQuotaRequest.isModelNotFoundError ? `We will periodically check if ${PREVIEW_GEMINI_MODEL} is available again.` : ''}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} else {
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Switched to fallback model.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[proQuotaRequest, historyManager],
|
||||
@@ -154,3 +142,15 @@ export function useQuotaAndFallback({
|
||||
handleProQuotaChoice,
|
||||
};
|
||||
}
|
||||
|
||||
function getResetTimeMessage(delayMs: number): string {
|
||||
const resetDate = new Date(Date.now() + delayMs);
|
||||
|
||||
const timeFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
|
||||
return `Access resets at ${timeFormatter.format(resetDate)}.`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user