diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index e3773244be..c0d7eeac6f 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -827,7 +827,7 @@ their corresponding top-level category object in your `settings.json` file. - **`experimental.codebaseInvestigatorSettings.model`** (string): - **Description:** The model to use for the Codebase Investigator agent. - - **Default:** `"pro"` + - **Default:** `"gemini-2.5-pro"` - **Requires restart:** Yes #### `hooks` diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 5a605ea4a0..5cc118dba9 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1283,7 +1283,7 @@ describe('loadCliConfig model selection', () => { argv, ); - expect(config.getModel()).toBe('auto'); + expect(config.getModel()).toBe('auto-gemini-2.5'); }); it('always prefers model from argv', async () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 81fc01f001..ded9892065 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -31,6 +31,7 @@ import { debugLogger, loadServerHierarchicalMemory, WEB_FETCH_TOOL_NAME, + PREVIEW_GEMINI_MODEL_AUTO, } from '@google/gemini-cli-core'; import type { Settings } from './settings.js'; @@ -569,7 +570,9 @@ export async function loadCliConfig( extraExcludes.length > 0 ? extraExcludes : undefined, ); - const defaultModel = DEFAULT_GEMINI_MODEL_AUTO; + const defaultModel = settings.general?.previewFeatures + ? PREVIEW_GEMINI_MODEL_AUTO + : DEFAULT_GEMINI_MODEL_AUTO; const resolvedModel: string = argv.model || process.env['GEMINI_MODEL'] || diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 5efca6f20f..9a6426158d 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -19,7 +19,7 @@ import { DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_MODEL_CONFIGS, - GEMINI_MODEL_ALIAS_PRO, + DEFAULT_GEMINI_MODEL, } from '@google/gemini-cli-core'; import type { CustomTheme } from '../ui/themes/theme.js'; import type { SessionRetentionSettings } from './settings.js'; @@ -1384,7 +1384,7 @@ const SETTINGS_SCHEMA = { label: 'Model', category: 'Experimental', requiresRestart: true, - default: GEMINI_MODEL_ALIAS_PRO, + default: DEFAULT_GEMINI_MODEL, description: 'The model to use for the Codebase Investigator agent.', showInDialog: false, diff --git a/packages/cli/src/ui/components/ModelDialog.test.tsx b/packages/cli/src/ui/components/ModelDialog.test.tsx index ebbf921348..25b472dee9 100644 --- a/packages/cli/src/ui/components/ModelDialog.test.tsx +++ b/packages/cli/src/ui/components/ModelDialog.test.tsx @@ -4,239 +4,186 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render } from '../../test-utils/render.js'; -import { cleanup } from 'ink-testing-library'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { - GEMINI_MODEL_ALIAS_FLASH_LITE, - GEMINI_MODEL_ALIAS_FLASH, - GEMINI_MODEL_ALIAS_PRO, - DEFAULT_GEMINI_MODEL_AUTO, -} from '@google/gemini-cli-core'; +import { render } from 'ink-testing-library'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ModelDialog } from './ModelDialog.js'; -import { useKeypress } from '../hooks/useKeypress.js'; -import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js'; import { ConfigContext } from '../contexts/ConfigContext.js'; -import type { Config } from '@google/gemini-cli-core'; +import { KeypressProvider } from '../contexts/KeypressContext.js'; +import { + DEFAULT_GEMINI_MODEL, + DEFAULT_GEMINI_MODEL_AUTO, + DEFAULT_GEMINI_FLASH_MODEL, + DEFAULT_GEMINI_FLASH_LITE_MODEL, + PREVIEW_GEMINI_MODEL, +} from '@google/gemini-cli-core'; +import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core'; -vi.mock('../hooks/useKeypress.js', () => ({ - useKeypress: vi.fn(), -})); -const mockedUseKeypress = vi.mocked(useKeypress); - -vi.mock('./shared/DescriptiveRadioButtonSelect.js', () => ({ - DescriptiveRadioButtonSelect: vi.fn(() => null), -})); -const mockedSelect = vi.mocked(DescriptiveRadioButtonSelect); - -const renderComponent = ( - props: Partial> = {}, - contextValue: Partial | undefined = undefined, -) => { - const defaultProps = { - onClose: vi.fn(), - }; - const combinedProps = { ...defaultProps, ...props }; - - const mockConfig = contextValue - ? ({ - // --- Functions used by ModelDialog --- - getModel: vi.fn(() => DEFAULT_GEMINI_MODEL_AUTO), - setModel: vi.fn(), - getPreviewFeatures: vi.fn(() => false), - - // --- Functions used by ClearcutLogger --- - getUsageStatisticsEnabled: vi.fn(() => true), - getSessionId: vi.fn(() => 'mock-session-id'), - getDebugMode: vi.fn(() => false), - getContentGeneratorConfig: vi.fn(() => ({ authType: 'mock' })), - getUseSmartEdit: vi.fn(() => false), - getProxy: vi.fn(() => undefined), - isInteractive: vi.fn(() => false), - getExperiments: () => {}, - - // --- Spread test-specific overrides --- - ...contextValue, - } as Config) - : undefined; - - const renderResult = render( - - - , - ); +// Mock dependencies +const mockGetDisplayString = vi.fn(); +const mockLogModelSlashCommand = vi.fn(); +const mockModelSlashCommandEvent = vi.fn(); +vi.mock('@google/gemini-cli-core', async () => { + const actual = await vi.importActual('@google/gemini-cli-core'); return { - ...renderResult, - props: combinedProps, - mockConfig, + ...actual, + getDisplayString: (val: string) => mockGetDisplayString(val), + logModelSlashCommand: (config: Config, event: ModelSlashCommandEvent) => + mockLogModelSlashCommand(config, event), + ModelSlashCommandEvent: class { + constructor(model: string) { + mockModelSlashCommandEvent(model); + } + }, }; -}; +}); describe('', () => { + const mockSetModel = vi.fn(); + const mockGetModel = vi.fn(); + const mockGetPreviewFeatures = vi.fn(); + const mockOnClose = vi.fn(); + + interface MockConfig extends Partial { + setModel: (model: string) => void; + getModel: () => string; + getPreviewFeatures: () => boolean; + } + + const mockConfig: MockConfig = { + setModel: mockSetModel, + getModel: mockGetModel, + getPreviewFeatures: mockGetPreviewFeatures, + }; + beforeEach(() => { - vi.clearAllMocks(); + vi.resetAllMocks(); + mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO); + mockGetPreviewFeatures.mockReturnValue(false); + + // Default implementation for getDisplayString + mockGetDisplayString.mockImplementation((val: string) => { + if (val === 'auto-gemini-2.5') return 'Auto (Gemini 2.5)'; + if (val === 'auto-gemini-3') return 'Auto (Preview)'; + return val; + }); }); - afterEach(() => { - cleanup(); - }); + const renderComponent = (contextValue = mockConfig as Config) => + render( + + + + + , + ); - it('renders the title and help text', () => { - const { lastFrame, unmount } = renderComponent(); + const waitForUpdate = () => + new Promise((resolve) => setTimeout(resolve, 150)); + + it('renders the initial "main" view correctly', () => { + const { lastFrame } = renderComponent(); expect(lastFrame()).toContain('Select Model'); - expect(lastFrame()).toContain('(Press Esc to close)'); - expect(lastFrame()).toContain( - 'To use a specific Gemini model on startup, use the --model flag.', - ); - unmount(); + expect(lastFrame()).toContain('Auto'); + expect(lastFrame()).toContain('Manual'); }); - it('passes all model options to DescriptiveRadioButtonSelect', () => { - const { unmount } = renderComponent(); - expect(mockedSelect).toHaveBeenCalledTimes(1); - - const props = mockedSelect.mock.calls[0][0]; - expect(props.items).toHaveLength(4); - expect(props.items[0].value).toBe(DEFAULT_GEMINI_MODEL_AUTO); - expect(props.items[1].value).toBe(GEMINI_MODEL_ALIAS_PRO); - expect(props.items[2].value).toBe(GEMINI_MODEL_ALIAS_FLASH); - expect(props.items[3].value).toBe(GEMINI_MODEL_ALIAS_FLASH_LITE); - expect(props.showNumbers).toBe(true); - unmount(); + it('renders "main" view with preview options when preview features are enabled', () => { + mockGetPreviewFeatures.mockReturnValue(true); + const { lastFrame } = renderComponent(); + expect(lastFrame()).toContain('Auto (Preview)'); }); - it('initializes with the model from ConfigContext', () => { - const mockGetModel = vi.fn(() => GEMINI_MODEL_ALIAS_FLASH); - const { unmount } = renderComponent({}, { getModel: mockGetModel }); + it('switches to "manual" view when "Manual" is selected', async () => { + const { lastFrame, stdin } = renderComponent(); - expect(mockGetModel).toHaveBeenCalled(); - expect(mockedSelect).toHaveBeenCalledWith( - expect.objectContaining({ - initialIndex: 2, - }), - undefined, - ); - unmount(); + // Select "Manual" (index 1) + // Press down arrow to move to "Manual" + stdin.write('\u001B[B'); // Arrow Down + await waitForUpdate(); + + // Press enter to select + stdin.write('\r'); + await waitForUpdate(); + + // Should now show manual options + expect(lastFrame()).toContain(DEFAULT_GEMINI_MODEL); + expect(lastFrame()).toContain(DEFAULT_GEMINI_FLASH_MODEL); + expect(lastFrame()).toContain(DEFAULT_GEMINI_FLASH_LITE_MODEL); }); - it('initializes with "auto" model if context is not provided', () => { - const { unmount } = renderComponent({}, undefined); + it('renders "manual" view with preview options when preview features are enabled', async () => { + mockGetPreviewFeatures.mockReturnValue(true); + mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO); + const { lastFrame, stdin } = renderComponent(); - expect(mockedSelect).toHaveBeenCalledWith( - expect.objectContaining({ - initialIndex: 0, - }), - undefined, - ); - unmount(); + // Select "Manual" (index 2 because Preview Auto is first, then Auto (Gemini 2.5)) + stdin.write('\u001B[B'); // Arrow Down (to Auto (Gemini 2.5)) + await waitForUpdate(); + stdin.write('\u001B[B'); // Arrow Down (to Manual) + await waitForUpdate(); + + // Press enter to select Manual + stdin.write('\r'); + await waitForUpdate(); + + expect(lastFrame()).toContain(PREVIEW_GEMINI_MODEL); }); - it('initializes with "auto" model if getModel returns undefined', () => { - const mockGetModel = vi.fn(() => undefined); - // @ts-expect-error This test validates component robustness when getModel - // returns an unexpected undefined value. - const { unmount } = renderComponent({}, { getModel: mockGetModel }); + it('sets model and closes when a model is selected in "main" view', async () => { + const { stdin } = renderComponent(); - expect(mockGetModel).toHaveBeenCalled(); + // Select "Auto" (index 0) + stdin.write('\r'); + await waitForUpdate(); - // When getModel returns undefined, preferredModel falls back to DEFAULT_GEMINI_MODEL_AUTO - // which has index 0, so initialIndex should be 0 - expect(mockedSelect).toHaveBeenCalledWith( - expect.objectContaining({ - initialIndex: 0, - }), - undefined, - ); - expect(mockedSelect).toHaveBeenCalledTimes(1); - unmount(); + expect(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL_AUTO); + expect(mockOnClose).toHaveBeenCalled(); }); - it('calls config.setModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', () => { - const { props, mockConfig, unmount } = renderComponent({}, {}); // Pass empty object for contextValue + it('sets model and closes when a model is selected in "manual" view', async () => { + const { stdin } = renderComponent(); - const childOnSelect = mockedSelect.mock.calls[0][0].onSelect; - expect(childOnSelect).toBeDefined(); + // Navigate to Manual (index 1) and select + stdin.write('\u001B[B'); + await waitForUpdate(); + stdin.write('\r'); + await waitForUpdate(); - childOnSelect(GEMINI_MODEL_ALIAS_PRO); + // Now in manual view. Default selection is first item (DEFAULT_GEMINI_MODEL) + stdin.write('\r'); + await waitForUpdate(); - // Assert against the default mock provided by renderComponent - expect(mockConfig?.setModel).toHaveBeenCalledWith(GEMINI_MODEL_ALIAS_PRO); - expect(props.onClose).toHaveBeenCalledTimes(1); - unmount(); + expect(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL); + expect(mockOnClose).toHaveBeenCalled(); }); - it('does not pass onHighlight to DescriptiveRadioButtonSelect', () => { - const { unmount } = renderComponent(); + it('closes dialog on escape in "main" view', async () => { + const { stdin } = renderComponent(); - const childOnHighlight = mockedSelect.mock.calls[0][0].onHighlight; - expect(childOnHighlight).toBeUndefined(); - unmount(); + stdin.write('\u001B'); // Escape + await waitForUpdate(); + + expect(mockOnClose).toHaveBeenCalled(); }); - it('calls onClose prop when "escape" key is pressed', () => { - const { props, unmount } = renderComponent(); + it('goes back to "main" view on escape in "manual" view', async () => { + const { lastFrame, stdin } = renderComponent(); - expect(mockedUseKeypress).toHaveBeenCalled(); + // Go to manual view + stdin.write('\u001B[B'); + await waitForUpdate(); + stdin.write('\r'); + await waitForUpdate(); - const keyPressHandler = mockedUseKeypress.mock.calls[0][0]; - const options = mockedUseKeypress.mock.calls[0][1]; + expect(lastFrame()).toContain(DEFAULT_GEMINI_MODEL); - expect(options).toEqual({ isActive: true }); + // Press Escape + stdin.write('\u001B'); + await waitForUpdate(); - keyPressHandler({ - name: 'escape', - ctrl: false, - meta: false, - shift: false, - paste: false, - insertable: false, - sequence: '', - }); - expect(props.onClose).toHaveBeenCalledTimes(1); - - keyPressHandler({ - name: 'a', - ctrl: false, - meta: false, - shift: false, - paste: false, - insertable: true, - sequence: '', - }); - expect(props.onClose).toHaveBeenCalledTimes(1); - unmount(); - }); - - it('updates initialIndex when config context changes', () => { - const mockGetModel = vi.fn(() => DEFAULT_GEMINI_MODEL_AUTO); - const oldMockConfig = { - getModel: mockGetModel, - getPreviewFeatures: vi.fn(() => false), - } as unknown as Config; - const { rerender, unmount } = render( - - - , - ); - - expect(mockedSelect.mock.calls[0][0].initialIndex).toBe(0); - - mockGetModel.mockReturnValue(GEMINI_MODEL_ALIAS_FLASH_LITE); - const newMockConfig = { - getModel: mockGetModel, - getPreviewFeatures: vi.fn(() => false), - } as unknown as Config; - - rerender( - - - , - ); - - // Should be called at least twice: initial render + re-render after context change - expect(mockedSelect).toHaveBeenCalledTimes(2); - expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(3); - unmount(); + expect(mockOnClose).not.toHaveBeenCalled(); + // Should be back to main view (Manual option visible) + expect(lastFrame()).toContain('Manual'); }); }); diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx index caec692e01..ff92e96939 100644 --- a/packages/cli/src/ui/components/ModelDialog.tsx +++ b/packages/cli/src/ui/components/ModelDialog.tsx @@ -5,19 +5,19 @@ */ import type React from 'react'; -import { useCallback, useContext, useMemo } from 'react'; +import { useCallback, useContext, useMemo, useState } from 'react'; import { Box, Text } from 'ink'; import { PREVIEW_GEMINI_MODEL, + PREVIEW_GEMINI_FLASH_MODEL, + PREVIEW_GEMINI_MODEL_AUTO, DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_FLASH_LITE_MODEL, DEFAULT_GEMINI_MODEL_AUTO, - GEMINI_MODEL_ALIAS_FLASH, - GEMINI_MODEL_ALIAS_FLASH_LITE, - GEMINI_MODEL_ALIAS_PRO, ModelSlashCommandEvent, logModelSlashCommand, + getDisplayString, } from '@google/gemini-cli-core'; import { useKeypress } from '../hooks/useKeypress.js'; import { theme } from '../semantic-colors.js'; @@ -31,61 +31,128 @@ interface ModelDialogProps { export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { const config = useContext(ConfigContext); + const [view, setView] = useState<'main' | 'manual'>('main'); // Determine the Preferred Model (read once when the dialog opens). const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO; + const manualModelSelected = useMemo(() => { + const manualModels = [ + DEFAULT_GEMINI_MODEL, + DEFAULT_GEMINI_FLASH_MODEL, + DEFAULT_GEMINI_FLASH_LITE_MODEL, + PREVIEW_GEMINI_MODEL, + PREVIEW_GEMINI_FLASH_MODEL, + ]; + if (manualModels.includes(preferredModel)) { + return preferredModel; + } + return ''; + }, [preferredModel]); + useKeypress( (key) => { if (key.name === 'escape') { - onClose(); + if (view === 'manual') { + setView('main'); + } else { + onClose(); + } } }, { isActive: true }, ); - const options = useMemo( - () => [ + const mainOptions = useMemo(() => { + const list = [ { value: DEFAULT_GEMINI_MODEL_AUTO, - title: 'Auto', - description: 'Let the system choose the best model for your task.', + title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO), + description: + 'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash', key: DEFAULT_GEMINI_MODEL_AUTO, }, { - value: GEMINI_MODEL_ALIAS_PRO, - title: config?.getPreviewFeatures() - ? `Pro (${PREVIEW_GEMINI_MODEL}, ${DEFAULT_GEMINI_MODEL})` - : `Pro (${DEFAULT_GEMINI_MODEL})`, + value: 'Manual', + title: manualModelSelected + ? `Manual (${manualModelSelected})` + : 'Manual', + description: 'Manually select a model', + key: 'Manual', + }, + ]; + + if (config?.getPreviewFeatures()) { + list.unshift({ + value: PREVIEW_GEMINI_MODEL_AUTO, + title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO), description: - 'For complex tasks that require deep reasoning and creativity', - key: GEMINI_MODEL_ALIAS_PRO, + 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash', + key: PREVIEW_GEMINI_MODEL_AUTO, + }); + } + return list; + }, [config, manualModelSelected]); + + const manualOptions = useMemo(() => { + const list = [ + { + value: DEFAULT_GEMINI_MODEL, + title: DEFAULT_GEMINI_MODEL, + key: DEFAULT_GEMINI_MODEL, }, { - value: GEMINI_MODEL_ALIAS_FLASH, - title: `Flash (${DEFAULT_GEMINI_FLASH_MODEL})`, - description: 'For tasks that need a balance of speed and reasoning', - key: GEMINI_MODEL_ALIAS_FLASH, + value: DEFAULT_GEMINI_FLASH_MODEL, + title: DEFAULT_GEMINI_FLASH_MODEL, + key: DEFAULT_GEMINI_FLASH_MODEL, }, { - value: GEMINI_MODEL_ALIAS_FLASH_LITE, - title: `Flash-Lite (${DEFAULT_GEMINI_FLASH_LITE_MODEL})`, - description: 'For simple tasks that need to be done quickly', - key: GEMINI_MODEL_ALIAS_FLASH_LITE, + value: DEFAULT_GEMINI_FLASH_LITE_MODEL, + title: DEFAULT_GEMINI_FLASH_LITE_MODEL, + key: DEFAULT_GEMINI_FLASH_LITE_MODEL, }, - ], - [config], - ); + ]; + + if (config?.getPreviewFeatures()) { + list.unshift( + { + value: PREVIEW_GEMINI_MODEL, + title: PREVIEW_GEMINI_MODEL, + key: PREVIEW_GEMINI_MODEL, + }, + { + value: PREVIEW_GEMINI_FLASH_MODEL, + title: PREVIEW_GEMINI_FLASH_MODEL, + key: PREVIEW_GEMINI_FLASH_MODEL, + }, + ); + } + return list; + }, [config]); + + const options = view === 'main' ? mainOptions : manualOptions; // Calculate the initial index based on the preferred model. - const initialIndex = useMemo( - () => options.findIndex((option) => option.value === preferredModel), - [preferredModel, options], - ); + const initialIndex = useMemo(() => { + const idx = options.findIndex((option) => option.value === preferredModel); + if (idx !== -1) { + return idx; + } + if (view === 'main') { + const manualIdx = options.findIndex((o) => o.value === 'Manual'); + return manualIdx !== -1 ? manualIdx : 0; + } + return 0; + }, [preferredModel, options, view]); // Handle selection internally (Autonomous Dialog). const handleSelect = useCallback( (model: string) => { + if (model === 'Manual') { + setView('manual'); + return; + } + if (config) { config.setModel(model); const event = new ModelSlashCommandEvent(model); diff --git a/packages/cli/src/ui/components/shared/DescriptiveRadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/DescriptiveRadioButtonSelect.tsx index 3cc563283d..c6faed2774 100644 --- a/packages/cli/src/ui/components/shared/DescriptiveRadioButtonSelect.tsx +++ b/packages/cli/src/ui/components/shared/DescriptiveRadioButtonSelect.tsx @@ -12,7 +12,7 @@ import type { SelectionListItem } from '../../hooks/useSelectionList.js'; export interface DescriptiveRadioSelectItem extends SelectionListItem { title: string; - description: string; + description?: string; } export interface DescriptiveRadioButtonSelectProps { @@ -62,7 +62,9 @@ export function DescriptiveRadioButtonSelect({ renderItem={(item, { titleColor }) => ( {item.title} - {item.description} + {item.description && ( + {item.description} + )} )} /> diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index 93e4121705..b20c01ee2e 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -256,9 +256,8 @@ export class Session { try { const model = getEffectiveModel( - this.config.isInFallbackMode(), this.config.getModel(), - this.config.getPreviewFeatures(), + this.config.isInFallbackMode(), ); const responseStream = await chat.sendMessageStream( { model }, diff --git a/packages/core/index.ts b/packages/core/index.ts index eff91f2228..e9f6bebac4 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -12,9 +12,6 @@ export { DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_FLASH_LITE_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL, - GEMINI_MODEL_ALIAS_PRO, - GEMINI_MODEL_ALIAS_FLASH, - GEMINI_MODEL_ALIAS_FLASH_LITE, } from './src/config/models.js'; export { serializeTerminalToObject, diff --git a/packages/core/src/agents/codebase-investigator.test.ts b/packages/core/src/agents/codebase-investigator.test.ts index f5398fc32a..3d8453cb97 100644 --- a/packages/core/src/agents/codebase-investigator.test.ts +++ b/packages/core/src/agents/codebase-investigator.test.ts @@ -12,7 +12,7 @@ import { LS_TOOL_NAME, READ_FILE_TOOL_NAME, } from '../tools/tool-names.js'; -import { GEMINI_MODEL_ALIAS_PRO } from '../config/models.js'; +import { DEFAULT_GEMINI_MODEL } from '../config/models.js'; describe('CodebaseInvestigatorAgent', () => { it('should have the correct agent definition', () => { @@ -26,7 +26,7 @@ describe('CodebaseInvestigatorAgent', () => { ).toBe(true); expect(CodebaseInvestigatorAgent.outputConfig?.outputName).toBe('report'); expect(CodebaseInvestigatorAgent.modelConfig?.model).toBe( - GEMINI_MODEL_ALIAS_PRO, + DEFAULT_GEMINI_MODEL, ); expect(CodebaseInvestigatorAgent.toolConfig?.tools).toEqual([ LS_TOOL_NAME, diff --git a/packages/core/src/agents/codebase-investigator.ts b/packages/core/src/agents/codebase-investigator.ts index 947fc1f9c4..adda3e96a5 100644 --- a/packages/core/src/agents/codebase-investigator.ts +++ b/packages/core/src/agents/codebase-investigator.ts @@ -11,7 +11,7 @@ import { LS_TOOL_NAME, READ_FILE_TOOL_NAME, } from '../tools/tool-names.js'; -import { GEMINI_MODEL_ALIAS_PRO } from '../config/models.js'; +import { DEFAULT_GEMINI_MODEL } from '../config/models.js'; import { z } from 'zod'; // Define a type that matches the outputConfig schema for type safety. @@ -69,7 +69,7 @@ export const CodebaseInvestigatorAgent: AgentDefinition< processOutput: (output) => JSON.stringify(output, null, 2), modelConfig: { - model: GEMINI_MODEL_ALIAS_PRO, + model: DEFAULT_GEMINI_MODEL, temp: 0.1, top_p: 0.95, thinkingBudget: -1, diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index 4df0b206a9..4e5995ff74 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -10,6 +10,7 @@ import { makeFakeConfig } from '../test-utils/config.js'; import type { AgentDefinition } from './types.js'; import type { Config } from '../config/config.js'; import { debugLogger } from '../utils/debugLogger.js'; +import { DEFAULT_GEMINI_MODEL } from '../config/models.js'; // A test-only subclass to expose the protected `registerAgent` method. class TestableAgentRegistry extends AgentRegistry { @@ -78,7 +79,7 @@ describe('AgentRegistry', () => { model: 'gemini-3-pro-preview', codebaseInvestigatorSettings: { enabled: true, - model: 'pro', + model: DEFAULT_GEMINI_MODEL, }, }); const previewRegistry = new TestableAgentRegistry(previewConfig); diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index c7d7c65fae..812c701ac6 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -10,9 +10,10 @@ import { CodebaseInvestigatorAgent } from './codebase-investigator.js'; import { type z } from 'zod'; import { debugLogger } from '../utils/debugLogger.js'; import { + DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_MODEL_AUTO, - GEMINI_MODEL_ALIAS_PRO, PREVIEW_GEMINI_MODEL, + PREVIEW_GEMINI_MODEL_AUTO, } from '../config/models.js'; import type { ModelConfigAlias } from '../services/modelConfigService.js'; @@ -59,10 +60,14 @@ export class AgentRegistry { // If the user is using the preview model for the main agent, force the sub-agent to use it too // if it's configured to use 'pro' or 'auto'. - if (this.config.getModel() === PREVIEW_GEMINI_MODEL) { + if ( + this.config.getModel() === PREVIEW_GEMINI_MODEL || + this.config.getModel() === PREVIEW_GEMINI_MODEL_AUTO + ) { if ( - model === GEMINI_MODEL_ALIAS_PRO || - model === DEFAULT_GEMINI_MODEL_AUTO + model === PREVIEW_GEMINI_MODEL_AUTO || + model === DEFAULT_GEMINI_MODEL_AUTO || + model === DEFAULT_GEMINI_MODEL ) { model = PREVIEW_GEMINI_MODEL; } diff --git a/packages/core/src/availability/policyHelpers.ts b/packages/core/src/availability/policyHelpers.ts index ee0de84147..a90386e824 100644 --- a/packages/core/src/availability/policyHelpers.ts +++ b/packages/core/src/availability/policyHelpers.ts @@ -33,11 +33,7 @@ export function resolvePolicyChain( // Switch to getActiveModel() const activeModel = preferredModel ?? - getEffectiveModel( - config.isInFallbackMode(), - config.getModel(), - config.getPreviewFeatures(), - ); + getEffectiveModel(config.getModel(), config.isInFallbackMode()); if (activeModel === 'auto') { return [...chain]; diff --git a/packages/core/src/config/models.test.ts b/packages/core/src/config/models.test.ts index 48a6f80030..fe1298e09f 100644 --- a/packages/core/src/config/models.test.ts +++ b/packages/core/src/config/models.test.ts @@ -7,220 +7,116 @@ import { describe, it, expect } from 'vitest'; import { getEffectiveModel, + isGemini2Model, DEFAULT_GEMINI_MODEL, PREVIEW_GEMINI_MODEL, DEFAULT_GEMINI_FLASH_MODEL, + PREVIEW_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_FLASH_LITE_MODEL, - GEMINI_MODEL_ALIAS_PRO, - GEMINI_MODEL_ALIAS_FLASH, - GEMINI_MODEL_ALIAS_FLASH_LITE, + PREVIEW_GEMINI_MODEL_AUTO, + DEFAULT_GEMINI_MODEL_AUTO, } from './models.js'; describe('getEffectiveModel', () => { describe('When NOT in fallback mode', () => { - const isInFallbackMode = false; + const useFallbackModel = false; - it('should return the Pro model when Pro is requested', () => { + it('should return the Preview Pro model when auto-preview is requested', () => { const model = getEffectiveModel( - isInFallbackMode, - DEFAULT_GEMINI_MODEL, - false, + PREVIEW_GEMINI_MODEL_AUTO, + useFallbackModel, + ); + expect(model).toBe(PREVIEW_GEMINI_MODEL); + }); + + it('should return the Default Pro model when auto-default is requested', () => { + const model = getEffectiveModel( + DEFAULT_GEMINI_MODEL_AUTO, + useFallbackModel, ); expect(model).toBe(DEFAULT_GEMINI_MODEL); }); - it('should return the Flash model when Flash is requested', () => { - const model = getEffectiveModel( - isInFallbackMode, - DEFAULT_GEMINI_FLASH_MODEL, - false, + it('should return the requested model as-is for explicit specific models', () => { + expect(getEffectiveModel(DEFAULT_GEMINI_MODEL, useFallbackModel)).toBe( + DEFAULT_GEMINI_MODEL, ); - expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL); - }); - - it('should return the Lite model when Lite is requested', () => { - const model = getEffectiveModel( - isInFallbackMode, - DEFAULT_GEMINI_FLASH_LITE_MODEL, - false, - ); - expect(model).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL); + expect( + getEffectiveModel(DEFAULT_GEMINI_FLASH_MODEL, useFallbackModel), + ).toBe(DEFAULT_GEMINI_FLASH_MODEL); + expect( + getEffectiveModel(DEFAULT_GEMINI_FLASH_LITE_MODEL, useFallbackModel), + ).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, false); + const model = getEffectiveModel(customModel, useFallbackModel); 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; + const useFallbackModel = true; - it('should downgrade the Pro model to the Flash model', () => { + it('should return the Preview Flash model when auto-preview is requested', () => { const model = getEffectiveModel( - isInFallbackMode, + PREVIEW_GEMINI_MODEL_AUTO, + useFallbackModel, + ); + expect(model).toBe(PREVIEW_GEMINI_FLASH_MODEL); + }); + + it('should return the Default Flash model when auto-default is requested', () => { + const model = getEffectiveModel( + DEFAULT_GEMINI_MODEL_AUTO, + useFallbackModel, + ); + expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL); + }); + + it('should return the requested model as-is for explicit specific models', () => { + expect(getEffectiveModel(DEFAULT_GEMINI_MODEL, useFallbackModel)).toBe( DEFAULT_GEMINI_MODEL, - false, ); - expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL); + expect( + getEffectiveModel(DEFAULT_GEMINI_FLASH_MODEL, useFallbackModel), + ).toBe(DEFAULT_GEMINI_FLASH_MODEL); + expect( + getEffectiveModel(DEFAULT_GEMINI_FLASH_LITE_MODEL, useFallbackModel), + ).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL); }); - it('should return the Flash model when Flash is requested', () => { - const model = getEffectiveModel( - isInFallbackMode, - DEFAULT_GEMINI_FLASH_MODEL, - false, - ); - expect(model).toBe(DEFAULT_GEMINI_FLASH_MODEL); - }); - - it('should HONOR the Lite model when Lite is requested', () => { - 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, 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, 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); - }); + it('should return custom model name as-is', () => { + const customModel = 'custom-model-v1'; + const model = getEffectiveModel(customModel, useFallbackModel); + expect(model).toBe(customModel); }); }); }); + +describe('isGemini2Model', () => { + it('should return true for gemini-2.5-pro', () => { + expect(isGemini2Model('gemini-2.5-pro')).toBe(true); + }); + + it('should return true for gemini-2.5-flash', () => { + expect(isGemini2Model('gemini-2.5-flash')).toBe(true); + }); + + it('should return true for gemini-2.0-flash', () => { + expect(isGemini2Model('gemini-2.0-flash')).toBe(true); + }); + + it('should return false for gemini-1.5-pro', () => { + expect(isGemini2Model('gemini-1.5-pro')).toBe(false); + }); + + it('should return false for gemini-3-pro', () => { + expect(isGemini2Model('gemini-3-pro')).toBe(false); + }); + + it('should return false for arbitrary strings', () => { + expect(isGemini2Model('gpt-4')).toBe(false); + }); +}); diff --git a/packages/core/src/config/models.ts b/packages/core/src/config/models.ts index 807761916a..bf743d31e0 100644 --- a/packages/core/src/config/models.ts +++ b/packages/core/src/config/models.ts @@ -10,83 +10,68 @@ 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'; +export const VALID_GEMINI_MODELS = new Set([ + PREVIEW_GEMINI_MODEL, + DEFAULT_GEMINI_MODEL, + DEFAULT_GEMINI_FLASH_MODEL, + DEFAULT_GEMINI_FLASH_LITE_MODEL, +]); -// 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 PREVIEW_GEMINI_MODEL_AUTO = 'auto-gemini-3'; +export const DEFAULT_GEMINI_MODEL_AUTO = 'auto-gemini-2.5'; 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. * * When fallback mode is active, this function enforces the use of the standard - * fallback model. However, it makes an exception for "lite" models (any model - * with "lite" in its name), allowing them to be used to preserve cost savings. - * This ensures that "pro" models are always downgraded, while "lite" model - * requests are honored. + * fallback model. * - * @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. + * @param isInFallbackMode Whether the application is in fallback mode. * @returns The effective model name. */ export function getEffectiveModel( - isInFallbackMode: boolean, requestedModel: string, - previewFeaturesEnabled: boolean | undefined, + useFallbackModel: boolean, ): string { - const resolvedModel = resolveModel(requestedModel, previewFeaturesEnabled); - // If we are not in fallback mode, simply use the resolved model. - if (!isInFallbackMode) { - return resolvedModel; + if (!useFallbackModel) { + switch (requestedModel) { + case PREVIEW_GEMINI_MODEL_AUTO: + return PREVIEW_GEMINI_MODEL; + case DEFAULT_GEMINI_MODEL_AUTO: + return DEFAULT_GEMINI_MODEL; + default: + return requestedModel; + } } - // If a "lite" model is requested, honor it. This allows for variations of - // lite models without needing to list them all as constants. - if (resolvedModel.includes('lite')) { - return resolvedModel; + // Fallback model for corresponding model family. We are doing fallback only + // for Auto modes + switch (requestedModel) { + case PREVIEW_GEMINI_MODEL_AUTO: + return PREVIEW_GEMINI_FLASH_MODEL; + case DEFAULT_GEMINI_MODEL_AUTO: + return DEFAULT_GEMINI_FLASH_MODEL; + default: + return requestedModel; } +} - // Default fallback for Gemini CLI. - return DEFAULT_GEMINI_FLASH_MODEL; +export function getDisplayString(model: string) { + switch (model) { + case PREVIEW_GEMINI_MODEL_AUTO: + return 'Auto (Gemini 3)'; + case DEFAULT_GEMINI_MODEL_AUTO: + return 'Auto (Gemini 2.5)'; + default: + return model; + } } /** diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 3ce8c1306f..236a9b9809 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -396,11 +396,7 @@ export class GeminiClient { } const configModel = this.config.getModel(); - return getEffectiveModel( - this.config.isInFallbackMode(), - configModel, - this.config.getPreviewFeatures(), - ); + return getEffectiveModel(configModel, this.config.isInFallbackMode()); } async *sendMessageStream( diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index 7afdd00ec7..587d90f60d 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -21,7 +21,9 @@ import { DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_MODEL, DEFAULT_THINKING_MODE, + PREVIEW_GEMINI_FLASH_MODEL, PREVIEW_GEMINI_MODEL, + PREVIEW_GEMINI_MODEL_AUTO, } from '../config/models.js'; import { AuthType } from './contentGenerator.js'; import { TerminalQuotaError } from '../utils/googleQuotaErrors.js'; @@ -155,7 +157,8 @@ describe('GeminiChat', () => { getUserTier: vi.fn().mockReturnValue(undefined), modelConfigService: { getResolvedConfig: vi.fn().mockImplementation((modelConfigKey) => { - const thinkingConfig = modelConfigKey.model.startsWith('gemini-3') + const model = modelConfigKey.model ?? mockConfig.getModel(); + const thinkingConfig = model.startsWith('gemini-3') ? { thinkingLevel: ThinkingLevel.HIGH, } @@ -163,7 +166,7 @@ describe('GeminiChat', () => { thinkingBudget: DEFAULT_THINKING_MODE, }; return { - model: modelConfigKey.model, + model, generateContentConfig: { temperature: 0, thinkingConfig, @@ -1859,7 +1862,7 @@ describe('GeminiChat', () => { } as unknown as GenerateContentResponse; it('should use the FLASH model when in fallback mode (sendMessageStream)', async () => { - vi.mocked(mockConfig.getModel).mockReturnValue('gemini-pro'); + vi.mocked(mockConfig.getModel).mockReturnValue('auto-gemini-2.5'); vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true); vi.mocked(mockContentGenerator.generateContentStream).mockImplementation( async () => @@ -1869,7 +1872,7 @@ describe('GeminiChat', () => { ); const stream = await chat.sendMessageStream( - { model: 'test-model' }, + { model: 'auto-gemini-2.5' }, 'test message', 'prompt-id-res3', new AbortController().signal, @@ -1983,7 +1986,7 @@ describe('GeminiChat', () => { expect(modelTurn.parts![0]!.text).toBe('Success on retry'); }); - it('should switch to DEFAULT_GEMINI_FLASH_MODEL and use thinkingBudget when falling back from a gemini-3 model', async () => { + it('should switch to PREVIEW_GEMINI_FLASH_MODEL and use thinkingLevel when falling back from a gemini-3 model', async () => { // ARRANGE const authType = AuthType.LOGIN_WITH_GOOGLE; vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({ @@ -2020,7 +2023,7 @@ describe('GeminiChat', () => { // ACT const stream = await chat.sendMessageStream( - { model: 'gemini-3-test-model' }, // Start with a gemini-3 model + { model: PREVIEW_GEMINI_MODEL_AUTO }, // Start with a gemini-3 model 'test fallback thinking', 'prompt-id-fb3', new AbortController().signal, @@ -2040,7 +2043,7 @@ describe('GeminiChat', () => { ).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - model: 'gemini-3-test-model', + model: PREVIEW_GEMINI_MODEL, config: expect.objectContaining({ thinkingConfig: { thinkingBudget: undefined, @@ -2051,17 +2054,17 @@ describe('GeminiChat', () => { 'prompt-id-fb3', ); - // Second call: DEFAULT_GEMINI_FLASH_MODEL (due to fallback), thinkingBudget set (due to fix) + // Second call: PREVIEW_GEMINI_FLASH_MODEL (due to fallback), thinkingLevel set expect( mockContentGenerator.generateContentStream, ).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - model: DEFAULT_GEMINI_FLASH_MODEL, + model: PREVIEW_GEMINI_FLASH_MODEL, config: expect.objectContaining({ thinkingConfig: { - thinkingBudget: DEFAULT_THINKING_MODE, - thinkingLevel: undefined, + thinkingBudget: undefined, + thinkingLevel: ThinkingLevel.HIGH, }, }), }), diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index bec85a5152..c1b0e17412 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -460,11 +460,7 @@ export class GeminiChat { } } } else { - modelToUse = getEffectiveModel( - this.config.isInFallbackMode(), - model, - this.config.getPreviewFeatures(), - ); + modelToUse = getEffectiveModel(model, this.config.isInFallbackMode()); // Preview Model Bypass Logic: // If we are in "Preview Model Bypass Mode" (transient failure), we force downgrade to 2.5 Pro diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index ef778410bc..d0bf8808d2 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -106,9 +106,8 @@ export function getCoreSystemPrompt( // TODO(joshualitt): Replace with system instructions on model configs. const desiredModel = getEffectiveModel( - config.isInFallbackMode(), config.getModel(), - config.getPreviewFeatures(), + config.isInFallbackMode(), ); const isGemini3 = desiredModel === PREVIEW_GEMINI_MODEL; diff --git a/packages/core/src/routing/strategies/classifierStrategy.test.ts b/packages/core/src/routing/strategies/classifierStrategy.test.ts index d17414318e..21d324c1fb 100644 --- a/packages/core/src/routing/strategies/classifierStrategy.test.ts +++ b/packages/core/src/routing/strategies/classifierStrategy.test.ts @@ -16,6 +16,7 @@ import { import { DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_MODEL, + DEFAULT_GEMINI_MODEL_AUTO, } from '../../config/models.js'; import { promptIdContext } from '../../utils/promptIdContext.js'; import type { Content } from '@google/genai'; @@ -50,6 +51,7 @@ describe('ClassifierStrategy', () => { modelConfigService: { getResolvedConfig: vi.fn().mockReturnValue(mockResolvedConfig), }, + getModel: () => DEFAULT_GEMINI_MODEL_AUTO, getPreviewFeatures: () => false, } as unknown as Config; mockBaseLlmClient = { diff --git a/packages/core/src/routing/strategies/classifierStrategy.ts b/packages/core/src/routing/strategies/classifierStrategy.ts index 17a35b65c0..2c52ee644e 100644 --- a/packages/core/src/routing/strategies/classifierStrategy.ts +++ b/packages/core/src/routing/strategies/classifierStrategy.ts @@ -12,11 +12,7 @@ import type { RoutingDecision, RoutingStrategy, } from '../routingStrategy.js'; -import { - GEMINI_MODEL_ALIAS_FLASH, - GEMINI_MODEL_ALIAS_PRO, - resolveModel, -} from '../../config/models.js'; +import { getEffectiveModel } from '../../config/models.js'; import { createUserContent, Type } from '@google/genai'; import type { Config } from '../../config/config.js'; import { @@ -174,10 +170,7 @@ export class ClassifierStrategy implements RoutingStrategy { if (routerResponse.model_choice === FLASH_MODEL) { return { - model: resolveModel( - GEMINI_MODEL_ALIAS_FLASH, - config.getPreviewFeatures(), - ), + model: getEffectiveModel(config.getModel(), true), metadata: { source: 'Classifier', latencyMs, @@ -186,10 +179,7 @@ export class ClassifierStrategy implements RoutingStrategy { }; } else { return { - model: resolveModel( - GEMINI_MODEL_ALIAS_PRO, - config.getPreviewFeatures(), - ), + model: getEffectiveModel(config.getModel(), false), metadata: { source: 'Classifier', reasoning, diff --git a/packages/core/src/routing/strategies/fallbackStrategy.test.ts b/packages/core/src/routing/strategies/fallbackStrategy.test.ts index 8723c483f2..0913a84fa3 100644 --- a/packages/core/src/routing/strategies/fallbackStrategy.test.ts +++ b/packages/core/src/routing/strategies/fallbackStrategy.test.ts @@ -13,6 +13,9 @@ import { DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_FLASH_LITE_MODEL, + DEFAULT_GEMINI_MODEL_AUTO, + PREVIEW_GEMINI_MODEL_AUTO, + PREVIEW_GEMINI_FLASH_MODEL, } from '../../config/models.js'; describe('FallbackStrategy', () => { @@ -32,11 +35,10 @@ describe('FallbackStrategy', () => { }); describe('when in fallback mode', () => { - it('should downgrade a pro model to the flash model', async () => { + it('should downgrade a default auto model to the flash model', async () => { const mockConfig = { isInFallbackMode: () => true, - getModel: () => DEFAULT_GEMINI_MODEL, - getPreviewFeatures: () => false, + getModel: () => DEFAULT_GEMINI_MODEL_AUTO, } as Config; const decision = await strategy.route( @@ -51,6 +53,42 @@ describe('FallbackStrategy', () => { expect(decision?.metadata.reasoning).toContain('In fallback mode'); }); + it('should downgrade a preview auto model to the preview flash model', async () => { + const mockConfig = { + isInFallbackMode: () => true, + getModel: () => PREVIEW_GEMINI_MODEL_AUTO, + } as Config; + + const decision = await strategy.route( + mockContext, + mockConfig, + mockClient, + ); + + expect(decision).not.toBeNull(); + expect(decision?.model).toBe(PREVIEW_GEMINI_FLASH_MODEL); + expect(decision?.metadata.source).toBe('fallback'); + expect(decision?.metadata.reasoning).toContain('In fallback mode'); + }); + + it('should not downgrade a pro model to the flash model', async () => { + const mockConfig = { + isInFallbackMode: () => true, + getModel: () => DEFAULT_GEMINI_MODEL, + } as Config; + + const decision = await strategy.route( + mockContext, + mockConfig, + mockClient, + ); + + expect(decision).not.toBeNull(); + expect(decision?.model).toBe(DEFAULT_GEMINI_MODEL); + expect(decision?.metadata.source).toBe('fallback'); + expect(decision?.metadata.reasoning).toContain('In fallback mode'); + }); + it('should honor a lite model request', async () => { const mockConfig = { isInFallbackMode: () => true, diff --git a/packages/core/src/routing/strategies/fallbackStrategy.ts b/packages/core/src/routing/strategies/fallbackStrategy.ts index d61c92a5cb..130a279eb4 100644 --- a/packages/core/src/routing/strategies/fallbackStrategy.ts +++ b/packages/core/src/routing/strategies/fallbackStrategy.ts @@ -28,9 +28,8 @@ export class FallbackStrategy implements RoutingStrategy { } const effectiveModel = getEffectiveModel( - isInFallbackMode, config.getModel(), - config.getPreviewFeatures(), + isInFallbackMode, ); return { model: effectiveModel, diff --git a/packages/core/src/routing/strategies/overrideStrategy.ts b/packages/core/src/routing/strategies/overrideStrategy.ts index 79050c01e6..bf7b7914ab 100644 --- a/packages/core/src/routing/strategies/overrideStrategy.ts +++ b/packages/core/src/routing/strategies/overrideStrategy.ts @@ -7,7 +7,8 @@ import type { Config } from '../../config/config.js'; import { DEFAULT_GEMINI_MODEL_AUTO, - resolveModel, + getEffectiveModel, + PREVIEW_GEMINI_MODEL_AUTO, } from '../../config/models.js'; import type { BaseLlmClient } from '../../core/baseLlmClient.js'; import type { @@ -30,11 +31,15 @@ export class OverrideStrategy implements RoutingStrategy { const overrideModel = config.getModel(); // If the model is 'auto' we should pass to the next strategy. - if (overrideModel === DEFAULT_GEMINI_MODEL_AUTO) return null; + if ( + overrideModel === DEFAULT_GEMINI_MODEL_AUTO || + overrideModel === PREVIEW_GEMINI_MODEL_AUTO + ) + return null; // Return the overridden model name. return { - model: resolveModel(overrideModel, config.getPreviewFeatures()), + model: getEffectiveModel(overrideModel, false), metadata: { source: this.name, latencyMs: 0, diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index b0755ff281..5948079a6b 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1353,8 +1353,8 @@ "model": { "title": "Model", "description": "The model to use for the Codebase Investigator agent.", - "markdownDescription": "The model to use for the Codebase Investigator agent.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `pro`", - "default": "pro", + "markdownDescription": "The model to use for the Codebase Investigator agent.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `gemini-2.5-pro`", + "default": "gemini-2.5-pro", "type": "string" } },