mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-24 04:52:43 -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:
@@ -20,9 +20,11 @@ import { AuthType } from '../core/contentGenerator.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
} from '../config/models.js';
|
||||
import { logFlashFallback } from '../telemetry/index.js';
|
||||
import type { FallbackModelHandler } from './types.js';
|
||||
import { ModelNotFoundError } from '../utils/httpErrors.js';
|
||||
|
||||
// Mock the telemetry logger and event class
|
||||
vi.mock('../telemetry/index.js', () => ({
|
||||
@@ -39,7 +41,12 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
({
|
||||
isInFallbackMode: vi.fn(() => false),
|
||||
setFallbackMode: vi.fn(),
|
||||
isPreviewModelFallbackMode: vi.fn(() => false),
|
||||
setPreviewModelFallbackMode: vi.fn(),
|
||||
isPreviewModelBypassMode: vi.fn(() => false),
|
||||
setPreviewModelBypassMode: vi.fn(),
|
||||
fallbackHandler: undefined,
|
||||
getFallbackModelHandler: vi.fn(),
|
||||
isInteractive: vi.fn(() => false),
|
||||
...overrides,
|
||||
}) as unknown as Config;
|
||||
@@ -99,7 +106,7 @@ describe('handleFallback', () => {
|
||||
|
||||
describe('when handler returns "retry"', () => {
|
||||
it('should activate fallback mode, log telemetry, and return true', async () => {
|
||||
mockHandler.mockResolvedValue('retry');
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
@@ -152,7 +159,7 @@ describe('handleFallback', () => {
|
||||
|
||||
it('should pass the correct context (failedModel, fallbackModel, error) to the handler', async () => {
|
||||
const mockError = new Error('Quota Exceeded');
|
||||
mockHandler.mockResolvedValue('retry');
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
await handleFallback(mockConfig, MOCK_PRO_MODEL, AUTH_OAUTH, mockError);
|
||||
|
||||
@@ -171,7 +178,7 @@ describe('handleFallback', () => {
|
||||
setFallbackMode: vi.fn(),
|
||||
});
|
||||
|
||||
mockHandler.mockResolvedValue('retry');
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
const result = await handleFallback(
|
||||
activeFallbackConfig,
|
||||
@@ -201,4 +208,107 @@ describe('handleFallback', () => {
|
||||
);
|
||||
expect(mockConfig.setFallbackMode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Preview Model Fallback Logic', () => {
|
||||
const previewModel = PREVIEW_GEMINI_MODEL;
|
||||
|
||||
it('should always set Preview Model bypass mode on failure', async () => {
|
||||
await handleFallback(mockConfig, previewModel, AUTH_OAUTH);
|
||||
expect(mockConfig.setPreviewModelBypassMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should silently retry if Preview Model fallback mode is already active', async () => {
|
||||
vi.spyOn(mockConfig, 'isPreviewModelFallbackMode').mockReturnValue(true);
|
||||
|
||||
const result = await handleFallback(mockConfig, previewModel, AUTH_OAUTH);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should activate Preview Model fallback mode when handler returns "retry_always"', async () => {
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
const result = await handleFallback(mockConfig, previewModel, AUTH_OAUTH);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockConfig.setPreviewModelBypassMode).toHaveBeenCalledWith(true);
|
||||
expect(mockConfig.setPreviewModelFallbackMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should NOT set fallback mode if user chooses "retry_once"', async () => {
|
||||
mockHandler.mockResolvedValue('retry_once');
|
||||
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
new Error('Capacity'),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockConfig.setPreviewModelBypassMode).toHaveBeenCalledWith(true);
|
||||
expect(mockConfig.setPreviewModelFallbackMode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set fallback mode if user chooses "retry_always"', async () => {
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
new Error('Capacity'),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockConfig.setPreviewModelBypassMode).toHaveBeenCalledWith(true);
|
||||
expect(mockConfig.setPreviewModelFallbackMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
it('should pass DEFAULT_GEMINI_MODEL as fallback when Preview Model fails', async () => {
|
||||
const mockFallbackHandler = vi.fn().mockResolvedValue('stop');
|
||||
vi.mocked(mockConfig.fallbackModelHandler!).mockImplementation(
|
||||
mockFallbackHandler,
|
||||
);
|
||||
|
||||
await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
|
||||
expect(mockConfig.fallbackModelHandler).toHaveBeenCalledWith(
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if ModelNotFoundError occurs for a non-preview model', async () => {
|
||||
const modelNotFoundError = new ModelNotFoundError('Not found');
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
DEFAULT_GEMINI_MODEL, // Not preview model
|
||||
AUTH_OAUTH,
|
||||
modelNotFoundError,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
expect(mockHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should consult handler if ModelNotFoundError occurs for preview model', async () => {
|
||||
const modelNotFoundError = new ModelNotFoundError('Not found');
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
AUTH_OAUTH,
|
||||
modelNotFoundError,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,19 @@
|
||||
|
||||
import type { Config } from '../config/config.js';
|
||||
import { AuthType } from '../core/contentGenerator.js';
|
||||
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
} from '../config/models.js';
|
||||
import { logFlashFallback, FlashFallbackEvent } from '../telemetry/index.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
import { openBrowserSecurely } from '../utils/secure-browser-launcher.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { getErrorMessage } from '../utils/errors.js';
|
||||
import { ModelNotFoundError } from '../utils/httpErrors.js';
|
||||
|
||||
const UPGRADE_URL_PAGE = 'https://goo.gle/set-up-gemini-code-assist';
|
||||
|
||||
export async function handleFallback(
|
||||
config: Config,
|
||||
@@ -19,7 +29,31 @@ export async function handleFallback(
|
||||
// Applicability Checks
|
||||
if (authType !== AuthType.LOGIN_WITH_GOOGLE) return null;
|
||||
|
||||
const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL;
|
||||
// Guardrail: If it's a ModelNotFoundError but NOT the preview model, do not handle it.
|
||||
if (
|
||||
error instanceof ModelNotFoundError &&
|
||||
failedModel !== PREVIEW_GEMINI_MODEL
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Preview Model Specific Logic
|
||||
if (failedModel === PREVIEW_GEMINI_MODEL) {
|
||||
// Always set bypass mode for the immediate retry.
|
||||
// This ensures the next attempt uses 2.5 Pro.
|
||||
config.setPreviewModelBypassMode(true);
|
||||
|
||||
// If we are already in Preview Model fallback mode (user previously said "Always"),
|
||||
// we silently retry (which will use 2.5 Pro due to bypass mode).
|
||||
if (config.isPreviewModelFallbackMode()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackModel =
|
||||
failedModel === PREVIEW_GEMINI_MODEL
|
||||
? DEFAULT_GEMINI_MODEL
|
||||
: DEFAULT_GEMINI_FLASH_MODEL;
|
||||
|
||||
// Consult UI Handler for Intent
|
||||
const fallbackModelHandler = config.fallbackModelHandler;
|
||||
@@ -35,11 +69,18 @@ export async function handleFallback(
|
||||
|
||||
// Process Intent and Update State
|
||||
switch (intent) {
|
||||
case 'retry':
|
||||
// Activate fallback mode. The NEXT retry attempt will pick this up.
|
||||
activateFallbackMode(config, authType);
|
||||
case 'retry_always':
|
||||
if (failedModel === PREVIEW_GEMINI_MODEL) {
|
||||
activatePreviewModelFallbackMode(config);
|
||||
} else {
|
||||
activateFallbackMode(config, authType);
|
||||
}
|
||||
return true; // Signal retryWithBackoff to continue.
|
||||
|
||||
case 'retry_once':
|
||||
// Just retry this time, do NOT set sticky fallback mode.
|
||||
return true;
|
||||
|
||||
case 'stop':
|
||||
activateFallbackMode(config, authType);
|
||||
return false;
|
||||
@@ -47,6 +88,10 @@ export async function handleFallback(
|
||||
case 'retry_later':
|
||||
return false;
|
||||
|
||||
case 'upgrade':
|
||||
await handleUpgrade();
|
||||
return false;
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Unexpected fallback intent received from fallbackModelHandler: "${intent}"`,
|
||||
@@ -58,6 +103,17 @@ export async function handleFallback(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpgrade() {
|
||||
try {
|
||||
await openBrowserSecurely(UPGRADE_URL_PAGE);
|
||||
} catch (error) {
|
||||
debugLogger.warn(
|
||||
'Failed to open browser automatically:',
|
||||
getErrorMessage(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function activateFallbackMode(config: Config, authType: string | undefined) {
|
||||
if (!config.isInFallbackMode()) {
|
||||
config.setFallbackMode(true);
|
||||
@@ -67,3 +123,10 @@ function activateFallbackMode(config: Config, authType: string | undefined) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activatePreviewModelFallbackMode(config: Config) {
|
||||
if (!config.isPreviewModelFallbackMode()) {
|
||||
config.setPreviewModelFallbackMode(true);
|
||||
// We might want a specific event for Preview Model fallback, but for now we just set the mode.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
* Defines the intent returned by the UI layer during a fallback scenario.
|
||||
*/
|
||||
export type FallbackIntent =
|
||||
| 'retry' // Immediately retry the current request with the fallback model.
|
||||
| 'retry_always' // Retry with fallback model and stick to it for future requests.
|
||||
| 'retry_once' // Retry with fallback model for this request only.
|
||||
| 'stop' // Switch to fallback for future requests, but stop the current request.
|
||||
| 'retry_later'; // Stop the current request and do not fallback. Intend to try again later with the same model.
|
||||
| 'retry_later' // Stop the current request and do not fallback. Intend to try again later with the same model.
|
||||
| 'upgrade'; // Give user an option to upgrade the tier.
|
||||
|
||||
/**
|
||||
* The interface for the handler provided by the UI layer (e.g., the CLI)
|
||||
|
||||
Reference in New Issue
Block a user