Add ModelDefinitions to ModelConfigService (#22302)

This commit is contained in:
kevinjwang1
2026-03-14 14:45:21 -07:00
committed by GitHub
parent 8f2697c2e5
commit 0bf7ea60c5
19 changed files with 904 additions and 56 deletions

View File

@@ -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.

View File

@@ -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));

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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<SectionProps> = ({ title, children }) => (
// Logic for building the unified list of table rows
const buildModelRows = (
models: Record<string, ModelMetrics>,
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<string, ModelMetrics>;
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<StatsDisplayProps> = ({
</Section>
<ModelUsageTable
models={models}
config={config}
quotas={quotas}
cacheEfficiency={computed.cacheEfficiency}
totalCachedTokens={computed.totalCachedTokens}

View File

@@ -122,7 +122,7 @@ export const BrowserAgentDefinition = (
): LocalAgentDefinition<typeof BrowserTaskResultSchema> => {
// 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;

View File

@@ -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({

View File

@@ -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;
}

View File

@@ -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 },
},
},
};

View File

@@ -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', () => {

View File

@@ -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-');
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -109,7 +109,7 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
return null;
}
if (!isGemini3Model(model)) {
if (!isGemini3Model(model, config)) {
return null;
}

View File

@@ -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;
}

View File

@@ -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 = {

View File

@@ -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<string, ModelConfigAlias>;
customAliases?: Record<string, ModelConfigAlias>;
overrides?: ModelConfigOverride[];
customOverrides?: ModelConfigOverride[];
modelDefinitions?: Record<string, ModelDefinition>;
}
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<string, ModelDefinition> {
return this.config.modelDefinitions ?? {};
}
registerRuntimeModelConfig(aliasName: string, alias: ModelConfigAlias): void {
this.runtimeAliases[aliasName] = alias;
}

View File

@@ -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) {

File diff suppressed because one or more lines are too long