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:
Shreya Keshive
2025-11-18 12:01:16 -05:00
committed by GitHub
parent 78075c8a37
commit 86828bb561
79 changed files with 3148 additions and 605 deletions
+94
View File
@@ -160,11 +160,16 @@ vi.mock('../utils/fetch.js', () => ({
import { BaseLlmClient } from '../core/baseLlmClient.js';
import { tokenLimit } from '../core/tokenLimits.js';
import { uiTelemetryService } from '../telemetry/index.js';
import { getCodeAssistServer } from '../code_assist/codeAssist.js';
import { getExperiments } from '../code_assist/experiments/experiments.js';
import type { CodeAssistServer } from '../code_assist/server.js';
vi.mock('../core/baseLlmClient.js');
vi.mock('../core/tokenLimits.js', () => ({
tokenLimit: vi.fn(),
}));
vi.mock('../code_assist/codeAssist.js');
vi.mock('../code_assist/experiments/experiments.js');
describe('Server Config (config.ts)', () => {
const MODEL = 'gemini-pro';
@@ -362,6 +367,23 @@ describe('Server Config (config.ts)', () => {
).toHaveBeenCalledWith();
});
it('should strip thoughts when switching from GenAI to Vertex AI', async () => {
const config = new Config(baseParams);
vi.mocked(createContentGeneratorConfig).mockImplementation(
async (_: Config, authType: AuthType | undefined) =>
({ authType }) as unknown as ContentGeneratorConfig,
);
await config.refreshAuth(AuthType.USE_GEMINI);
await config.refreshAuth(AuthType.USE_VERTEX_AI);
expect(
config.getGeminiClient().stripThoughtsFromHistory,
).toHaveBeenCalledWith();
});
it('should not strip thoughts when switching from Vertex to GenAI', async () => {
const config = new Config(baseParams);
@@ -380,6 +402,78 @@ describe('Server Config (config.ts)', () => {
});
});
describe('Preview Features Logic in refreshAuth', () => {
beforeEach(() => {
// Set up default mock behavior for these functions before each test
vi.mocked(getCodeAssistServer).mockReturnValue(undefined);
vi.mocked(getExperiments).mockResolvedValue({
flags: {},
experimentIds: [],
});
});
it('should enable preview features for Google auth when remote flag is true', async () => {
// Override the default mock for this specific test
vi.mocked(getCodeAssistServer).mockReturnValue({} as CodeAssistServer); // Simulate Google auth by returning a truthy value
vi.mocked(getExperiments).mockResolvedValue({
flags: {
[ExperimentFlags.ENABLE_PREVIEW]: { boolValue: true },
},
experimentIds: [],
});
const config = new Config({ ...baseParams, previewFeatures: undefined });
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
expect(config.getPreviewFeatures()).toBe(true);
});
it('should disable preview features for Google auth when remote flag is false', async () => {
// Override the default mock
vi.mocked(getCodeAssistServer).mockReturnValue({} as CodeAssistServer);
vi.mocked(getExperiments).mockResolvedValue({
flags: {
[ExperimentFlags.ENABLE_PREVIEW]: { boolValue: false },
},
experimentIds: [],
});
const config = new Config({ ...baseParams, previewFeatures: undefined });
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
expect(config.getPreviewFeatures()).toBe(undefined);
});
it('should disable preview features for Google auth when remote flag is missing', async () => {
// Override the default mock for getCodeAssistServer, the getExperiments mock is already correct
vi.mocked(getCodeAssistServer).mockReturnValue({} as CodeAssistServer);
const config = new Config({ ...baseParams, previewFeatures: undefined });
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
expect(config.getPreviewFeatures()).toBe(undefined);
});
it('should not change preview features or model if it is already set to true', async () => {
const initialModel = 'some-other-model';
const config = new Config({
...baseParams,
previewFeatures: true,
model: initialModel,
});
// It doesn't matter which auth method we use here, the logic should exit early
await config.refreshAuth(AuthType.USE_GEMINI);
expect(config.getPreviewFeatures()).toBe(true);
expect(config.getModel()).toBe(initialModel);
});
it('should not change preview features or model if it is already set to false', async () => {
const initialModel = 'some-other-model';
const config = new Config({
...baseParams,
previewFeatures: false,
model: initialModel,
});
await config.refreshAuth(AuthType.USE_GEMINI);
expect(config.getPreviewFeatures()).toBe(false);
expect(config.getModel()).toBe(initialModel);
});
});
it('Config constructor should store userMemory correctly', () => {
const config = new Config(baseParams);
+62 -1
View File
@@ -305,6 +305,7 @@ export interface ConfigParameters {
hooks?: {
[K in HookEventName]?: HookDefinition[];
};
previewFeatures?: boolean;
}
export class Config {
@@ -357,6 +358,7 @@ export class Config {
private readonly cwd: string;
private readonly bugCommand: BugCommandSettings | undefined;
private model: string;
private previewFeatures: boolean | undefined;
private readonly noBrowser: boolean;
private readonly folderTrust: boolean;
private ideMode: boolean;
@@ -419,6 +421,9 @@ export class Config {
private experiments: Experiments | undefined;
private experimentsPromise: Promise<void> | undefined;
private previewModelFallbackMode = false;
private previewModelBypassMode = false;
constructor(params: ConfigParameters) {
this.sessionId = params.sessionId;
this.embeddingModel =
@@ -475,6 +480,7 @@ export class Config {
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
this.bugCommand = params.bugCommand;
this.model = params.model;
this.previewFeatures = params.previewFeatures ?? undefined;
this.maxSessionTurns = params.maxSessionTurns ?? -1;
this.experimentalZedIntegration =
params.experimentalZedIntegration ?? false;
@@ -649,7 +655,7 @@ export class Config {
// thoughtSignature from Genai to Vertex will fail, we need to strip them
if (
this.contentGeneratorConfig?.authType === AuthType.USE_GEMINI &&
authMethod === AuthType.LOGIN_WITH_GOOGLE
authMethod !== AuthType.USE_GEMINI
) {
// Restore the conversation history to the new client
this.geminiClient.stripThoughtsFromHistory();
@@ -670,11 +676,22 @@ export class Config {
// Initialize BaseLlmClient now that the ContentGenerator is available
this.baseLlmClient = new BaseLlmClient(this.contentGenerator, this);
const previewFeatures = this.getPreviewFeatures();
const codeAssistServer = getCodeAssistServer(this);
if (codeAssistServer) {
this.experimentsPromise = getExperiments(codeAssistServer)
.then((experiments) => {
this.setExperiments(experiments);
// If preview features have not been set and the user authenticated through Google, we enable preview based on remote config only if it's true
if (previewFeatures === undefined) {
const remotePreviewFeatures =
experiments.flags[ExperimentFlags.ENABLE_PREVIEW]?.boolValue;
if (remotePreviewFeatures === true) {
this.setPreviewFeatures(remotePreviewFeatures);
}
}
})
.catch((e) => {
debugLogger.error('Failed to fetch experiments', e);
@@ -760,6 +777,26 @@ export class Config {
this.fallbackModelHandler = handler;
}
getFallbackModelHandler(): FallbackModelHandler | undefined {
return this.fallbackModelHandler;
}
isPreviewModelFallbackMode(): boolean {
return this.previewModelFallbackMode;
}
setPreviewModelFallbackMode(active: boolean): void {
this.previewModelFallbackMode = active;
}
isPreviewModelBypassMode(): boolean {
return this.previewModelBypassMode;
}
setPreviewModelBypassMode(active: boolean): void {
this.previewModelBypassMode = active;
}
getMaxSessionTurns(): number {
return this.maxSessionTurns;
}
@@ -822,6 +859,14 @@ export class Config {
return this.question;
}
getPreviewFeatures(): boolean | undefined {
return this.previewFeatures;
}
setPreviewFeatures(previewFeatures: boolean) {
this.previewFeatures = previewFeatures;
}
getCoreTools(): string[] | undefined {
return this.coreTools;
}
@@ -1169,6 +1214,22 @@ export class Config {
return this.experiments?.flags[ExperimentFlags.USER_CACHING]?.boolValue;
}
async getBannerTextNoCapacityIssues(): Promise<string> {
await this.ensureExperimentsLoaded();
return (
this.experiments?.flags[ExperimentFlags.BANNER_TEXT_NO_CAPACITY_ISSUES]
?.stringValue ?? ''
);
}
async getBannerTextCapacityIssues(): Promise<string> {
await this.ensureExperimentsLoaded();
return (
this.experiments?.flags[ExperimentFlags.BANNER_TEXT_CAPACITY_ISSUES]
?.stringValue ?? ''
);
}
private async ensureExperimentsLoaded(): Promise<void> {
if (!this.experimentsPromise) {
return;
+148 -5
View File
@@ -8,8 +8,12 @@ import { describe, it, expect } from 'vitest';
import {
getEffectiveModel,
DEFAULT_GEMINI_MODEL,
PREVIEW_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
GEMINI_MODEL_ALIAS_PRO,
GEMINI_MODEL_ALIAS_FLASH,
GEMINI_MODEL_ALIAS_FLASH_LITE,
} from './models.js';
describe('getEffectiveModel', () => {
@@ -17,7 +21,11 @@ describe('getEffectiveModel', () => {
const isInFallbackMode = false;
it('should return the Pro model when Pro is requested', () => {
const model = getEffectiveModel(isInFallbackMode, DEFAULT_GEMINI_MODEL);
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_MODEL,
false,
);
expect(model).toBe(DEFAULT_GEMINI_MODEL);
});
@@ -25,6 +33,7 @@ describe('getEffectiveModel', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_FLASH_MODEL,
false,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
@@ -33,22 +42,92 @@ describe('getEffectiveModel', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
false,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
});
it('should return a custom model name when requested', () => {
const customModel = 'custom-model-v1';
const model = getEffectiveModel(isInFallbackMode, customModel);
const model = getEffectiveModel(isInFallbackMode, customModel, false);
expect(model).toBe(customModel);
});
describe('with preview features', () => {
it('should return the preview model when pro alias is requested', () => {
const model = getEffectiveModel(
isInFallbackMode,
GEMINI_MODEL_ALIAS_PRO,
true,
);
expect(model).toBe(PREVIEW_GEMINI_MODEL);
});
it('should return the default pro model when pro alias is requested and preview is off', () => {
const model = getEffectiveModel(
isInFallbackMode,
GEMINI_MODEL_ALIAS_PRO,
false,
);
expect(model).toBe(DEFAULT_GEMINI_MODEL);
});
it('should return the flash model when flash is requested and preview is on', () => {
const model = getEffectiveModel(
isInFallbackMode,
GEMINI_MODEL_ALIAS_FLASH,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
it('should return the flash model when lite is requested and preview is on', () => {
const model = getEffectiveModel(
isInFallbackMode,
GEMINI_MODEL_ALIAS_FLASH_LITE,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
});
it('should return the flash model when the flash model name is explicitly requested and preview is on', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_FLASH_MODEL,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
it('should return the lite model when the lite model name is requested and preview is on', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
});
it('should return the default gemini model when the model is explicitly set and preview is on', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_MODEL,
true,
);
expect(model).toBe(DEFAULT_GEMINI_MODEL);
});
});
});
describe('When IN fallback mode', () => {
const isInFallbackMode = true;
it('should downgrade the Pro model to the Flash model', () => {
const model = getEffectiveModel(isInFallbackMode, DEFAULT_GEMINI_MODEL);
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_MODEL,
false,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
@@ -56,6 +135,7 @@ describe('getEffectiveModel', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_FLASH_MODEL,
false,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
@@ -64,20 +144,83 @@ describe('getEffectiveModel', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
false,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
});
it('should HONOR any model with "lite" in its name', () => {
const customLiteModel = 'gemini-2.5-custom-lite-vNext';
const model = getEffectiveModel(isInFallbackMode, customLiteModel);
const model = getEffectiveModel(isInFallbackMode, customLiteModel, false);
expect(model).toBe(customLiteModel);
});
it('should downgrade any other custom model to the Flash model', () => {
const customModel = 'custom-model-v1-unlisted';
const model = getEffectiveModel(isInFallbackMode, customModel);
const model = getEffectiveModel(isInFallbackMode, customModel, false);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
describe('with preview features', () => {
it('should downgrade the Pro alias to the Flash model', () => {
const model = getEffectiveModel(
isInFallbackMode,
GEMINI_MODEL_ALIAS_PRO,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
it('should return the Flash alias when requested', () => {
const model = getEffectiveModel(
isInFallbackMode,
GEMINI_MODEL_ALIAS_FLASH,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
it('should return the Lite alias when requested', () => {
const model = getEffectiveModel(
isInFallbackMode,
GEMINI_MODEL_ALIAS_FLASH_LITE,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
});
it('should downgrade the default Gemini model to the Flash model', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_MODEL,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
it('should return the default Flash model when requested', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_FLASH_MODEL,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
it('should return the default Lite model when requested', () => {
const model = getEffectiveModel(
isInFallbackMode,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
true,
);
expect(model).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
});
it('should downgrade any other custom model to the Flash model', () => {
const customModel = 'custom-model-v1-unlisted';
const model = getEffectiveModel(isInFallbackMode, customModel, true);
expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
});
});
});
+55 -4
View File
@@ -4,17 +4,54 @@
* SPDX-License-Identifier: Apache-2.0
*/
export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview';
export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
export const DEFAULT_GEMINI_MODEL_AUTO = 'auto';
// Model aliases for user convenience.
export const GEMINI_MODEL_ALIAS_PRO = 'pro';
export const GEMINI_MODEL_ALIAS_FLASH = 'flash';
export const GEMINI_MODEL_ALIAS_FLASH_LITE = 'flash-lite';
export const DEFAULT_GEMINI_EMBEDDING_MODEL = 'gemini-embedding-001';
// Cap the thinking at 8192 to prevent run-away thinking loops.
export const DEFAULT_THINKING_MODE = 8192;
/**
* Resolves the requested model alias (e.g., 'auto', 'pro', 'flash', 'flash-lite')
* to a concrete model name, considering preview features.
*
* @param requestedModel The model alias or concrete model name requested by the user.
* @param previewFeaturesEnabled A boolean indicating if preview features are enabled.
* @returns The resolved concrete model name.
*/
export function resolveModel(
requestedModel: string,
previewFeaturesEnabled: boolean | undefined,
): string {
switch (requestedModel) {
case DEFAULT_GEMINI_MODEL_AUTO:
case GEMINI_MODEL_ALIAS_PRO: {
return previewFeaturesEnabled
? PREVIEW_GEMINI_MODEL
: DEFAULT_GEMINI_MODEL;
}
case GEMINI_MODEL_ALIAS_FLASH: {
return DEFAULT_GEMINI_FLASH_MODEL;
}
case GEMINI_MODEL_ALIAS_FLASH_LITE: {
return DEFAULT_GEMINI_FLASH_LITE_MODEL;
}
default: {
return requestedModel;
}
}
}
/**
* Determines the effective model to use, applying fallback logic if necessary.
*
@@ -26,23 +63,37 @@ export const DEFAULT_THINKING_MODE = 8192;
*
* @param isInFallbackMode Whether the application is in fallback mode.
* @param requestedModel The model that was originally requested.
* @param previewFeaturesEnabled A boolean indicating if preview features are enabled.
* @returns The effective model name.
*/
export function getEffectiveModel(
isInFallbackMode: boolean,
requestedModel: string,
previewFeaturesEnabled: boolean | undefined,
): string {
// If we are not in fallback mode, simply use the requested model.
const resolvedModel = resolveModel(requestedModel, previewFeaturesEnabled);
// If we are not in fallback mode, simply use the resolved model.
if (!isInFallbackMode) {
return requestedModel;
return resolvedModel;
}
// If a "lite" model is requested, honor it. This allows for variations of
// lite models without needing to list them all as constants.
if (requestedModel.includes('lite')) {
return requestedModel;
if (resolvedModel.includes('lite')) {
return resolvedModel;
}
// Default fallback for Gemini CLI.
return DEFAULT_GEMINI_FLASH_MODEL;
}
/**
* Checks if the model is a Gemini 2.x model.
*
* @param model The model name to check.
* @returns True if the model is a Gemini 2.x model.
*/
export function isGemini2Model(model: string): boolean {
return /^gemini-2(\.|$)/.test(model);
}