diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 422f6cd2ac..995be3fc61 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1773,7 +1773,7 @@ describe('loadCliConfig model selection', () => { }); it('always prefers model from argv', async () => { - process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash-preview']; + process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash']; const argv = await parseArguments(createTestMergedSettings()); const config = await loadCliConfig( createTestMergedSettings({ @@ -1785,11 +1785,11 @@ describe('loadCliConfig model selection', () => { argv, ); - expect(config.getModel()).toBe('gemini-2.5-flash-preview'); + expect(config.getModel()).toBe('gemini-2.5-flash'); }); it('selects the model from argv if provided', async () => { - process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash-preview']; + process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash']; const argv = await parseArguments(createTestMergedSettings()); const config = await loadCliConfig( createTestMergedSettings({ @@ -1799,7 +1799,7 @@ describe('loadCliConfig model selection', () => { argv, ); - expect(config.getModel()).toBe('gemini-2.5-flash-preview'); + expect(config.getModel()).toBe('gemini-2.5-flash'); }); it('selects the default auto model if provided via auto alias', async () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 010fb8e17f..e910d47546 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -31,6 +31,8 @@ import { type HierarchicalMemory, coreEvents, GEMINI_MODEL_ALIAS_AUTO, + isValidModelOrAlias, + getValidModelsAndAliases, getAdminErrorMessage, isHeadlessMode, Config, @@ -671,6 +673,18 @@ export async function loadCliConfig( const specifiedModel = argv.model || process.env['GEMINI_MODEL'] || settings.model?.name; + // Validate the model if one was explicitly specified + if (specifiedModel && specifiedModel !== GEMINI_MODEL_ALIAS_AUTO) { + if (!isValidModelOrAlias(specifiedModel)) { + const validModels = getValidModelsAndAliases(); + + throw new FatalConfigError( + `Invalid model: "${specifiedModel}"\n\n` + + `Valid models and aliases:\n${validModels.map((m) => ` - ${m}`).join('\n')}\n\n` + + `Use /model to switch models interactively.`, + ); + } + } const resolvedModel = specifiedModel === GEMINI_MODEL_ALIAS_AUTO ? defaultModel diff --git a/packages/core/src/config/models.test.ts b/packages/core/src/config/models.test.ts index d62827ed91..b3f5db9430 100644 --- a/packages/core/src/config/models.test.ts +++ b/packages/core/src/config/models.test.ts @@ -22,6 +22,7 @@ import { GEMINI_MODEL_ALIAS_PRO, GEMINI_MODEL_ALIAS_FLASH, GEMINI_MODEL_ALIAS_AUTO, + GEMINI_MODEL_ALIAS_FLASH_LITE, PREVIEW_GEMINI_FLASH_MODEL, PREVIEW_GEMINI_MODEL_AUTO, DEFAULT_GEMINI_MODEL_AUTO, @@ -30,6 +31,10 @@ import { PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, isPreviewModel, isProModel, + isValidModelOrAlias, + getValidModelsAndAliases, + VALID_GEMINI_MODELS, + VALID_ALIASES, } from './models.js'; describe('isPreviewModel', () => { @@ -389,3 +394,62 @@ describe('isActiveModel', () => { ).toBe(false); }); }); + +describe('isValidModelOrAlias', () => { + it('should return true for valid model names', () => { + expect(isValidModelOrAlias(DEFAULT_GEMINI_MODEL)).toBe(true); + expect(isValidModelOrAlias(PREVIEW_GEMINI_MODEL)).toBe(true); + expect(isValidModelOrAlias(DEFAULT_GEMINI_FLASH_MODEL)).toBe(true); + expect(isValidModelOrAlias(DEFAULT_GEMINI_FLASH_LITE_MODEL)).toBe(true); + expect(isValidModelOrAlias(PREVIEW_GEMINI_FLASH_MODEL)).toBe(true); + expect(isValidModelOrAlias(PREVIEW_GEMINI_3_1_MODEL)).toBe(true); + expect(isValidModelOrAlias(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL)).toBe( + true, + ); + }); + + it('should return true for valid aliases', () => { + expect(isValidModelOrAlias(GEMINI_MODEL_ALIAS_AUTO)).toBe(true); + expect(isValidModelOrAlias(GEMINI_MODEL_ALIAS_PRO)).toBe(true); + expect(isValidModelOrAlias(GEMINI_MODEL_ALIAS_FLASH)).toBe(true); + expect(isValidModelOrAlias(GEMINI_MODEL_ALIAS_FLASH_LITE)).toBe(true); + expect(isValidModelOrAlias(PREVIEW_GEMINI_MODEL_AUTO)).toBe(true); + expect(isValidModelOrAlias(DEFAULT_GEMINI_MODEL_AUTO)).toBe(true); + }); + + it('should return true for custom (non-gemini) models', () => { + expect(isValidModelOrAlias('gpt-4')).toBe(true); + expect(isValidModelOrAlias('claude-3')).toBe(true); + expect(isValidModelOrAlias('my-custom-model')).toBe(true); + }); + + it('should return false for invalid gemini model names', () => { + expect(isValidModelOrAlias('gemini-4-pro')).toBe(false); + expect(isValidModelOrAlias('gemini-99-flash')).toBe(false); + expect(isValidModelOrAlias('gemini-invalid')).toBe(false); + }); +}); + +describe('getValidModelsAndAliases', () => { + it('should return a sorted array', () => { + const result = getValidModelsAndAliases(); + const sorted = [...result].sort(); + expect(result).toEqual(sorted); + }); + + it('should include all valid models and aliases', () => { + const result = getValidModelsAndAliases(); + for (const model of VALID_GEMINI_MODELS) { + expect(result).toContain(model); + } + for (const alias of VALID_ALIASES) { + expect(result).toContain(alias); + } + }); + + it('should not contain duplicates', () => { + const result = getValidModelsAndAliases(); + const unique = [...new Set(result)]; + expect(result).toEqual(unique); + }); +}); diff --git a/packages/core/src/config/models.ts b/packages/core/src/config/models.ts index ffbf597793..59e7e4b457 100644 --- a/packages/core/src/config/models.ts +++ b/packages/core/src/config/models.ts @@ -32,6 +32,15 @@ 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 VALID_ALIASES = new Set([ + GEMINI_MODEL_ALIAS_AUTO, + GEMINI_MODEL_ALIAS_PRO, + GEMINI_MODEL_ALIAS_FLASH, + GEMINI_MODEL_ALIAS_FLASH_LITE, + PREVIEW_GEMINI_MODEL_AUTO, + DEFAULT_GEMINI_MODEL_AUTO, +]); + export const DEFAULT_GEMINI_EMBEDDING_MODEL = 'gemini-embedding-001'; // Cap the thinking at 8192 to prevent run-away thinking loops. @@ -283,3 +292,37 @@ export function isActiveModel( ); } } + +/** + * Checks if the model name is valid (either a valid model or a valid alias). + * + * @param model The model name to check. + * @returns True if the model is valid. + */ +export function isValidModelOrAlias(model: string): boolean { + // Check if it's a valid alias + if (VALID_ALIASES.has(model)) { + return true; + } + + // Check if it's a valid model name + if (VALID_GEMINI_MODELS.has(model)) { + return true; + } + + // Allow custom models (non-gemini models) + if (!model.startsWith('gemini-')) { + return true; + } + + return false; +} + +/** + * Gets a list of all valid model names and aliases for error messages. + * + * @returns Array of valid model names and aliases. + */ +export function getValidModelsAndAliases(): string[] { + return [...new Set([...VALID_ALIASES, ...VALID_GEMINI_MODELS])].sort(); +}