mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 18:11:02 -07:00
Add ModelDefinitions to ModelConfigService (#22302)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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-');
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isGemini3Model(model)) {
|
||||
if (!isGemini3Model(model, config)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user