mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Update models menu dialog to support manual model selection and group… (#80)
* Update models menu dialog to support manual model selection and group by model family * fix tests * update codebase investigator model setting * fix test * fix test * update generated golden file * regenerate scheme and doc for settings * use Preview Auto if previewFeatures is set to true
This commit is contained in:
committed by
Tommaso Sciortino
parent
c3f6e7132b
commit
17bf02b901
@@ -827,7 +827,7 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
|
|
||||||
- **`experimental.codebaseInvestigatorSettings.model`** (string):
|
- **`experimental.codebaseInvestigatorSettings.model`** (string):
|
||||||
- **Description:** The model to use for the Codebase Investigator agent.
|
- **Description:** The model to use for the Codebase Investigator agent.
|
||||||
- **Default:** `"pro"`
|
- **Default:** `"gemini-2.5-pro"`
|
||||||
- **Requires restart:** Yes
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
#### `hooks`
|
#### `hooks`
|
||||||
|
|||||||
@@ -1283,7 +1283,7 @@ describe('loadCliConfig model selection', () => {
|
|||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(config.getModel()).toBe('auto');
|
expect(config.getModel()).toBe('auto-gemini-2.5');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('always prefers model from argv', async () => {
|
it('always prefers model from argv', async () => {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
debugLogger,
|
debugLogger,
|
||||||
loadServerHierarchicalMemory,
|
loadServerHierarchicalMemory,
|
||||||
WEB_FETCH_TOOL_NAME,
|
WEB_FETCH_TOOL_NAME,
|
||||||
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { Settings } from './settings.js';
|
import type { Settings } from './settings.js';
|
||||||
|
|
||||||
@@ -569,7 +570,9 @@ export async function loadCliConfig(
|
|||||||
extraExcludes.length > 0 ? extraExcludes : undefined,
|
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 =
|
const resolvedModel: string =
|
||||||
argv.model ||
|
argv.model ||
|
||||||
process.env['GEMINI_MODEL'] ||
|
process.env['GEMINI_MODEL'] ||
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||||
DEFAULT_MODEL_CONFIGS,
|
DEFAULT_MODEL_CONFIGS,
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
DEFAULT_GEMINI_MODEL,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
import type { CustomTheme } from '../ui/themes/theme.js';
|
||||||
import type { SessionRetentionSettings } from './settings.js';
|
import type { SessionRetentionSettings } from './settings.js';
|
||||||
@@ -1384,7 +1384,7 @@ const SETTINGS_SCHEMA = {
|
|||||||
label: 'Model',
|
label: 'Model',
|
||||||
category: 'Experimental',
|
category: 'Experimental',
|
||||||
requiresRestart: true,
|
requiresRestart: true,
|
||||||
default: GEMINI_MODEL_ALIAS_PRO,
|
default: DEFAULT_GEMINI_MODEL,
|
||||||
description:
|
description:
|
||||||
'The model to use for the Codebase Investigator agent.',
|
'The model to use for the Codebase Investigator agent.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
|
|||||||
@@ -4,239 +4,186 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from '../../test-utils/render.js';
|
import { render } from 'ink-testing-library';
|
||||||
import { cleanup } from 'ink-testing-library';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
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 { ModelDialog } from './ModelDialog.js';
|
import { ModelDialog } from './ModelDialog.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
|
||||||
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
|
||||||
import { ConfigContext } from '../contexts/ConfigContext.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', () => ({
|
// Mock dependencies
|
||||||
useKeypress: vi.fn(),
|
const mockGetDisplayString = vi.fn();
|
||||||
}));
|
const mockLogModelSlashCommand = vi.fn();
|
||||||
const mockedUseKeypress = vi.mocked(useKeypress);
|
const mockModelSlashCommandEvent = vi.fn();
|
||||||
|
|
||||||
vi.mock('./shared/DescriptiveRadioButtonSelect.js', () => ({
|
|
||||||
DescriptiveRadioButtonSelect: vi.fn(() => null),
|
|
||||||
}));
|
|
||||||
const mockedSelect = vi.mocked(DescriptiveRadioButtonSelect);
|
|
||||||
|
|
||||||
const renderComponent = (
|
|
||||||
props: Partial<React.ComponentProps<typeof ModelDialog>> = {},
|
|
||||||
contextValue: Partial<Config> | 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(
|
|
||||||
<ConfigContext.Provider value={mockConfig}>
|
|
||||||
<ModelDialog {...combinedProps} />
|
|
||||||
</ConfigContext.Provider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
vi.mock('@google/gemini-cli-core', async () => {
|
||||||
|
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||||
return {
|
return {
|
||||||
...renderResult,
|
...actual,
|
||||||
props: combinedProps,
|
getDisplayString: (val: string) => mockGetDisplayString(val),
|
||||||
mockConfig,
|
logModelSlashCommand: (config: Config, event: ModelSlashCommandEvent) =>
|
||||||
|
mockLogModelSlashCommand(config, event),
|
||||||
|
ModelSlashCommandEvent: class {
|
||||||
|
constructor(model: string) {
|
||||||
|
mockModelSlashCommandEvent(model);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
|
|
||||||
describe('<ModelDialog />', () => {
|
describe('<ModelDialog />', () => {
|
||||||
|
const mockSetModel = vi.fn();
|
||||||
|
const mockGetModel = vi.fn();
|
||||||
|
const mockGetPreviewFeatures = vi.fn();
|
||||||
|
const mockOnClose = vi.fn();
|
||||||
|
|
||||||
|
interface MockConfig extends Partial<Config> {
|
||||||
|
setModel: (model: string) => void;
|
||||||
|
getModel: () => string;
|
||||||
|
getPreviewFeatures: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockConfig: MockConfig = {
|
||||||
|
setModel: mockSetModel,
|
||||||
|
getModel: mockGetModel,
|
||||||
|
getPreviewFeatures: mockGetPreviewFeatures,
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
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(() => {
|
const renderComponent = (contextValue = mockConfig as Config) =>
|
||||||
cleanup();
|
render(
|
||||||
});
|
<KeypressProvider>
|
||||||
|
<ConfigContext.Provider value={contextValue}>
|
||||||
|
<ModelDialog onClose={mockOnClose} />
|
||||||
|
</ConfigContext.Provider>
|
||||||
|
</KeypressProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
it('renders the title and help text', () => {
|
const waitForUpdate = () =>
|
||||||
const { lastFrame, unmount } = renderComponent();
|
new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
|
||||||
|
it('renders the initial "main" view correctly', () => {
|
||||||
|
const { lastFrame } = renderComponent();
|
||||||
expect(lastFrame()).toContain('Select Model');
|
expect(lastFrame()).toContain('Select Model');
|
||||||
expect(lastFrame()).toContain('(Press Esc to close)');
|
expect(lastFrame()).toContain('Auto');
|
||||||
expect(lastFrame()).toContain(
|
expect(lastFrame()).toContain('Manual');
|
||||||
'To use a specific Gemini model on startup, use the --model flag.',
|
|
||||||
);
|
|
||||||
unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes all model options to DescriptiveRadioButtonSelect', () => {
|
it('renders "main" view with preview options when preview features are enabled', () => {
|
||||||
const { unmount } = renderComponent();
|
mockGetPreviewFeatures.mockReturnValue(true);
|
||||||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
const { lastFrame } = renderComponent();
|
||||||
|
expect(lastFrame()).toContain('Auto (Preview)');
|
||||||
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('initializes with the model from ConfigContext', () => {
|
it('switches to "manual" view when "Manual" is selected', async () => {
|
||||||
const mockGetModel = vi.fn(() => GEMINI_MODEL_ALIAS_FLASH);
|
const { lastFrame, stdin } = renderComponent();
|
||||||
const { unmount } = renderComponent({}, { getModel: mockGetModel });
|
|
||||||
|
|
||||||
expect(mockGetModel).toHaveBeenCalled();
|
// Select "Manual" (index 1)
|
||||||
expect(mockedSelect).toHaveBeenCalledWith(
|
// Press down arrow to move to "Manual"
|
||||||
expect.objectContaining({
|
stdin.write('\u001B[B'); // Arrow Down
|
||||||
initialIndex: 2,
|
await waitForUpdate();
|
||||||
}),
|
|
||||||
undefined,
|
// Press enter to select
|
||||||
);
|
stdin.write('\r');
|
||||||
unmount();
|
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', () => {
|
it('renders "manual" view with preview options when preview features are enabled', async () => {
|
||||||
const { unmount } = renderComponent({}, undefined);
|
mockGetPreviewFeatures.mockReturnValue(true);
|
||||||
|
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
|
||||||
|
const { lastFrame, stdin } = renderComponent();
|
||||||
|
|
||||||
expect(mockedSelect).toHaveBeenCalledWith(
|
// Select "Manual" (index 2 because Preview Auto is first, then Auto (Gemini 2.5))
|
||||||
expect.objectContaining({
|
stdin.write('\u001B[B'); // Arrow Down (to Auto (Gemini 2.5))
|
||||||
initialIndex: 0,
|
await waitForUpdate();
|
||||||
}),
|
stdin.write('\u001B[B'); // Arrow Down (to Manual)
|
||||||
undefined,
|
await waitForUpdate();
|
||||||
);
|
|
||||||
unmount();
|
// 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', () => {
|
it('sets model and closes when a model is selected in "main" view', async () => {
|
||||||
const mockGetModel = vi.fn(() => undefined);
|
const { stdin } = renderComponent();
|
||||||
// @ts-expect-error This test validates component robustness when getModel
|
|
||||||
// returns an unexpected undefined value.
|
|
||||||
const { unmount } = renderComponent({}, { getModel: mockGetModel });
|
|
||||||
|
|
||||||
expect(mockGetModel).toHaveBeenCalled();
|
// Select "Auto" (index 0)
|
||||||
|
stdin.write('\r');
|
||||||
|
await waitForUpdate();
|
||||||
|
|
||||||
// When getModel returns undefined, preferredModel falls back to DEFAULT_GEMINI_MODEL_AUTO
|
expect(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL_AUTO);
|
||||||
// which has index 0, so initialIndex should be 0
|
expect(mockOnClose).toHaveBeenCalled();
|
||||||
expect(mockedSelect).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
initialIndex: 0,
|
|
||||||
}),
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
|
||||||
unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls config.setModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', () => {
|
it('sets model and closes when a model is selected in "manual" view', async () => {
|
||||||
const { props, mockConfig, unmount } = renderComponent({}, {}); // Pass empty object for contextValue
|
const { stdin } = renderComponent();
|
||||||
|
|
||||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
// Navigate to Manual (index 1) and select
|
||||||
expect(childOnSelect).toBeDefined();
|
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(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL);
|
||||||
expect(mockConfig?.setModel).toHaveBeenCalledWith(GEMINI_MODEL_ALIAS_PRO);
|
expect(mockOnClose).toHaveBeenCalled();
|
||||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
|
||||||
unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not pass onHighlight to DescriptiveRadioButtonSelect', () => {
|
it('closes dialog on escape in "main" view', async () => {
|
||||||
const { unmount } = renderComponent();
|
const { stdin } = renderComponent();
|
||||||
|
|
||||||
const childOnHighlight = mockedSelect.mock.calls[0][0].onHighlight;
|
stdin.write('\u001B'); // Escape
|
||||||
expect(childOnHighlight).toBeUndefined();
|
await waitForUpdate();
|
||||||
unmount();
|
|
||||||
|
expect(mockOnClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onClose prop when "escape" key is pressed', () => {
|
it('goes back to "main" view on escape in "manual" view', async () => {
|
||||||
const { props, unmount } = renderComponent();
|
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];
|
expect(lastFrame()).toContain(DEFAULT_GEMINI_MODEL);
|
||||||
const options = mockedUseKeypress.mock.calls[0][1];
|
|
||||||
|
|
||||||
expect(options).toEqual({ isActive: true });
|
// Press Escape
|
||||||
|
stdin.write('\u001B');
|
||||||
|
await waitForUpdate();
|
||||||
|
|
||||||
keyPressHandler({
|
expect(mockOnClose).not.toHaveBeenCalled();
|
||||||
name: 'escape',
|
// Should be back to main view (Manual option visible)
|
||||||
ctrl: false,
|
expect(lastFrame()).toContain('Manual');
|
||||||
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(
|
|
||||||
<ConfigContext.Provider value={oldMockConfig}>
|
|
||||||
<ModelDialog onClose={vi.fn()} />
|
|
||||||
</ConfigContext.Provider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
<ConfigContext.Provider value={newMockConfig}>
|
|
||||||
<ModelDialog onClose={vi.fn()} />
|
|
||||||
</ConfigContext.Provider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useCallback, useContext, useMemo } from 'react';
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import {
|
import {
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
DEFAULT_GEMINI_MODEL_AUTO,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
GEMINI_MODEL_ALIAS_FLASH,
|
|
||||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
|
||||||
ModelSlashCommandEvent,
|
ModelSlashCommandEvent,
|
||||||
logModelSlashCommand,
|
logModelSlashCommand,
|
||||||
|
getDisplayString,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
@@ -31,61 +31,128 @@ interface ModelDialogProps {
|
|||||||
|
|
||||||
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||||
const config = useContext(ConfigContext);
|
const config = useContext(ConfigContext);
|
||||||
|
const [view, setView] = useState<'main' | 'manual'>('main');
|
||||||
|
|
||||||
// Determine the Preferred Model (read once when the dialog opens).
|
// Determine the Preferred Model (read once when the dialog opens).
|
||||||
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
|
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(
|
useKeypress(
|
||||||
(key) => {
|
(key) => {
|
||||||
if (key.name === 'escape') {
|
if (key.name === 'escape') {
|
||||||
|
if (view === 'manual') {
|
||||||
|
setView('main');
|
||||||
|
} else {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ isActive: true },
|
{ isActive: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = useMemo(
|
const mainOptions = useMemo(() => {
|
||||||
() => [
|
const list = [
|
||||||
{
|
{
|
||||||
value: DEFAULT_GEMINI_MODEL_AUTO,
|
value: DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
title: 'Auto',
|
title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
|
||||||
description: 'Let the system choose the best model for your task.',
|
description:
|
||||||
|
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
|
||||||
key: DEFAULT_GEMINI_MODEL_AUTO,
|
key: DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: GEMINI_MODEL_ALIAS_PRO,
|
value: 'Manual',
|
||||||
title: config?.getPreviewFeatures()
|
title: manualModelSelected
|
||||||
? `Pro (${PREVIEW_GEMINI_MODEL}, ${DEFAULT_GEMINI_MODEL})`
|
? `Manual (${manualModelSelected})`
|
||||||
: `Pro (${DEFAULT_GEMINI_MODEL})`,
|
: '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:
|
description:
|
||||||
'For complex tasks that require deep reasoning and creativity',
|
'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
|
||||||
key: GEMINI_MODEL_ALIAS_PRO,
|
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,
|
value: DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
title: `Flash (${DEFAULT_GEMINI_FLASH_MODEL})`,
|
title: DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
description: 'For tasks that need a balance of speed and reasoning',
|
key: DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
key: GEMINI_MODEL_ALIAS_FLASH,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: GEMINI_MODEL_ALIAS_FLASH_LITE,
|
value: DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
title: `Flash-Lite (${DEFAULT_GEMINI_FLASH_LITE_MODEL})`,
|
title: DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
description: 'For simple tasks that need to be done quickly',
|
key: DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
key: GEMINI_MODEL_ALIAS_FLASH_LITE,
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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,
|
||||||
},
|
},
|
||||||
],
|
|
||||||
[config],
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const options = view === 'main' ? mainOptions : manualOptions;
|
||||||
|
|
||||||
// Calculate the initial index based on the preferred model.
|
// Calculate the initial index based on the preferred model.
|
||||||
const initialIndex = useMemo(
|
const initialIndex = useMemo(() => {
|
||||||
() => options.findIndex((option) => option.value === preferredModel),
|
const idx = options.findIndex((option) => option.value === preferredModel);
|
||||||
[preferredModel, options],
|
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).
|
// Handle selection internally (Autonomous Dialog).
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(model: string) => {
|
(model: string) => {
|
||||||
|
if (model === 'Manual') {
|
||||||
|
setView('manual');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (config) {
|
if (config) {
|
||||||
config.setModel(model);
|
config.setModel(model);
|
||||||
const event = new ModelSlashCommandEvent(model);
|
const event = new ModelSlashCommandEvent(model);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
|||||||
|
|
||||||
export interface DescriptiveRadioSelectItem<T> extends SelectionListItem<T> {
|
export interface DescriptiveRadioSelectItem<T> extends SelectionListItem<T> {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DescriptiveRadioButtonSelectProps<T> {
|
export interface DescriptiveRadioButtonSelectProps<T> {
|
||||||
@@ -62,7 +62,9 @@ export function DescriptiveRadioButtonSelect<T>({
|
|||||||
renderItem={(item, { titleColor }) => (
|
renderItem={(item, { titleColor }) => (
|
||||||
<Box flexDirection="column" key={item.key}>
|
<Box flexDirection="column" key={item.key}>
|
||||||
<Text color={titleColor}>{item.title}</Text>
|
<Text color={titleColor}>{item.title}</Text>
|
||||||
|
{item.description && (
|
||||||
<Text color={theme.text.secondary}>{item.description}</Text>
|
<Text color={theme.text.secondary}>{item.description}</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -256,9 +256,8 @@ export class Session {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const model = getEffectiveModel(
|
const model = getEffectiveModel(
|
||||||
this.config.isInFallbackMode(),
|
|
||||||
this.config.getModel(),
|
this.config.getModel(),
|
||||||
this.config.getPreviewFeatures(),
|
this.config.isInFallbackMode(),
|
||||||
);
|
);
|
||||||
const responseStream = await chat.sendMessageStream(
|
const responseStream = await chat.sendMessageStream(
|
||||||
{ model },
|
{ model },
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ export {
|
|||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
|
||||||
GEMINI_MODEL_ALIAS_FLASH,
|
|
||||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
|
||||||
} from './src/config/models.js';
|
} from './src/config/models.js';
|
||||||
export {
|
export {
|
||||||
serializeTerminalToObject,
|
serializeTerminalToObject,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
LS_TOOL_NAME,
|
LS_TOOL_NAME,
|
||||||
READ_FILE_TOOL_NAME,
|
READ_FILE_TOOL_NAME,
|
||||||
} from '../tools/tool-names.js';
|
} from '../tools/tool-names.js';
|
||||||
import { GEMINI_MODEL_ALIAS_PRO } from '../config/models.js';
|
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
|
||||||
|
|
||||||
describe('CodebaseInvestigatorAgent', () => {
|
describe('CodebaseInvestigatorAgent', () => {
|
||||||
it('should have the correct agent definition', () => {
|
it('should have the correct agent definition', () => {
|
||||||
@@ -26,7 +26,7 @@ describe('CodebaseInvestigatorAgent', () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(CodebaseInvestigatorAgent.outputConfig?.outputName).toBe('report');
|
expect(CodebaseInvestigatorAgent.outputConfig?.outputName).toBe('report');
|
||||||
expect(CodebaseInvestigatorAgent.modelConfig?.model).toBe(
|
expect(CodebaseInvestigatorAgent.modelConfig?.model).toBe(
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
DEFAULT_GEMINI_MODEL,
|
||||||
);
|
);
|
||||||
expect(CodebaseInvestigatorAgent.toolConfig?.tools).toEqual([
|
expect(CodebaseInvestigatorAgent.toolConfig?.tools).toEqual([
|
||||||
LS_TOOL_NAME,
|
LS_TOOL_NAME,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
LS_TOOL_NAME,
|
LS_TOOL_NAME,
|
||||||
READ_FILE_TOOL_NAME,
|
READ_FILE_TOOL_NAME,
|
||||||
} from '../tools/tool-names.js';
|
} 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';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Define a type that matches the outputConfig schema for type safety.
|
// 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),
|
processOutput: (output) => JSON.stringify(output, null, 2),
|
||||||
|
|
||||||
modelConfig: {
|
modelConfig: {
|
||||||
model: GEMINI_MODEL_ALIAS_PRO,
|
model: DEFAULT_GEMINI_MODEL,
|
||||||
temp: 0.1,
|
temp: 0.1,
|
||||||
top_p: 0.95,
|
top_p: 0.95,
|
||||||
thinkingBudget: -1,
|
thinkingBudget: -1,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { makeFakeConfig } from '../test-utils/config.js';
|
|||||||
import type { AgentDefinition } from './types.js';
|
import type { AgentDefinition } from './types.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.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.
|
// A test-only subclass to expose the protected `registerAgent` method.
|
||||||
class TestableAgentRegistry extends AgentRegistry {
|
class TestableAgentRegistry extends AgentRegistry {
|
||||||
@@ -78,7 +79,7 @@ describe('AgentRegistry', () => {
|
|||||||
model: 'gemini-3-pro-preview',
|
model: 'gemini-3-pro-preview',
|
||||||
codebaseInvestigatorSettings: {
|
codebaseInvestigatorSettings: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
model: 'pro',
|
model: DEFAULT_GEMINI_MODEL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const previewRegistry = new TestableAgentRegistry(previewConfig);
|
const previewRegistry = new TestableAgentRegistry(previewConfig);
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
|
|||||||
import { type z } from 'zod';
|
import { type z } from 'zod';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_GEMINI_MODEL_AUTO,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
} from '../config/models.js';
|
} from '../config/models.js';
|
||||||
import type { ModelConfigAlias } from '../services/modelConfigService.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 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 it's configured to use 'pro' or 'auto'.
|
||||||
if (this.config.getModel() === PREVIEW_GEMINI_MODEL) {
|
|
||||||
if (
|
if (
|
||||||
model === GEMINI_MODEL_ALIAS_PRO ||
|
this.config.getModel() === PREVIEW_GEMINI_MODEL ||
|
||||||
model === DEFAULT_GEMINI_MODEL_AUTO
|
this.config.getModel() === PREVIEW_GEMINI_MODEL_AUTO
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
model === PREVIEW_GEMINI_MODEL_AUTO ||
|
||||||
|
model === DEFAULT_GEMINI_MODEL_AUTO ||
|
||||||
|
model === DEFAULT_GEMINI_MODEL
|
||||||
) {
|
) {
|
||||||
model = PREVIEW_GEMINI_MODEL;
|
model = PREVIEW_GEMINI_MODEL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ export function resolvePolicyChain(
|
|||||||
// Switch to getActiveModel()
|
// Switch to getActiveModel()
|
||||||
const activeModel =
|
const activeModel =
|
||||||
preferredModel ??
|
preferredModel ??
|
||||||
getEffectiveModel(
|
getEffectiveModel(config.getModel(), config.isInFallbackMode());
|
||||||
config.isInFallbackMode(),
|
|
||||||
config.getModel(),
|
|
||||||
config.getPreviewFeatures(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeModel === 'auto') {
|
if (activeModel === 'auto') {
|
||||||
return [...chain];
|
return [...chain];
|
||||||
|
|||||||
@@ -7,220 +7,116 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
getEffectiveModel,
|
getEffectiveModel,
|
||||||
|
isGemini2Model,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
GEMINI_MODEL_ALIAS_FLASH,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
|
||||||
} from './models.js';
|
} from './models.js';
|
||||||
|
|
||||||
describe('getEffectiveModel', () => {
|
describe('getEffectiveModel', () => {
|
||||||
describe('When NOT in fallback mode', () => {
|
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(
|
const model = getEffectiveModel(
|
||||||
isInFallbackMode,
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
DEFAULT_GEMINI_MODEL,
|
useFallbackModel,
|
||||||
false,
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a custom model name when requested', () => {
|
|
||||||
const customModel = 'custom-model-v1';
|
|
||||||
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);
|
expect(model).toBe(PREVIEW_GEMINI_MODEL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the default pro model when pro alias is requested and preview is off', () => {
|
it('should return the Default Pro model when auto-default is requested', () => {
|
||||||
const model = getEffectiveModel(
|
const model = getEffectiveModel(
|
||||||
isInFallbackMode,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
useFallbackModel,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
expect(model).toBe(DEFAULT_GEMINI_MODEL);
|
expect(model).toBe(DEFAULT_GEMINI_MODEL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the flash model when flash is requested and preview is on', () => {
|
it('should return the requested model as-is for explicit specific models', () => {
|
||||||
const model = getEffectiveModel(
|
expect(getEffectiveModel(DEFAULT_GEMINI_MODEL, useFallbackModel)).toBe(
|
||||||
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,
|
DEFAULT_GEMINI_MODEL,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
expect(model).toBe(DEFAULT_GEMINI_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(customModel, useFallbackModel);
|
||||||
|
expect(model).toBe(customModel);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When IN fallback mode', () => {
|
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(
|
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,
|
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', () => {
|
it('should return custom model name as-is', () => {
|
||||||
const model = getEffectiveModel(
|
const customModel = 'custom-model-v1';
|
||||||
isInFallbackMode,
|
const model = getEffectiveModel(customModel, useFallbackModel);
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
expect(model).toBe(customModel);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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_MODEL = 'gemini-2.5-flash';
|
||||||
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
|
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 PREVIEW_GEMINI_MODEL_AUTO = 'auto-gemini-3';
|
||||||
export const GEMINI_MODEL_ALIAS_PRO = 'pro';
|
export const DEFAULT_GEMINI_MODEL_AUTO = 'auto-gemini-2.5';
|
||||||
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';
|
export const DEFAULT_GEMINI_EMBEDDING_MODEL = 'gemini-embedding-001';
|
||||||
|
|
||||||
// Cap the thinking at 8192 to prevent run-away thinking loops.
|
// Cap the thinking at 8192 to prevent run-away thinking loops.
|
||||||
export const DEFAULT_THINKING_MODE = 8192;
|
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.
|
* Determines the effective model to use, applying fallback logic if necessary.
|
||||||
*
|
*
|
||||||
* When fallback mode is active, this function enforces the use of the standard
|
* 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
|
* fallback 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.
|
|
||||||
*
|
*
|
||||||
* @param isInFallbackMode Whether the application is in fallback mode.
|
|
||||||
* @param requestedModel The model that was originally requested.
|
* @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.
|
* @returns The effective model name.
|
||||||
*/
|
*/
|
||||||
export function getEffectiveModel(
|
export function getEffectiveModel(
|
||||||
isInFallbackMode: boolean,
|
|
||||||
requestedModel: string,
|
requestedModel: string,
|
||||||
previewFeaturesEnabled: boolean | undefined,
|
useFallbackModel: boolean,
|
||||||
): string {
|
): string {
|
||||||
const resolvedModel = resolveModel(requestedModel, previewFeaturesEnabled);
|
|
||||||
|
|
||||||
// If we are not in fallback mode, simply use the resolved model.
|
// If we are not in fallback mode, simply use the resolved model.
|
||||||
if (!isInFallbackMode) {
|
if (!useFallbackModel) {
|
||||||
return resolvedModel;
|
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
|
// Fallback model for corresponding model family. We are doing fallback only
|
||||||
// lite models without needing to list them all as constants.
|
// for Auto modes
|
||||||
if (resolvedModel.includes('lite')) {
|
switch (requestedModel) {
|
||||||
return resolvedModel;
|
case PREVIEW_GEMINI_MODEL_AUTO:
|
||||||
}
|
return PREVIEW_GEMINI_FLASH_MODEL;
|
||||||
|
case DEFAULT_GEMINI_MODEL_AUTO:
|
||||||
// Default fallback for Gemini CLI.
|
|
||||||
return DEFAULT_GEMINI_FLASH_MODEL;
|
return DEFAULT_GEMINI_FLASH_MODEL;
|
||||||
|
default:
|
||||||
|
return requestedModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -396,11 +396,7 @@ export class GeminiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configModel = this.config.getModel();
|
const configModel = this.config.getModel();
|
||||||
return getEffectiveModel(
|
return getEffectiveModel(configModel, this.config.isInFallbackMode());
|
||||||
this.config.isInFallbackMode(),
|
|
||||||
configModel,
|
|
||||||
this.config.getPreviewFeatures(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async *sendMessageStream(
|
async *sendMessageStream(
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import {
|
|||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_THINKING_MODE,
|
DEFAULT_THINKING_MODE,
|
||||||
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
PREVIEW_GEMINI_MODEL,
|
PREVIEW_GEMINI_MODEL,
|
||||||
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
} from '../config/models.js';
|
} from '../config/models.js';
|
||||||
import { AuthType } from './contentGenerator.js';
|
import { AuthType } from './contentGenerator.js';
|
||||||
import { TerminalQuotaError } from '../utils/googleQuotaErrors.js';
|
import { TerminalQuotaError } from '../utils/googleQuotaErrors.js';
|
||||||
@@ -155,7 +157,8 @@ describe('GeminiChat', () => {
|
|||||||
getUserTier: vi.fn().mockReturnValue(undefined),
|
getUserTier: vi.fn().mockReturnValue(undefined),
|
||||||
modelConfigService: {
|
modelConfigService: {
|
||||||
getResolvedConfig: vi.fn().mockImplementation((modelConfigKey) => {
|
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,
|
thinkingLevel: ThinkingLevel.HIGH,
|
||||||
}
|
}
|
||||||
@@ -163,7 +166,7 @@ describe('GeminiChat', () => {
|
|||||||
thinkingBudget: DEFAULT_THINKING_MODE,
|
thinkingBudget: DEFAULT_THINKING_MODE,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
model: modelConfigKey.model,
|
model,
|
||||||
generateContentConfig: {
|
generateContentConfig: {
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
thinkingConfig,
|
thinkingConfig,
|
||||||
@@ -1859,7 +1862,7 @@ describe('GeminiChat', () => {
|
|||||||
} as unknown as GenerateContentResponse;
|
} as unknown as GenerateContentResponse;
|
||||||
|
|
||||||
it('should use the FLASH model when in fallback mode (sendMessageStream)', async () => {
|
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(mockConfig.isInFallbackMode).mockReturnValue(true);
|
||||||
vi.mocked(mockContentGenerator.generateContentStream).mockImplementation(
|
vi.mocked(mockContentGenerator.generateContentStream).mockImplementation(
|
||||||
async () =>
|
async () =>
|
||||||
@@ -1869,7 +1872,7 @@ describe('GeminiChat', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const stream = await chat.sendMessageStream(
|
const stream = await chat.sendMessageStream(
|
||||||
{ model: 'test-model' },
|
{ model: 'auto-gemini-2.5' },
|
||||||
'test message',
|
'test message',
|
||||||
'prompt-id-res3',
|
'prompt-id-res3',
|
||||||
new AbortController().signal,
|
new AbortController().signal,
|
||||||
@@ -1983,7 +1986,7 @@ describe('GeminiChat', () => {
|
|||||||
expect(modelTurn.parts![0]!.text).toBe('Success on retry');
|
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
|
// ARRANGE
|
||||||
const authType = AuthType.LOGIN_WITH_GOOGLE;
|
const authType = AuthType.LOGIN_WITH_GOOGLE;
|
||||||
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
||||||
@@ -2020,7 +2023,7 @@ describe('GeminiChat', () => {
|
|||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const stream = await chat.sendMessageStream(
|
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',
|
'test fallback thinking',
|
||||||
'prompt-id-fb3',
|
'prompt-id-fb3',
|
||||||
new AbortController().signal,
|
new AbortController().signal,
|
||||||
@@ -2040,7 +2043,7 @@ describe('GeminiChat', () => {
|
|||||||
).toHaveBeenNthCalledWith(
|
).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
model: 'gemini-3-test-model',
|
model: PREVIEW_GEMINI_MODEL,
|
||||||
config: expect.objectContaining({
|
config: expect.objectContaining({
|
||||||
thinkingConfig: {
|
thinkingConfig: {
|
||||||
thinkingBudget: undefined,
|
thinkingBudget: undefined,
|
||||||
@@ -2051,17 +2054,17 @@ describe('GeminiChat', () => {
|
|||||||
'prompt-id-fb3',
|
'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(
|
expect(
|
||||||
mockContentGenerator.generateContentStream,
|
mockContentGenerator.generateContentStream,
|
||||||
).toHaveBeenNthCalledWith(
|
).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
model: DEFAULT_GEMINI_FLASH_MODEL,
|
model: PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
config: expect.objectContaining({
|
config: expect.objectContaining({
|
||||||
thinkingConfig: {
|
thinkingConfig: {
|
||||||
thinkingBudget: DEFAULT_THINKING_MODE,
|
thinkingBudget: undefined,
|
||||||
thinkingLevel: undefined,
|
thinkingLevel: ThinkingLevel.HIGH,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -460,11 +460,7 @@ export class GeminiChat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
modelToUse = getEffectiveModel(
|
modelToUse = getEffectiveModel(model, this.config.isInFallbackMode());
|
||||||
this.config.isInFallbackMode(),
|
|
||||||
model,
|
|
||||||
this.config.getPreviewFeatures(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Preview Model Bypass Logic:
|
// Preview Model Bypass Logic:
|
||||||
// If we are in "Preview Model Bypass Mode" (transient failure), we force downgrade to 2.5 Pro
|
// If we are in "Preview Model Bypass Mode" (transient failure), we force downgrade to 2.5 Pro
|
||||||
|
|||||||
@@ -106,9 +106,8 @@ export function getCoreSystemPrompt(
|
|||||||
|
|
||||||
// TODO(joshualitt): Replace with system instructions on model configs.
|
// TODO(joshualitt): Replace with system instructions on model configs.
|
||||||
const desiredModel = getEffectiveModel(
|
const desiredModel = getEffectiveModel(
|
||||||
config.isInFallbackMode(),
|
|
||||||
config.getModel(),
|
config.getModel(),
|
||||||
config.getPreviewFeatures(),
|
config.isInFallbackMode(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isGemini3 = desiredModel === PREVIEW_GEMINI_MODEL;
|
const isGemini3 = desiredModel === PREVIEW_GEMINI_MODEL;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
} from '../../config/models.js';
|
} from '../../config/models.js';
|
||||||
import { promptIdContext } from '../../utils/promptIdContext.js';
|
import { promptIdContext } from '../../utils/promptIdContext.js';
|
||||||
import type { Content } from '@google/genai';
|
import type { Content } from '@google/genai';
|
||||||
@@ -50,6 +51,7 @@ describe('ClassifierStrategy', () => {
|
|||||||
modelConfigService: {
|
modelConfigService: {
|
||||||
getResolvedConfig: vi.fn().mockReturnValue(mockResolvedConfig),
|
getResolvedConfig: vi.fn().mockReturnValue(mockResolvedConfig),
|
||||||
},
|
},
|
||||||
|
getModel: () => DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
getPreviewFeatures: () => false,
|
getPreviewFeatures: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
mockBaseLlmClient = {
|
mockBaseLlmClient = {
|
||||||
|
|||||||
@@ -12,11 +12,7 @@ import type {
|
|||||||
RoutingDecision,
|
RoutingDecision,
|
||||||
RoutingStrategy,
|
RoutingStrategy,
|
||||||
} from '../routingStrategy.js';
|
} from '../routingStrategy.js';
|
||||||
import {
|
import { getEffectiveModel } from '../../config/models.js';
|
||||||
GEMINI_MODEL_ALIAS_FLASH,
|
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
|
||||||
resolveModel,
|
|
||||||
} from '../../config/models.js';
|
|
||||||
import { createUserContent, Type } from '@google/genai';
|
import { createUserContent, Type } from '@google/genai';
|
||||||
import type { Config } from '../../config/config.js';
|
import type { Config } from '../../config/config.js';
|
||||||
import {
|
import {
|
||||||
@@ -174,10 +170,7 @@ export class ClassifierStrategy implements RoutingStrategy {
|
|||||||
|
|
||||||
if (routerResponse.model_choice === FLASH_MODEL) {
|
if (routerResponse.model_choice === FLASH_MODEL) {
|
||||||
return {
|
return {
|
||||||
model: resolveModel(
|
model: getEffectiveModel(config.getModel(), true),
|
||||||
GEMINI_MODEL_ALIAS_FLASH,
|
|
||||||
config.getPreviewFeatures(),
|
|
||||||
),
|
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier',
|
source: 'Classifier',
|
||||||
latencyMs,
|
latencyMs,
|
||||||
@@ -186,10 +179,7 @@ export class ClassifierStrategy implements RoutingStrategy {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
model: resolveModel(
|
model: getEffectiveModel(config.getModel(), false),
|
||||||
GEMINI_MODEL_ALIAS_PRO,
|
|
||||||
config.getPreviewFeatures(),
|
|
||||||
),
|
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier',
|
source: 'Classifier',
|
||||||
reasoning,
|
reasoning,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import {
|
|||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||||
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
|
PREVIEW_GEMINI_FLASH_MODEL,
|
||||||
} from '../../config/models.js';
|
} from '../../config/models.js';
|
||||||
|
|
||||||
describe('FallbackStrategy', () => {
|
describe('FallbackStrategy', () => {
|
||||||
@@ -32,11 +35,10 @@ describe('FallbackStrategy', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when in fallback mode', () => {
|
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 = {
|
const mockConfig = {
|
||||||
isInFallbackMode: () => true,
|
isInFallbackMode: () => true,
|
||||||
getModel: () => DEFAULT_GEMINI_MODEL,
|
getModel: () => DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
getPreviewFeatures: () => false,
|
|
||||||
} as Config;
|
} as Config;
|
||||||
|
|
||||||
const decision = await strategy.route(
|
const decision = await strategy.route(
|
||||||
@@ -51,6 +53,42 @@ describe('FallbackStrategy', () => {
|
|||||||
expect(decision?.metadata.reasoning).toContain('In fallback mode');
|
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 () => {
|
it('should honor a lite model request', async () => {
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isInFallbackMode: () => true,
|
isInFallbackMode: () => true,
|
||||||
|
|||||||
@@ -28,9 +28,8 @@ export class FallbackStrategy implements RoutingStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const effectiveModel = getEffectiveModel(
|
const effectiveModel = getEffectiveModel(
|
||||||
isInFallbackMode,
|
|
||||||
config.getModel(),
|
config.getModel(),
|
||||||
config.getPreviewFeatures(),
|
isInFallbackMode,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
model: effectiveModel,
|
model: effectiveModel,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
import type { Config } from '../../config/config.js';
|
import type { Config } from '../../config/config.js';
|
||||||
import {
|
import {
|
||||||
DEFAULT_GEMINI_MODEL_AUTO,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
resolveModel,
|
getEffectiveModel,
|
||||||
|
PREVIEW_GEMINI_MODEL_AUTO,
|
||||||
} from '../../config/models.js';
|
} from '../../config/models.js';
|
||||||
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
|
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -30,11 +31,15 @@ export class OverrideStrategy implements RoutingStrategy {
|
|||||||
const overrideModel = config.getModel();
|
const overrideModel = config.getModel();
|
||||||
|
|
||||||
// If the model is 'auto' we should pass to the next strategy.
|
// 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 the overridden model name.
|
||||||
return {
|
return {
|
||||||
model: resolveModel(overrideModel, config.getPreviewFeatures()),
|
model: getEffectiveModel(overrideModel, false),
|
||||||
metadata: {
|
metadata: {
|
||||||
source: this.name,
|
source: this.name,
|
||||||
latencyMs: 0,
|
latencyMs: 0,
|
||||||
|
|||||||
@@ -1353,8 +1353,8 @@
|
|||||||
"model": {
|
"model": {
|
||||||
"title": "Model",
|
"title": "Model",
|
||||||
"description": "The model to use for the Codebase Investigator agent.",
|
"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`",
|
"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": "pro",
|
"default": "gemini-2.5-pro",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user