From 0bf7ea60c553d659791ae1ff0c74f9d2c88feac5 Mon Sep 17 00:00:00 2001 From: kevinjwang1 Date: Sat, 14 Mar 2026 14:45:21 -0700 Subject: [PATCH] Add ModelDefinitions to ModelConfigService (#22302) --- docs/reference/configuration.md | 141 ++++++++ packages/cli/src/acp/acpClient.ts | 2 + packages/cli/src/config/config.ts | 1 + packages/cli/src/config/settingsSchema.ts | 43 +++ .../cli/src/ui/components/StatsDisplay.tsx | 23 +- .../agents/browser/browserAgentDefinition.ts | 2 +- .../core/src/availability/policyHelpers.ts | 10 +- packages/core/src/config/config.ts | 81 +++-- .../core/src/config/defaultModelConfigs.ts | 90 ++++++ packages/core/src/config/models.test.ts | 90 ++++++ packages/core/src/config/models.ts | 110 ++++++- .../strategies/approvalModeStrategy.ts | 2 +- .../routing/strategies/classifierStrategy.ts | 2 +- .../strategies/numericalClassifierStrategy.ts | 2 +- .../routing/strategies/overrideStrategy.ts | 2 +- packages/core/src/scheduler/tool-executor.ts | 2 + .../core/src/services/modelConfigService.ts | 45 +++ .../utils/generateContentResponseUtilities.ts | 7 +- schemas/settings.schema.json | 305 +++++++++++++++++- 19 files changed, 904 insertions(+), 56 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 8ef25767c5..01aaea676f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -677,6 +677,141 @@ their corresponding top-level category object in your `settings.json` file. used. - **Default:** `[]` +- **`modelConfigs.modelDefinitions`** (object): + - **Description:** Registry of model metadata, including tier, family, and + features. + - **Default:** + + ```json + { + "gemini-3.1-pro-preview": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3.1-pro-preview-customtools": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3-pro-preview": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3-flash-preview": { + "tier": "flash", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": true + } + }, + "gemini-2.5-pro": { + "tier": "pro", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "gemini-2.5-flash": { + "tier": "flash", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "gemini-2.5-flash-lite": { + "tier": "flash-lite", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "auto": { + "tier": "auto", + "isPreview": true, + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "pro": { + "tier": "pro", + "isPreview": false, + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "flash": { + "tier": "flash", + "isPreview": false, + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "flash-lite": { + "tier": "flash-lite", + "isPreview": false, + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "auto-gemini-3": { + "displayName": "Auto (Gemini 3)", + "tier": "auto", + "isPreview": true, + "dialogLocation": "main", + "dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash", + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "auto-gemini-2.5": { + "displayName": "Auto (Gemini 2.5)", + "tier": "auto", + "isPreview": false, + "dialogLocation": "main", + "dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash", + "features": { + "thinking": false, + "multimodalToolUse": false + } + } + } + ``` + + - **Requires restart:** Yes + #### `agents` - **`agents.overrides`** (object): @@ -1091,6 +1226,12 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `false` - **Requires restart:** Yes +- **`experimental.dynamicModelConfiguration`** (boolean): + - **Description:** Enable dynamic model configuration (definitions, + resolutions, and chains) via settings. + - **Default:** `false` + - **Requires restart:** Yes + - **`experimental.gemmaModelRouter.enabled`** (boolean): - **Description:** Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. diff --git a/packages/cli/src/acp/acpClient.ts b/packages/cli/src/acp/acpClient.ts index db2d04dab4..072d91c20a 100644 --- a/packages/cli/src/acp/acpClient.ts +++ b/packages/cli/src/acp/acpClient.ts @@ -1004,6 +1004,7 @@ export class Session { callId, toolResult.llmContent, this.config.getActiveModel(), + this.config, ), resultDisplay: toolResult.returnDisplay, error: undefined, @@ -1017,6 +1018,7 @@ export class Session { callId, toolResult.llmContent, this.config.getActiveModel(), + this.config, ); } catch (e) { const error = e instanceof Error ? e : new Error(String(e)); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index f89f464ba3..ab6a22fb64 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -849,6 +849,7 @@ export async function loadCliConfig( disableLLMCorrection: settings.tools?.disableLLMCorrection, rawOutput: argv.rawOutput, acceptRawOutputRisk: argv.acceptRawOutputRisk, + dynamicModelConfiguration: settings.experimental?.dynamicModelConfiguration, modelConfigServiceConfig: settings.modelConfigs, // TODO: loading of hooks based on workspace trust enableHooks: settings.hooksConfig.enabled, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 657d7f61d3..87fbe98fc3 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1039,6 +1039,20 @@ const SETTINGS_SCHEMA = { 'Apply specific configuration overrides based on matches, with a primary key of model (or alias). The most specific match will be used.', showInDialog: false, }, + modelDefinitions: { + type: 'object', + label: 'Model Definitions', + category: 'Model', + requiresRestart: true, + default: DEFAULT_MODEL_CONFIGS.modelDefinitions, + description: + 'Registry of model metadata, including tier, family, and features.', + showInDialog: false, + additionalProperties: { + type: 'object', + ref: 'ModelDefinition', + }, + }, }, }, @@ -1943,6 +1957,16 @@ const SETTINGS_SCHEMA = { 'Enable web fetch behavior that bypasses LLM summarization.', showInDialog: true, }, + dynamicModelConfiguration: { + type: 'boolean', + label: 'Dynamic Model Configuration', + category: 'Experimental', + requiresRestart: true, + default: false, + description: + 'Enable dynamic model configuration (definitions, resolutions, and chains) via settings.', + showInDialog: false, + }, gemmaModelRouter: { type: 'object', label: 'Gemma Model Router', @@ -2769,6 +2793,25 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record< }, }, }, + ModelDefinition: { + type: 'object', + description: 'Model metadata registry entry.', + properties: { + displayName: { type: 'string' }, + tier: { enum: ['pro', 'flash', 'flash-lite', 'custom', 'auto'] }, + family: { type: 'string' }, + isPreview: { type: 'boolean' }, + dialogLocation: { enum: ['main', 'manual'] }, + dialogDescription: { type: 'string' }, + features: { + type: 'object', + properties: { + thinking: { type: 'boolean' }, + multimodalToolUse: { type: 'boolean' }, + }, + }, + }, + }, }; export function getSettingsSchema(): SettingsSchemaType { diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 320203f3dc..9effb39b5c 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -27,6 +27,7 @@ import { } from '../utils/displayUtils.js'; import { computeSessionStats } from '../utils/computeStats.js'; import { + type Config, type RetrieveUserQuotaResponse, isActiveModel, getDisplayString, @@ -88,13 +89,16 @@ const Section: React.FC = ({ title, children }) => ( // Logic for building the unified list of table rows const buildModelRows = ( models: Record, + config: Config, quotas?: RetrieveUserQuotaResponse, useGemini3_1 = false, useCustomToolModel = false, ) => { const getBaseModelName = (name: string) => name.replace('-001', ''); const usedModelNames = new Set( - Object.keys(models).map(getBaseModelName).map(getDisplayString), + Object.keys(models) + .map(getBaseModelName) + .map((name) => getDisplayString(name, config)), ); // 1. Models with active usage @@ -104,7 +108,7 @@ const buildModelRows = ( const inputTokens = metrics.tokens.input; return { key: name, - modelName: getDisplayString(modelName), + modelName: getDisplayString(modelName, config), requests: metrics.api.totalRequests, cachedTokens: cachedTokens.toLocaleString(), inputTokens: inputTokens.toLocaleString(), @@ -121,11 +125,11 @@ const buildModelRows = ( (b) => b.modelId && isActiveModel(b.modelId, useGemini3_1, useCustomToolModel) && - !usedModelNames.has(getDisplayString(b.modelId)), + !usedModelNames.has(getDisplayString(b.modelId, config)), ) .map((bucket) => ({ key: bucket.modelId!, - modelName: getDisplayString(bucket.modelId!), + modelName: getDisplayString(bucket.modelId!, config), requests: '-', cachedTokens: '-', inputTokens: '-', @@ -139,6 +143,7 @@ const buildModelRows = ( const ModelUsageTable: React.FC<{ models: Record; + config: Config; quotas?: RetrieveUserQuotaResponse; cacheEfficiency: number; totalCachedTokens: number; @@ -150,6 +155,7 @@ const ModelUsageTable: React.FC<{ useCustomToolModel?: boolean; }> = ({ models, + config, quotas, cacheEfficiency, totalCachedTokens, @@ -162,7 +168,13 @@ const ModelUsageTable: React.FC<{ }) => { const { stdout } = useStdout(); const terminalWidth = stdout?.columns ?? 84; - const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel); + const rows = buildModelRows( + models, + config, + quotas, + useGemini3_1, + useCustomToolModel, + ); if (rows.length === 0) { return null; @@ -676,6 +688,7 @@ export const StatsDisplay: React.FC = ({ => { // Use Preview Flash model if the main model is any of the preview models. // If the main model is not a preview model, use the default flash model. - const model = isPreviewModel(config.getModel()) + const model = isPreviewModel(config.getModel(), config) ? PREVIEW_GEMINI_FLASH_MODEL : DEFAULT_GEMINI_FLASH_MODEL; diff --git a/packages/core/src/availability/policyHelpers.ts b/packages/core/src/availability/policyHelpers.ts index 406abde5e3..290c47d896 100644 --- a/packages/core/src/availability/policyHelpers.ts +++ b/packages/core/src/availability/policyHelpers.ts @@ -54,19 +54,21 @@ export function resolvePolicyChain( useCustomToolModel, hasAccessToPreview, ); - const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false; - const isAutoConfigured = isAutoModel(configuredModel); + const isAutoPreferred = preferredModel + ? isAutoModel(preferredModel, config) + : false; + const isAutoConfigured = isAutoModel(configuredModel, config); if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) { chain = getFlashLitePolicyChain(); } else if ( - isGemini3Model(resolvedModel) || + isGemini3Model(resolvedModel, config) || isAutoPreferred || isAutoConfigured ) { if (hasAccessToPreview) { const previewEnabled = - isGemini3Model(resolvedModel) || + isGemini3Model(resolvedModel, config) || preferredModel === PREVIEW_GEMINI_MODEL_AUTO || configuredModel === PREVIEW_GEMINI_MODEL_AUTO; chain = getModelPolicyChain({ diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 077e13101b..31c2128f31 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -609,6 +609,7 @@ export interface ConfigParameters { disableAlwaysAllow?: boolean; rawOutput?: boolean; acceptRawOutputRisk?: boolean; + dynamicModelConfiguration?: boolean; modelConfigServiceConfig?: ModelConfigServiceConfig; enableHooks?: boolean; enableHooksUI?: boolean; @@ -810,6 +811,7 @@ export class Config implements McpContext, AgentLoopContext { private readonly disableAlwaysAllow: boolean; private readonly rawOutput: boolean; private readonly acceptRawOutputRisk: boolean; + private readonly dynamicModelConfiguration: boolean; private pendingIncludeDirectories: string[]; private readonly enableHooks: boolean; private readonly enableHooksUI: boolean; @@ -957,6 +959,40 @@ export class Config implements McpContext, AgentLoopContext { this.disabledSkills = params.disabledSkills ?? []; this.adminSkillsEnabled = params.adminSkillsEnabled ?? true; this.modelAvailabilityService = new ModelAvailabilityService(); + this.dynamicModelConfiguration = params.dynamicModelConfiguration ?? false; + + // HACK: The settings loading logic doesn't currently merge the default + // generation config with the user's settings. This means if a user provides + // any `generation` settings (e.g., just `overrides`), the default `aliases` + // are lost. This hack manually merges the default aliases back in if they + // are missing from the user's config. + // TODO(12593): Fix the settings loading logic to properly merge defaults and + // remove this hack. + let modelConfigServiceConfig = params.modelConfigServiceConfig; + if (modelConfigServiceConfig) { + // Ensure user-defined model definitions augment, not replace, the defaults. + const mergedModelDefinitions = { + ...DEFAULT_MODEL_CONFIGS.modelDefinitions, + ...modelConfigServiceConfig.modelDefinitions, + }; + + modelConfigServiceConfig = { + // Preserve other user settings like customAliases + ...modelConfigServiceConfig, + // Apply defaults for aliases and overrides if they are not provided + aliases: + modelConfigServiceConfig.aliases ?? DEFAULT_MODEL_CONFIGS.aliases, + overrides: + modelConfigServiceConfig.overrides ?? DEFAULT_MODEL_CONFIGS.overrides, + // Use the merged model definitions + modelDefinitions: mergedModelDefinitions, + }; + } + + this.modelConfigService = new ModelConfigService( + modelConfigServiceConfig ?? DEFAULT_MODEL_CONFIGS, + ); + this.experimentalJitContext = params.experimentalJitContext ?? false; this.topicUpdateNarration = params.topicUpdateNarration ?? false; this.modelSteering = params.modelSteering ?? false; @@ -1013,7 +1049,7 @@ export class Config implements McpContext, AgentLoopContext { this.truncateToolOutputThreshold = params.truncateToolOutputThreshold ?? DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD; - this.useWriteTodos = isPreviewModel(this.model) + this.useWriteTodos = isPreviewModel(this.model, this) ? false : (params.useWriteTodos ?? true); this.workspacePoliciesDir = params.workspacePoliciesDir; @@ -1131,33 +1167,6 @@ export class Config implements McpContext, AgentLoopContext { this._sandboxManager = createSandboxManager(params.toolSandboxing ?? false); this.shellExecutionConfig.sandboxManager = this._sandboxManager; this.modelRouterService = new ModelRouterService(this); - - // HACK: The settings loading logic doesn't currently merge the default - // generation config with the user's settings. This means if a user provides - // any `generation` settings (e.g., just `overrides`), the default `aliases` - // are lost. This hack manually merges the default aliases back in if they - // are missing from the user's config. - // TODO(12593): Fix the settings loading logic to properly merge defaults and - // remove this hack. - let modelConfigServiceConfig = params.modelConfigServiceConfig; - if (modelConfigServiceConfig) { - if (!modelConfigServiceConfig.aliases) { - modelConfigServiceConfig = { - ...modelConfigServiceConfig, - aliases: DEFAULT_MODEL_CONFIGS.aliases, - }; - } - if (!modelConfigServiceConfig.overrides) { - modelConfigServiceConfig = { - ...modelConfigServiceConfig, - overrides: DEFAULT_MODEL_CONFIGS.overrides, - }; - } - } - - this.modelConfigService = new ModelConfigService( - modelConfigServiceConfig ?? DEFAULT_MODEL_CONFIGS, - ); } get config(): Config { @@ -1355,7 +1364,10 @@ export class Config implements McpContext, AgentLoopContext { // Only reset when we have explicit "no access" (hasAccessToPreviewModel === false). // When null (quota not fetched) or true, we preserve the saved model. - if (isPreviewModel(this.model) && this.hasAccessToPreviewModel === false) { + if ( + isPreviewModel(this.model, this) && + this.hasAccessToPreviewModel === false + ) { this.setModel(DEFAULT_GEMINI_MODEL_AUTO); } @@ -1627,7 +1639,7 @@ export class Config implements McpContext, AgentLoopContext { const isPreview = model === PREVIEW_GEMINI_MODEL_AUTO || - isPreviewModel(this.getActiveModel()); + isPreviewModel(this.getActiveModel(), this); const proModel = isPreview ? PREVIEW_GEMINI_MODEL : DEFAULT_GEMINI_MODEL; const flashModel = isPreview ? PREVIEW_GEMINI_FLASH_MODEL @@ -1825,8 +1837,9 @@ export class Config implements McpContext, AgentLoopContext { } const hasAccess = - quota.buckets?.some((b) => b.modelId && isPreviewModel(b.modelId)) ?? - false; + quota.buckets?.some( + (b) => b.modelId && isPreviewModel(b.modelId, this), + ) ?? false; this.setHasAccessToPreviewModel(hasAccess); return quota; } catch (e) { @@ -2226,6 +2239,10 @@ export class Config implements McpContext, AgentLoopContext { return this.acceptRawOutputRisk; } + getExperimentalDynamicModelConfiguration(): boolean { + return this.dynamicModelConfiguration; + } + getPendingIncludeDirectories(): string[] { return this.pendingIncludeDirectories; } diff --git a/packages/core/src/config/defaultModelConfigs.ts b/packages/core/src/config/defaultModelConfigs.ts index 5344aa4421..c0e8b6c6ba 100644 --- a/packages/core/src/config/defaultModelConfigs.ts +++ b/packages/core/src/config/defaultModelConfigs.ts @@ -249,4 +249,94 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = { }, }, ], + modelDefinitions: { + // Concrete Models + 'gemini-3.1-pro-preview': { + tier: 'pro', + family: 'gemini-3', + isPreview: true, + dialogLocation: 'manual', + features: { thinking: true, multimodalToolUse: true }, + }, + 'gemini-3.1-pro-preview-customtools': { + tier: 'pro', + family: 'gemini-3', + isPreview: true, + features: { thinking: true, multimodalToolUse: true }, + }, + 'gemini-3-pro-preview': { + tier: 'pro', + family: 'gemini-3', + isPreview: true, + dialogLocation: 'manual', + features: { thinking: true, multimodalToolUse: true }, + }, + 'gemini-3-flash-preview': { + tier: 'flash', + family: 'gemini-3', + isPreview: true, + dialogLocation: 'manual', + features: { thinking: false, multimodalToolUse: true }, + }, + 'gemini-2.5-pro': { + tier: 'pro', + family: 'gemini-2.5', + isPreview: false, + dialogLocation: 'manual', + features: { thinking: false, multimodalToolUse: false }, + }, + 'gemini-2.5-flash': { + tier: 'flash', + family: 'gemini-2.5', + isPreview: false, + dialogLocation: 'manual', + features: { thinking: false, multimodalToolUse: false }, + }, + 'gemini-2.5-flash-lite': { + tier: 'flash-lite', + family: 'gemini-2.5', + isPreview: false, + dialogLocation: 'manual', + features: { thinking: false, multimodalToolUse: false }, + }, + // Aliases + auto: { + tier: 'auto', + isPreview: true, + features: { thinking: true, multimodalToolUse: false }, + }, + pro: { + tier: 'pro', + isPreview: false, + features: { thinking: true, multimodalToolUse: false }, + }, + flash: { + tier: 'flash', + isPreview: false, + features: { thinking: false, multimodalToolUse: false }, + }, + 'flash-lite': { + tier: 'flash-lite', + isPreview: false, + features: { thinking: false, multimodalToolUse: false }, + }, + 'auto-gemini-3': { + displayName: 'Auto (Gemini 3)', + tier: 'auto', + isPreview: true, + dialogLocation: 'main', + dialogDescription: + 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash', + features: { thinking: true, multimodalToolUse: false }, + }, + 'auto-gemini-2.5': { + displayName: 'Auto (Gemini 2.5)', + tier: 'auto', + isPreview: false, + dialogLocation: 'main', + dialogDescription: + 'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash', + features: { thinking: false, multimodalToolUse: false }, + }, + }, }; diff --git a/packages/core/src/config/models.test.ts b/packages/core/src/config/models.test.ts index d62827ed91..26da6ca1cb 100644 --- a/packages/core/src/config/models.test.ts +++ b/packages/core/src/config/models.test.ts @@ -31,6 +31,96 @@ import { isPreviewModel, isProModel, } from './models.js'; +import type { Config } from './config.js'; +import { ModelConfigService } from '../services/modelConfigService.js'; +import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js'; + +const modelConfigService = new ModelConfigService(DEFAULT_MODEL_CONFIGS); + +const dynamicConfig = { + getExperimentalDynamicModelConfiguration: () => true, + modelConfigService, +} as unknown as Config; + +const legacyConfig = { + getExperimentalDynamicModelConfiguration: () => false, + modelConfigService, +} as unknown as Config; + +describe('Dynamic Configuration Parity', () => { + const modelsToTest = [ + GEMINI_MODEL_ALIAS_AUTO, + GEMINI_MODEL_ALIAS_PRO, + GEMINI_MODEL_ALIAS_FLASH, + PREVIEW_GEMINI_MODEL_AUTO, + DEFAULT_GEMINI_MODEL_AUTO, + PREVIEW_GEMINI_MODEL, + DEFAULT_GEMINI_MODEL, + 'custom-model', + ]; + + it('getDisplayString should match legacy behavior', () => { + for (const model of modelsToTest) { + const legacy = getDisplayString(model, legacyConfig); + const dynamic = getDisplayString(model, dynamicConfig); + expect(dynamic).toBe(legacy); + } + }); + + it('isPreviewModel should match legacy behavior', () => { + const allModels = [ + ...modelsToTest, + PREVIEW_GEMINI_3_1_MODEL, + PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, + PREVIEW_GEMINI_FLASH_MODEL, + ]; + for (const model of allModels) { + const legacy = isPreviewModel(model, legacyConfig); + const dynamic = isPreviewModel(model, dynamicConfig); + expect(dynamic).toBe(legacy); + } + }); + + it('isProModel should match legacy behavior', () => { + for (const model of modelsToTest) { + const legacy = isProModel(model, legacyConfig); + const dynamic = isProModel(model, dynamicConfig); + expect(dynamic).toBe(legacy); + } + }); + + it('isGemini3Model should match legacy behavior', () => { + for (const model of modelsToTest) { + const legacy = isGemini3Model(model, legacyConfig); + const dynamic = isGemini3Model(model, dynamicConfig); + expect(dynamic).toBe(legacy); + } + }); + + it('isCustomModel should match legacy behavior', () => { + for (const model of modelsToTest) { + const legacy = isCustomModel(model, legacyConfig); + const dynamic = isCustomModel(model, dynamicConfig); + expect(dynamic).toBe(legacy); + } + }); + + it('supportsModernFeatures should match legacy behavior', () => { + for (const model of modelsToTest) { + const legacy = supportsModernFeatures(model); + const dynamic = supportsModernFeatures(model); + expect(dynamic).toBe(legacy); + } + }); + + it('supportsMultimodalFunctionResponse should match legacy behavior', () => { + for (const model of modelsToTest) { + const legacy = supportsMultimodalFunctionResponse(model, legacyConfig); + const dynamic = supportsMultimodalFunctionResponse(model, dynamicConfig); + expect(dynamic).toBe(legacy); + } + }); +}); describe('isPreviewModel', () => { it('should return true for preview models', () => { diff --git a/packages/core/src/config/models.ts b/packages/core/src/config/models.ts index ffbf597793..73eab4633c 100644 --- a/packages/core/src/config/models.ts +++ b/packages/core/src/config/models.ts @@ -4,6 +4,33 @@ * SPDX-License-Identifier: Apache-2.0 */ +/** + * Interface for the ModelConfigService to break circular dependencies. + */ +export interface IModelConfigService { + getModelDefinition(modelId: string): + | { + tier?: string; + family?: string; + isPreview?: boolean; + displayName?: string; + features?: { + thinking?: boolean; + multimodalToolUse?: boolean; + }; + } + | undefined; +} + +/** + * Interface defining the minimal configuration required for model capability checks. + * This helps break circular dependencies between Config and models.ts. + */ +export interface ModelCapabilityContext { + readonly modelConfigService: IModelConfigService; + getExperimentalDynamicModelConfiguration(): boolean; +} + export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview'; export const PREVIEW_GEMINI_3_1_MODEL = 'gemini-3.1-pro-preview'; export const PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL = @@ -139,7 +166,17 @@ export function resolveClassifierModel( } return resolveModel(requestedModel, useGemini3_1, useCustomToolModel); } -export function getDisplayString(model: string) { +export function getDisplayString( + model: string, + config?: ModelCapabilityContext, +) { + if (config?.getExperimentalDynamicModelConfiguration?.() === true) { + const definition = config.modelConfigService.getModelDefinition(model); + if (definition?.displayName) { + return definition.displayName; + } + } + switch (model) { case PREVIEW_GEMINI_MODEL_AUTO: return 'Auto (Gemini 3)'; @@ -160,9 +197,19 @@ export function getDisplayString(model: string) { * Checks if the model is a preview model. * * @param model The model name to check. + * @param config Optional config object for dynamic model configuration. * @returns True if the model is a preview model. */ -export function isPreviewModel(model: string): boolean { +export function isPreviewModel( + model: string, + config?: ModelCapabilityContext, +): boolean { + if (config?.getExperimentalDynamicModelConfiguration?.() === true) { + return ( + config.modelConfigService.getModelDefinition(model)?.isPreview === true + ); + } + return ( model === PREVIEW_GEMINI_MODEL || model === PREVIEW_GEMINI_3_1_MODEL || @@ -177,9 +224,16 @@ export function isPreviewModel(model: string): boolean { * Checks if the model is a Pro model. * * @param model The model name to check. + * @param config Optional config object for dynamic model configuration. * @returns True if the model is a Pro model. */ -export function isProModel(model: string): boolean { +export function isProModel( + model: string, + config?: ModelCapabilityContext, +): boolean { + if (config?.getExperimentalDynamicModelConfiguration?.() === true) { + return config.modelConfigService.getModelDefinition(model)?.tier === 'pro'; + } return model.toLowerCase().includes('pro'); } @@ -187,9 +241,22 @@ export function isProModel(model: string): boolean { * Checks if the model is a Gemini 3 model. * * @param model The model name to check. + * @param config Optional config object for dynamic model configuration. * @returns True if the model is a Gemini 3 model. */ -export function isGemini3Model(model: string): boolean { +export function isGemini3Model( + model: string, + config?: ModelCapabilityContext, +): boolean { + if (config?.getExperimentalDynamicModelConfiguration?.() === true) { + // Legacy behavior resolves the model first. + const resolved = resolveModel(model); + return ( + config.modelConfigService.getModelDefinition(resolved)?.family === + 'gemini-3' + ); + } + const resolved = resolveModel(model); return /^gemini-3(\.|-|$)/.test(resolved); } @@ -201,6 +268,8 @@ export function isGemini3Model(model: string): boolean { * @returns True if the model is a Gemini-2.x model. */ export function isGemini2Model(model: string): boolean { + // This is legacy behavior, will remove this when gemini 2 models are no + // longer needed. return /^gemini-2(\.|$)/.test(model); } @@ -208,9 +277,20 @@ export function isGemini2Model(model: string): boolean { * Checks if the model is a "custom" model (not Gemini branded). * * @param model The model name to check. + * @param config Optional config object for dynamic model configuration. * @returns True if the model is not a Gemini branded model. */ -export function isCustomModel(model: string): boolean { +export function isCustomModel( + model: string, + config?: ModelCapabilityContext, +): boolean { + if (config?.getExperimentalDynamicModelConfiguration?.() === true) { + const resolved = resolveModel(model); + return ( + config.modelConfigService.getModelDefinition(resolved)?.tier === + 'custom' || !resolved.startsWith('gemini-') + ); + } const resolved = resolveModel(model); return !resolved.startsWith('gemini-'); } @@ -231,9 +311,16 @@ export function supportsModernFeatures(model: string): boolean { * Checks if the model is an auto model. * * @param model The model name to check. + * @param config Optional config object for dynamic model configuration. * @returns True if the model is an auto model. */ -export function isAutoModel(model: string): boolean { +export function isAutoModel( + model: string, + config?: ModelCapabilityContext, +): boolean { + if (config?.getExperimentalDynamicModelConfiguration?.() === true) { + return config.modelConfigService.getModelDefinition(model)?.tier === 'auto'; + } return ( model === GEMINI_MODEL_ALIAS_AUTO || model === PREVIEW_GEMINI_MODEL_AUTO || @@ -248,7 +335,16 @@ export function isAutoModel(model: string): boolean { * @param model The model name to check. * @returns True if the model supports multimodal function responses. */ -export function supportsMultimodalFunctionResponse(model: string): boolean { +export function supportsMultimodalFunctionResponse( + model: string, + config?: ModelCapabilityContext, +): boolean { + if (config?.getExperimentalDynamicModelConfiguration?.() === true) { + return ( + config.modelConfigService.getModelDefinition(model)?.features + ?.multimodalToolUse === true + ); + } return model.startsWith('gemini-3-'); } diff --git a/packages/core/src/routing/strategies/approvalModeStrategy.ts b/packages/core/src/routing/strategies/approvalModeStrategy.ts index 403a4c3176..b7565f6dc3 100644 --- a/packages/core/src/routing/strategies/approvalModeStrategy.ts +++ b/packages/core/src/routing/strategies/approvalModeStrategy.ts @@ -36,7 +36,7 @@ export class ApprovalModeStrategy implements RoutingStrategy { const model = context.requestedModel ?? config.getModel(); // This strategy only applies to "auto" models. - if (!isAutoModel(model)) { + if (!isAutoModel(model, config)) { return null; } diff --git a/packages/core/src/routing/strategies/classifierStrategy.ts b/packages/core/src/routing/strategies/classifierStrategy.ts index 2040e7eccd..3532e34c63 100644 --- a/packages/core/src/routing/strategies/classifierStrategy.ts +++ b/packages/core/src/routing/strategies/classifierStrategy.ts @@ -139,7 +139,7 @@ export class ClassifierStrategy implements RoutingStrategy { const model = context.requestedModel ?? config.getModel(); if ( (await config.getNumericalRoutingEnabled()) && - isGemini3Model(model) + isGemini3Model(model, config) ) { return null; } diff --git a/packages/core/src/routing/strategies/numericalClassifierStrategy.ts b/packages/core/src/routing/strategies/numericalClassifierStrategy.ts index c86576d6ce..a97180c8eb 100644 --- a/packages/core/src/routing/strategies/numericalClassifierStrategy.ts +++ b/packages/core/src/routing/strategies/numericalClassifierStrategy.ts @@ -109,7 +109,7 @@ export class NumericalClassifierStrategy implements RoutingStrategy { return null; } - if (!isGemini3Model(model)) { + if (!isGemini3Model(model, config)) { return null; } diff --git a/packages/core/src/routing/strategies/overrideStrategy.ts b/packages/core/src/routing/strategies/overrideStrategy.ts index 9a89d2af70..37e23e188b 100644 --- a/packages/core/src/routing/strategies/overrideStrategy.ts +++ b/packages/core/src/routing/strategies/overrideStrategy.ts @@ -29,7 +29,7 @@ export class OverrideStrategy implements RoutingStrategy { const overrideModel = context.requestedModel ?? config.getModel(); // If the model is 'auto' we should pass to the next strategy. - if (isAutoModel(overrideModel)) { + if (isAutoModel(overrideModel, config)) { return null; } diff --git a/packages/core/src/scheduler/tool-executor.ts b/packages/core/src/scheduler/tool-executor.ts index 4c7ef2ee04..83d77c5a0b 100644 --- a/packages/core/src/scheduler/tool-executor.ts +++ b/packages/core/src/scheduler/tool-executor.ts @@ -296,6 +296,7 @@ export class ToolExecutor { call.request.callId, output, this.config.getActiveModel(), + this.config, ); // Inject the cancellation error into the response object @@ -352,6 +353,7 @@ export class ToolExecutor { callId, content, this.config.getActiveModel(), + this.config, ); const successResponse: ToolCallResponseInfo = { diff --git a/packages/core/src/services/modelConfigService.ts b/packages/core/src/services/modelConfigService.ts index 5142411be7..2999129116 100644 --- a/packages/core/src/services/modelConfigService.ts +++ b/packages/core/src/services/modelConfigService.ts @@ -51,11 +51,34 @@ export interface ModelConfigAlias { modelConfig: ModelConfig; } +// A model definition is a mapping from a model name to a list of features +// that the model supports. Model names can be either direct model IDs +// (gemini-2.5-pro) or aliases (auto). +export interface ModelDefinition { + displayName?: string; + tier?: string; // 'pro' | 'flash' | 'flash-lite' | 'custom' | 'auto' + family?: string; // The gemini family, e.g. 'gemini-3' | 'gemini-2' + isPreview?: boolean; + // Specifies which view the model should appear in. If unset, the model will + // not appear in the dialog. + dialogLocation?: 'main' | 'manual'; + /** A short description of the model for the dialog. */ + dialogDescription?: string; + features?: { + // Whether the model supports thinking. + thinking?: boolean; + // Whether the model supports mutlimodal function responses. This is + // supported in Gemini 3. + multimodalToolUse?: boolean; + }; +} + export interface ModelConfigServiceConfig { aliases?: Record; customAliases?: Record; overrides?: ModelConfigOverride[]; customOverrides?: ModelConfigOverride[]; + modelDefinitions?: Record; } const MAX_ALIAS_CHAIN_DEPTH = 100; @@ -76,6 +99,28 @@ export class ModelConfigService { // TODO(12597): Process config to build a typed alias hierarchy. constructor(private readonly config: ModelConfigServiceConfig) {} + getModelDefinition(modelId: string): ModelDefinition | undefined { + const definition = this.config.modelDefinitions?.[modelId]; + if (definition) { + return definition; + } + + // For unknown models, return an implicit custom definition to match legacy behavior. + if (!modelId.startsWith('gemini-')) { + return { + tier: 'custom', + family: 'custom', + features: {}, + }; + } + + return undefined; + } + + getModelDefinitions(): Record { + return this.config.modelDefinitions ?? {}; + } + registerRuntimeModelConfig(aliasName: string, alias: ModelConfigAlias): void { this.runtimeAliases[aliasName] = alias; } diff --git a/packages/core/src/utils/generateContentResponseUtilities.ts b/packages/core/src/utils/generateContentResponseUtilities.ts index fdd5dff81a..3b27dd372f 100644 --- a/packages/core/src/utils/generateContentResponseUtilities.ts +++ b/packages/core/src/utils/generateContentResponseUtilities.ts @@ -13,6 +13,7 @@ import type { import { getResponseText } from './partUtils.js'; import { supportsMultimodalFunctionResponse } from '../config/models.js'; import { debugLogger } from './debugLogger.js'; +import type { Config } from '../config/config.js'; /** * Formats tool output for a Gemini FunctionResponse. @@ -48,6 +49,7 @@ export function convertToFunctionResponse( callId: string, llmContent: PartListUnion, model: string, + config?: Config, ): Part[] { if (typeof llmContent === 'string') { return [createFunctionResponsePart(callId, toolName, llmContent)]; @@ -96,7 +98,10 @@ export function convertToFunctionResponse( }, }; - const isMultimodalFRSupported = supportsMultimodalFunctionResponse(model); + const isMultimodalFRSupported = supportsMultimodalFunctionResponse( + model, + config, + ); const siblingParts: Part[] = [...fileDataParts]; if (inlineDataParts.length > 0) { diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index aeed9af419..f482053d9f 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -629,7 +629,7 @@ "modelConfigs": { "title": "Model Configs", "description": "Model configurations.", - "markdownDescription": "Model configurations.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `{\n \"aliases\": {\n \"base\": {\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 0,\n \"topP\": 1\n }\n }\n },\n \"chat-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"includeThoughts\": true\n },\n \"temperature\": 1,\n \"topP\": 0.95,\n \"topK\": 64\n }\n }\n },\n \"chat-base-2.5\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 8192\n }\n }\n }\n },\n \"chat-base-3\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingLevel\": \"HIGH\"\n }\n }\n }\n },\n \"gemini-3-pro-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"gemini-3-flash-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"gemini-2.5-pro\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"gemini-2.5-flash\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"gemini-2.5-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-3-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"classifier\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 1024,\n \"thinkingConfig\": {\n \"thinkingBudget\": 512\n }\n }\n }\n },\n \"prompt-completion\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.3,\n \"maxOutputTokens\": 16000,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"fast-ack-helper\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.2,\n \"maxOutputTokens\": 120,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"edit-corrector\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"summarizer-default\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"summarizer-shell\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"web-search\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"googleSearch\": {}\n }\n ]\n }\n }\n },\n \"web-fetch\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"urlContext\": {}\n }\n ]\n }\n }\n },\n \"web-fetch-fallback\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection-double-check\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"llm-edit-fixer\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"next-speaker-checker\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"chat-compression-3-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"chat-compression-3-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"chat-compression-2.5-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"chat-compression-2.5-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"chat-compression-2.5-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"chat-compression-default\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n }\n },\n \"overrides\": [\n {\n \"match\": {\n \"model\": \"chat-base\",\n \"isRetry\": true\n },\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 1\n }\n }\n }\n ]\n}`", + "markdownDescription": "Model configurations.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `{\n \"aliases\": {\n \"base\": {\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 0,\n \"topP\": 1\n }\n }\n },\n \"chat-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"includeThoughts\": true\n },\n \"temperature\": 1,\n \"topP\": 0.95,\n \"topK\": 64\n }\n }\n },\n \"chat-base-2.5\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 8192\n }\n }\n }\n },\n \"chat-base-3\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingLevel\": \"HIGH\"\n }\n }\n }\n },\n \"gemini-3-pro-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"gemini-3-flash-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"gemini-2.5-pro\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"gemini-2.5-flash\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"gemini-2.5-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-3-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"classifier\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 1024,\n \"thinkingConfig\": {\n \"thinkingBudget\": 512\n }\n }\n }\n },\n \"prompt-completion\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.3,\n \"maxOutputTokens\": 16000,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"fast-ack-helper\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.2,\n \"maxOutputTokens\": 120,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"edit-corrector\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"summarizer-default\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"summarizer-shell\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"web-search\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"googleSearch\": {}\n }\n ]\n }\n }\n },\n \"web-fetch\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"urlContext\": {}\n }\n ]\n }\n }\n },\n \"web-fetch-fallback\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection-double-check\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"llm-edit-fixer\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"next-speaker-checker\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"chat-compression-3-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"chat-compression-3-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"chat-compression-2.5-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"chat-compression-2.5-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"chat-compression-2.5-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"chat-compression-default\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n }\n },\n \"overrides\": [\n {\n \"match\": {\n \"model\": \"chat-base\",\n \"isRetry\": true\n },\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 1\n }\n }\n }\n ],\n \"modelDefinitions\": {\n \"gemini-3.1-pro-preview\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3.1-pro-preview-customtools\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3-pro-preview\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3-flash-preview\": {\n \"tier\": \"flash\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-2.5-pro\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"gemini-2.5-flash\": {\n \"tier\": \"flash\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"tier\": \"flash-lite\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"auto\": {\n \"tier\": \"auto\",\n \"isPreview\": true,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"pro\": {\n \"tier\": \"pro\",\n \"isPreview\": false,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"flash\": {\n \"tier\": \"flash\",\n \"isPreview\": false,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"flash-lite\": {\n \"tier\": \"flash-lite\",\n \"isPreview\": false,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"auto-gemini-3\": {\n \"displayName\": \"Auto (Gemini 3)\",\n \"tier\": \"auto\",\n \"isPreview\": true,\n \"dialogLocation\": \"main\",\n \"dialogDescription\": \"Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash\",\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"auto-gemini-2.5\": {\n \"displayName\": \"Auto (Gemini 2.5)\",\n \"tier\": \"auto\",\n \"isPreview\": false,\n \"dialogLocation\": \"main\",\n \"dialogDescription\": \"Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n }\n }\n}`", "default": { "aliases": { "base": { @@ -871,7 +871,132 @@ } } } - ] + ], + "modelDefinitions": { + "gemini-3.1-pro-preview": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3.1-pro-preview-customtools": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3-pro-preview": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3-flash-preview": { + "tier": "flash", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": true + } + }, + "gemini-2.5-pro": { + "tier": "pro", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "gemini-2.5-flash": { + "tier": "flash", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "gemini-2.5-flash-lite": { + "tier": "flash-lite", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "auto": { + "tier": "auto", + "isPreview": true, + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "pro": { + "tier": "pro", + "isPreview": false, + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "flash": { + "tier": "flash", + "isPreview": false, + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "flash-lite": { + "tier": "flash-lite", + "isPreview": false, + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "auto-gemini-3": { + "displayName": "Auto (Gemini 3)", + "tier": "auto", + "isPreview": true, + "dialogLocation": "main", + "dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash", + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "auto-gemini-2.5": { + "displayName": "Auto (Gemini 2.5)", + "tier": "auto", + "isPreview": false, + "dialogLocation": "main", + "dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash", + "features": { + "thinking": false, + "multimodalToolUse": false + } + } + } }, "type": "object", "properties": { @@ -1133,6 +1258,140 @@ "default": [], "type": "array", "items": {} + }, + "modelDefinitions": { + "title": "Model Definitions", + "description": "Registry of model metadata, including tier, family, and features.", + "markdownDescription": "Registry of model metadata, including tier, family, and features.\n\n- Category: `Model`\n- Requires restart: `yes`\n- Default: `{\n \"gemini-3.1-pro-preview\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3.1-pro-preview-customtools\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3-pro-preview\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3-flash-preview\": {\n \"tier\": \"flash\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-2.5-pro\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"gemini-2.5-flash\": {\n \"tier\": \"flash\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"tier\": \"flash-lite\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"dialogLocation\": \"manual\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"auto\": {\n \"tier\": \"auto\",\n \"isPreview\": true,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"pro\": {\n \"tier\": \"pro\",\n \"isPreview\": false,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"flash\": {\n \"tier\": \"flash\",\n \"isPreview\": false,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"flash-lite\": {\n \"tier\": \"flash-lite\",\n \"isPreview\": false,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"auto-gemini-3\": {\n \"displayName\": \"Auto (Gemini 3)\",\n \"tier\": \"auto\",\n \"isPreview\": true,\n \"dialogLocation\": \"main\",\n \"dialogDescription\": \"Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash\",\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"auto-gemini-2.5\": {\n \"displayName\": \"Auto (Gemini 2.5)\",\n \"tier\": \"auto\",\n \"isPreview\": false,\n \"dialogLocation\": \"main\",\n \"dialogDescription\": \"Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n }\n}`", + "default": { + "gemini-3.1-pro-preview": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3.1-pro-preview-customtools": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3-pro-preview": { + "tier": "pro", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": true, + "multimodalToolUse": true + } + }, + "gemini-3-flash-preview": { + "tier": "flash", + "family": "gemini-3", + "isPreview": true, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": true + } + }, + "gemini-2.5-pro": { + "tier": "pro", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "gemini-2.5-flash": { + "tier": "flash", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "gemini-2.5-flash-lite": { + "tier": "flash-lite", + "family": "gemini-2.5", + "isPreview": false, + "dialogLocation": "manual", + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "auto": { + "tier": "auto", + "isPreview": true, + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "pro": { + "tier": "pro", + "isPreview": false, + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "flash": { + "tier": "flash", + "isPreview": false, + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "flash-lite": { + "tier": "flash-lite", + "isPreview": false, + "features": { + "thinking": false, + "multimodalToolUse": false + } + }, + "auto-gemini-3": { + "displayName": "Auto (Gemini 3)", + "tier": "auto", + "isPreview": true, + "dialogLocation": "main", + "dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash", + "features": { + "thinking": true, + "multimodalToolUse": false + } + }, + "auto-gemini-2.5": { + "displayName": "Auto (Gemini 2.5)", + "tier": "auto", + "isPreview": false, + "dialogLocation": "main", + "dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash", + "features": { + "thinking": false, + "multimodalToolUse": false + } + } + }, + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ModelDefinition" + } } }, "additionalProperties": false @@ -1800,6 +2059,13 @@ "default": false, "type": "boolean" }, + "dynamicModelConfiguration": { + "title": "Dynamic Model Configuration", + "description": "Enable dynamic model configuration (definitions, resolutions, and chains) via settings.", + "markdownDescription": "Enable dynamic model configuration (definitions, resolutions, and chains) via settings.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`", + "default": false, + "type": "boolean" + }, "gemmaModelRouter": { "title": "Gemma Model Router", "description": "Enable Gemma model router (experimental).", @@ -2561,6 +2827,41 @@ } } } + }, + "ModelDefinition": { + "type": "object", + "description": "Model metadata registry entry.", + "properties": { + "displayName": { + "type": "string" + }, + "tier": { + "enum": ["pro", "flash", "flash-lite", "custom", "auto"] + }, + "family": { + "type": "string" + }, + "isPreview": { + "type": "boolean" + }, + "dialogLocation": { + "enum": ["main", "manual"] + }, + "dialogDescription": { + "type": "string" + }, + "features": { + "type": "object", + "properties": { + "thinking": { + "type": "boolean" + }, + "multimodalToolUse": { + "type": "boolean" + } + } + } + } } } }