Add support for dynamic model Resolution to ModelConfigService (#22578)

This commit is contained in:
kevinjwang1
2026-03-17 14:15:50 -07:00
committed by GitHub
parent 77ca3c0e13
commit 27a50191e3
17 changed files with 1050 additions and 42 deletions
+191 -8
View File
@@ -688,7 +688,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro", "tier": "pro",
"family": "gemini-3", "family": "gemini-3",
"isPreview": true, "isPreview": true,
"dialogLocation": "manual", "isVisible": true,
"features": { "features": {
"thinking": true, "thinking": true,
"multimodalToolUse": true "multimodalToolUse": true
@@ -698,6 +698,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro", "tier": "pro",
"family": "gemini-3", "family": "gemini-3",
"isPreview": true, "isPreview": true,
"isVisible": false,
"features": { "features": {
"thinking": true, "thinking": true,
"multimodalToolUse": true "multimodalToolUse": true
@@ -707,7 +708,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro", "tier": "pro",
"family": "gemini-3", "family": "gemini-3",
"isPreview": true, "isPreview": true,
"dialogLocation": "manual", "isVisible": true,
"features": { "features": {
"thinking": true, "thinking": true,
"multimodalToolUse": true "multimodalToolUse": true
@@ -717,7 +718,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash", "tier": "flash",
"family": "gemini-3", "family": "gemini-3",
"isPreview": true, "isPreview": true,
"dialogLocation": "manual", "isVisible": true,
"features": { "features": {
"thinking": false, "thinking": false,
"multimodalToolUse": true "multimodalToolUse": true
@@ -727,7 +728,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro", "tier": "pro",
"family": "gemini-2.5", "family": "gemini-2.5",
"isPreview": false, "isPreview": false,
"dialogLocation": "manual", "isVisible": true,
"features": { "features": {
"thinking": false, "thinking": false,
"multimodalToolUse": false "multimodalToolUse": false
@@ -737,7 +738,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash", "tier": "flash",
"family": "gemini-2.5", "family": "gemini-2.5",
"isPreview": false, "isPreview": false,
"dialogLocation": "manual", "isVisible": true,
"features": { "features": {
"thinking": false, "thinking": false,
"multimodalToolUse": false "multimodalToolUse": false
@@ -747,7 +748,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash-lite", "tier": "flash-lite",
"family": "gemini-2.5", "family": "gemini-2.5",
"isPreview": false, "isPreview": false,
"dialogLocation": "manual", "isVisible": true,
"features": { "features": {
"thinking": false, "thinking": false,
"multimodalToolUse": false "multimodalToolUse": false
@@ -756,6 +757,7 @@ their corresponding top-level category object in your `settings.json` file.
"auto": { "auto": {
"tier": "auto", "tier": "auto",
"isPreview": true, "isPreview": true,
"isVisible": false,
"features": { "features": {
"thinking": true, "thinking": true,
"multimodalToolUse": false "multimodalToolUse": false
@@ -764,6 +766,7 @@ their corresponding top-level category object in your `settings.json` file.
"pro": { "pro": {
"tier": "pro", "tier": "pro",
"isPreview": false, "isPreview": false,
"isVisible": false,
"features": { "features": {
"thinking": true, "thinking": true,
"multimodalToolUse": false "multimodalToolUse": false
@@ -772,6 +775,7 @@ their corresponding top-level category object in your `settings.json` file.
"flash": { "flash": {
"tier": "flash", "tier": "flash",
"isPreview": false, "isPreview": false,
"isVisible": false,
"features": { "features": {
"thinking": false, "thinking": false,
"multimodalToolUse": false "multimodalToolUse": false
@@ -780,6 +784,7 @@ their corresponding top-level category object in your `settings.json` file.
"flash-lite": { "flash-lite": {
"tier": "flash-lite", "tier": "flash-lite",
"isPreview": false, "isPreview": false,
"isVisible": false,
"features": { "features": {
"thinking": false, "thinking": false,
"multimodalToolUse": false "multimodalToolUse": false
@@ -789,7 +794,7 @@ their corresponding top-level category object in your `settings.json` file.
"displayName": "Auto (Gemini 3)", "displayName": "Auto (Gemini 3)",
"tier": "auto", "tier": "auto",
"isPreview": true, "isPreview": true,
"dialogLocation": "main", "isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash", "dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash",
"features": { "features": {
"thinking": true, "thinking": true,
@@ -800,7 +805,7 @@ their corresponding top-level category object in your `settings.json` file.
"displayName": "Auto (Gemini 2.5)", "displayName": "Auto (Gemini 2.5)",
"tier": "auto", "tier": "auto",
"isPreview": false, "isPreview": false,
"dialogLocation": "main", "isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash", "dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash",
"features": { "features": {
"thinking": false, "thinking": false,
@@ -812,6 +817,184 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes - **Requires restart:** Yes
- **`modelConfigs.modelIdResolutions`** (object):
- **Description:** Rules for resolving requested model names to concrete model
IDs based on context.
- **Default:**
```json
{
"gemini-3-pro-preview": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto-gemini-3": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"pro": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto-gemini-2.5": {
"default": "gemini-2.5-pro"
},
"flash": {
"default": "gemini-3-flash-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-flash"
}
]
},
"flash-lite": {
"default": "gemini-2.5-flash-lite"
}
}
```
- **Requires restart:** Yes
- **`modelConfigs.classifierIdResolutions`** (object):
- **Description:** Rules for resolving classifier tiers (flash, pro) to
concrete model IDs.
- **Default:**
```json
{
"flash": {
"default": "gemini-3-flash-preview",
"contexts": [
{
"condition": {
"requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
},
"target": "gemini-2.5-flash"
},
{
"condition": {
"requestedModels": ["auto-gemini-3", "gemini-3-pro-preview"]
},
"target": "gemini-3-flash-preview"
}
]
},
"pro": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
}
}
```
- **Requires restart:** Yes
#### `agents` #### `agents`
- **`agents.overrides`** (object): - **`agents.overrides`** (object):
+57 -1
View File
@@ -1053,6 +1053,34 @@ const SETTINGS_SCHEMA = {
ref: 'ModelDefinition', ref: 'ModelDefinition',
}, },
}, },
modelIdResolutions: {
type: 'object',
label: 'Model ID Resolutions',
category: 'Model',
requiresRestart: true,
default: DEFAULT_MODEL_CONFIGS.modelIdResolutions,
description:
'Rules for resolving requested model names to concrete model IDs based on context.',
showInDialog: false,
additionalProperties: {
type: 'object',
ref: 'ModelResolution',
},
},
classifierIdResolutions: {
type: 'object',
label: 'Classifier ID Resolutions',
category: 'Model',
requiresRestart: true,
default: DEFAULT_MODEL_CONFIGS.classifierIdResolutions,
description:
'Rules for resolving classifier tiers (flash, pro) to concrete model IDs.',
showInDialog: false,
additionalProperties: {
type: 'object',
ref: 'ModelResolution',
},
},
}, },
}, },
@@ -2800,7 +2828,7 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
tier: { enum: ['pro', 'flash', 'flash-lite', 'custom', 'auto'] }, tier: { enum: ['pro', 'flash', 'flash-lite', 'custom', 'auto'] },
family: { type: 'string' }, family: { type: 'string' },
isPreview: { type: 'boolean' }, isPreview: { type: 'boolean' },
dialogLocation: { enum: ['main', 'manual'] }, isVisible: { type: 'boolean' },
dialogDescription: { type: 'string' }, dialogDescription: { type: 'string' },
features: { features: {
type: 'object', type: 'object',
@@ -2811,6 +2839,34 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
}, },
}, },
}, },
ModelResolution: {
type: 'object',
description: 'Model resolution rule.',
properties: {
default: { type: 'string' },
contexts: {
type: 'array',
items: {
type: 'object',
properties: {
condition: {
type: 'object',
properties: {
useGemini3_1: { type: 'boolean' },
useCustomTools: { type: 'boolean' },
hasAccessToPreview: { type: 'boolean' },
requestedModels: {
type: 'array',
items: { type: 'string' },
},
},
},
target: { type: 'string' },
},
},
},
},
},
}; };
export function getSettingsSchema(): SettingsSchemaType { export function getSettingsSchema(): SettingsSchemaType {
+10
View File
@@ -981,6 +981,14 @@ export class Config implements McpContext, AgentLoopContext {
...DEFAULT_MODEL_CONFIGS.modelDefinitions, ...DEFAULT_MODEL_CONFIGS.modelDefinitions,
...modelConfigServiceConfig.modelDefinitions, ...modelConfigServiceConfig.modelDefinitions,
}; };
const mergedModelIdResolutions = {
...DEFAULT_MODEL_CONFIGS.modelIdResolutions,
...modelConfigServiceConfig.modelIdResolutions,
};
const mergedClassifierIdResolutions = {
...DEFAULT_MODEL_CONFIGS.classifierIdResolutions,
...modelConfigServiceConfig.classifierIdResolutions,
};
modelConfigServiceConfig = { modelConfigServiceConfig = {
// Preserve other user settings like customAliases // Preserve other user settings like customAliases
@@ -992,6 +1000,8 @@ export class Config implements McpContext, AgentLoopContext {
modelConfigServiceConfig.overrides ?? DEFAULT_MODEL_CONFIGS.overrides, modelConfigServiceConfig.overrides ?? DEFAULT_MODEL_CONFIGS.overrides,
// Use the merged model definitions // Use the merged model definitions
modelDefinitions: mergedModelDefinitions, modelDefinitions: mergedModelDefinitions,
modelIdResolutions: mergedModelIdResolutions,
classifierIdResolutions: mergedClassifierIdResolutions,
}; };
} }
+120 -8
View File
@@ -255,76 +255,81 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
tier: 'pro', tier: 'pro',
family: 'gemini-3', family: 'gemini-3',
isPreview: true, isPreview: true,
dialogLocation: 'manual', isVisible: true,
features: { thinking: true, multimodalToolUse: true }, features: { thinking: true, multimodalToolUse: true },
}, },
'gemini-3.1-pro-preview-customtools': { 'gemini-3.1-pro-preview-customtools': {
tier: 'pro', tier: 'pro',
family: 'gemini-3', family: 'gemini-3',
isPreview: true, isPreview: true,
isVisible: false,
features: { thinking: true, multimodalToolUse: true }, features: { thinking: true, multimodalToolUse: true },
}, },
'gemini-3-pro-preview': { 'gemini-3-pro-preview': {
tier: 'pro', tier: 'pro',
family: 'gemini-3', family: 'gemini-3',
isPreview: true, isPreview: true,
dialogLocation: 'manual', isVisible: true,
features: { thinking: true, multimodalToolUse: true }, features: { thinking: true, multimodalToolUse: true },
}, },
'gemini-3-flash-preview': { 'gemini-3-flash-preview': {
tier: 'flash', tier: 'flash',
family: 'gemini-3', family: 'gemini-3',
isPreview: true, isPreview: true,
dialogLocation: 'manual', isVisible: true,
features: { thinking: false, multimodalToolUse: true }, features: { thinking: false, multimodalToolUse: true },
}, },
'gemini-2.5-pro': { 'gemini-2.5-pro': {
tier: 'pro', tier: 'pro',
family: 'gemini-2.5', family: 'gemini-2.5',
isPreview: false, isPreview: false,
dialogLocation: 'manual', isVisible: true,
features: { thinking: false, multimodalToolUse: false }, features: { thinking: false, multimodalToolUse: false },
}, },
'gemini-2.5-flash': { 'gemini-2.5-flash': {
tier: 'flash', tier: 'flash',
family: 'gemini-2.5', family: 'gemini-2.5',
isPreview: false, isPreview: false,
dialogLocation: 'manual', isVisible: true,
features: { thinking: false, multimodalToolUse: false }, features: { thinking: false, multimodalToolUse: false },
}, },
'gemini-2.5-flash-lite': { 'gemini-2.5-flash-lite': {
tier: 'flash-lite', tier: 'flash-lite',
family: 'gemini-2.5', family: 'gemini-2.5',
isPreview: false, isPreview: false,
dialogLocation: 'manual', isVisible: true,
features: { thinking: false, multimodalToolUse: false }, features: { thinking: false, multimodalToolUse: false },
}, },
// Aliases // Aliases
auto: { auto: {
tier: 'auto', tier: 'auto',
isPreview: true, isPreview: true,
isVisible: false,
features: { thinking: true, multimodalToolUse: false }, features: { thinking: true, multimodalToolUse: false },
}, },
pro: { pro: {
tier: 'pro', tier: 'pro',
isPreview: false, isPreview: false,
isVisible: false,
features: { thinking: true, multimodalToolUse: false }, features: { thinking: true, multimodalToolUse: false },
}, },
flash: { flash: {
tier: 'flash', tier: 'flash',
isPreview: false, isPreview: false,
isVisible: false,
features: { thinking: false, multimodalToolUse: false }, features: { thinking: false, multimodalToolUse: false },
}, },
'flash-lite': { 'flash-lite': {
tier: 'flash-lite', tier: 'flash-lite',
isPreview: false, isPreview: false,
isVisible: false,
features: { thinking: false, multimodalToolUse: false }, features: { thinking: false, multimodalToolUse: false },
}, },
'auto-gemini-3': { 'auto-gemini-3': {
displayName: 'Auto (Gemini 3)', displayName: 'Auto (Gemini 3)',
tier: 'auto', tier: 'auto',
isPreview: true, isPreview: true,
dialogLocation: 'main', isVisible: true,
dialogDescription: dialogDescription:
'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash', 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash',
features: { thinking: true, multimodalToolUse: false }, features: { thinking: true, multimodalToolUse: false },
@@ -333,10 +338,117 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
displayName: 'Auto (Gemini 2.5)', displayName: 'Auto (Gemini 2.5)',
tier: 'auto', tier: 'auto',
isPreview: false, isPreview: false,
dialogLocation: 'main', isVisible: true,
dialogDescription: dialogDescription:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash', 'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
features: { thinking: false, multimodalToolUse: false }, features: { thinking: false, multimodalToolUse: false },
}, },
}, },
modelIdResolutions: {
'gemini-3-pro-preview': {
default: 'gemini-3-pro-preview',
contexts: [
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
{
condition: { useGemini3_1: true, useCustomTools: true },
target: 'gemini-3.1-pro-preview-customtools',
},
{
condition: { useGemini3_1: true },
target: 'gemini-3.1-pro-preview',
},
],
},
'auto-gemini-3': {
default: 'gemini-3-pro-preview',
contexts: [
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
{
condition: { useGemini3_1: true, useCustomTools: true },
target: 'gemini-3.1-pro-preview-customtools',
},
{
condition: { useGemini3_1: true },
target: 'gemini-3.1-pro-preview',
},
],
},
auto: {
default: 'gemini-3-pro-preview',
contexts: [
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
{
condition: { useGemini3_1: true, useCustomTools: true },
target: 'gemini-3.1-pro-preview-customtools',
},
{
condition: { useGemini3_1: true },
target: 'gemini-3.1-pro-preview',
},
],
},
pro: {
default: 'gemini-3-pro-preview',
contexts: [
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
{
condition: { useGemini3_1: true, useCustomTools: true },
target: 'gemini-3.1-pro-preview-customtools',
},
{
condition: { useGemini3_1: true },
target: 'gemini-3.1-pro-preview',
},
],
},
'auto-gemini-2.5': {
default: 'gemini-2.5-pro',
},
flash: {
default: 'gemini-3-flash-preview',
contexts: [
{
condition: { hasAccessToPreview: false },
target: 'gemini-2.5-flash',
},
],
},
'flash-lite': {
default: 'gemini-2.5-flash-lite',
},
},
classifierIdResolutions: {
flash: {
default: 'gemini-3-flash-preview',
contexts: [
{
condition: { requestedModels: ['auto-gemini-2.5', 'gemini-2.5-pro'] },
target: 'gemini-2.5-flash',
},
{
condition: {
requestedModels: ['auto-gemini-3', 'gemini-3-pro-preview'],
},
target: 'gemini-3-flash-preview',
},
],
},
pro: {
default: 'gemini-3-pro-preview',
contexts: [
{
condition: { requestedModels: ['auto-gemini-2.5', 'gemini-2.5-pro'] },
target: 'gemini-2.5-pro',
},
{
condition: { useGemini3_1: true, useCustomTools: true },
target: 'gemini-3.1-pro-preview-customtools',
},
{
condition: { useGemini3_1: true },
target: 'gemini-3.1-pro-preview',
},
],
},
},
}; };
+84
View File
@@ -60,6 +60,90 @@ describe('Dynamic Configuration Parity', () => {
'custom-model', 'custom-model',
]; ];
const flagCombos = [
{ useGemini3_1: false, useCustomToolModel: false },
{ useGemini3_1: true, useCustomToolModel: false },
{ useGemini3_1: true, useCustomToolModel: true },
];
it('resolveModel should match legacy behavior when dynamicModelConfiguration flag enabled.', () => {
for (const model of modelsToTest) {
for (const flags of flagCombos) {
for (const hasAccess of [true, false]) {
const mockLegacyConfig = {
...legacyConfig,
getHasAccessToPreviewModel: () => hasAccess,
} as unknown as Config;
const mockDynamicConfig = {
...dynamicConfig,
getHasAccessToPreviewModel: () => hasAccess,
} as unknown as Config;
const legacy = resolveModel(
model,
flags.useGemini3_1,
flags.useCustomToolModel,
hasAccess,
mockLegacyConfig,
);
const dynamic = resolveModel(
model,
flags.useGemini3_1,
flags.useCustomToolModel,
hasAccess,
mockDynamicConfig,
);
expect(dynamic).toBe(legacy);
}
}
}
});
it('resolveClassifierModel should match legacy behavior.', () => {
const classifierTiers = [GEMINI_MODEL_ALIAS_PRO, GEMINI_MODEL_ALIAS_FLASH];
const anchorModels = [
PREVIEW_GEMINI_MODEL_AUTO,
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL,
DEFAULT_GEMINI_MODEL,
];
for (const hasAccess of [true, false]) {
const mockLegacyConfig = {
...legacyConfig,
getHasAccessToPreviewModel: () => hasAccess,
} as unknown as Config;
const mockDynamicConfig = {
...dynamicConfig,
getHasAccessToPreviewModel: () => hasAccess,
} as unknown as Config;
for (const tier of classifierTiers) {
for (const anchor of anchorModels) {
for (const flags of flagCombos) {
const legacy = resolveClassifierModel(
anchor,
tier,
flags.useGemini3_1,
flags.useCustomToolModel,
hasAccess,
mockLegacyConfig,
);
const dynamic = resolveClassifierModel(
anchor,
tier,
flags.useGemini3_1,
flags.useCustomToolModel,
hasAccess,
mockDynamicConfig,
);
expect(dynamic).toBe(legacy);
}
}
}
}
});
it('getDisplayString should match legacy behavior', () => { it('getDisplayString should match legacy behavior', () => {
for (const model of modelsToTest) { for (const model of modelsToTest) {
const legacy = getDisplayString(model, legacyConfig); const legacy = getDisplayString(model, legacyConfig);
+46 -1
View File
@@ -4,6 +4,13 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
export interface ModelResolutionContext {
useGemini3_1?: boolean;
useCustomTools?: boolean;
hasAccessToPreview?: boolean;
requestedModel?: string;
}
/** /**
* Interface for the ModelConfigService to break circular dependencies. * Interface for the ModelConfigService to break circular dependencies.
*/ */
@@ -20,6 +27,17 @@ export interface IModelConfigService {
}; };
} }
| undefined; | undefined;
resolveModelId(
requestedModel: string,
context?: ModelResolutionContext,
): string;
resolveClassifierModelId(
tier: string,
requestedModel: string,
context?: ModelResolutionContext,
): string;
} }
/** /**
@@ -81,7 +99,16 @@ export function resolveModel(
useGemini3_1: boolean = false, useGemini3_1: boolean = false,
useCustomToolModel: boolean = false, useCustomToolModel: boolean = false,
hasAccessToPreview: boolean = true, hasAccessToPreview: boolean = true,
config?: ModelCapabilityContext,
): string { ): string {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
return config.modelConfigService.resolveModelId(requestedModel, {
useGemini3_1,
useCustomTools: useCustomToolModel,
hasAccessToPreview,
});
}
let resolved: string; let resolved: string;
switch (requestedModel) { switch (requestedModel) {
case PREVIEW_GEMINI_MODEL: case PREVIEW_GEMINI_MODEL:
@@ -144,6 +171,9 @@ export function resolveModel(
* *
* @param requestedModel The current requested model (e.g. auto-gemini-2.5). * @param requestedModel The current requested model (e.g. auto-gemini-2.5).
* @param modelAlias The alias selected by the classifier ('flash' or 'pro'). * @param modelAlias The alias selected by the classifier ('flash' or 'pro').
* @param useGemini3_1 Whether to use Gemini 3.1 Pro Preview.
* @param useCustomToolModel Whether to use the custom tool model.
* @param config Optional config object for dynamic model configuration.
* @returns The resolved concrete model name. * @returns The resolved concrete model name.
*/ */
export function resolveClassifierModel( export function resolveClassifierModel(
@@ -151,7 +181,21 @@ export function resolveClassifierModel(
modelAlias: string, modelAlias: string,
useGemini3_1: boolean = false, useGemini3_1: boolean = false,
useCustomToolModel: boolean = false, useCustomToolModel: boolean = false,
hasAccessToPreview: boolean = true,
config?: ModelCapabilityContext,
): string { ): string {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
return config.modelConfigService.resolveClassifierModelId(
modelAlias,
requestedModel,
{
useGemini3_1,
useCustomTools: useCustomToolModel,
hasAccessToPreview,
},
);
}
if (modelAlias === GEMINI_MODEL_ALIAS_FLASH) { if (modelAlias === GEMINI_MODEL_ALIAS_FLASH) {
if ( if (
requestedModel === DEFAULT_GEMINI_MODEL_AUTO || requestedModel === DEFAULT_GEMINI_MODEL_AUTO ||
@@ -169,6 +213,7 @@ export function resolveClassifierModel(
} }
return resolveModel(requestedModel, useGemini3_1, useCustomToolModel); return resolveModel(requestedModel, useGemini3_1, useCustomToolModel);
} }
export function getDisplayString( export function getDisplayString(
model: string, model: string,
config?: ModelCapabilityContext, config?: ModelCapabilityContext,
@@ -289,7 +334,7 @@ export function isCustomModel(
config?: ModelCapabilityContext, config?: ModelCapabilityContext,
): boolean { ): boolean {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) { if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
const resolved = resolveModel(model); const resolved = resolveModel(model, false, false, true, config);
return ( return (
config.modelConfigService.getModelDefinition(resolved)?.tier === config.modelConfigService.getModelDefinition(resolved)?.tier ===
'custom' || !resolved.startsWith('gemini-') 'custom' || !resolved.startsWith('gemini-')
+3
View File
@@ -569,6 +569,9 @@ export class GeminiClient {
return resolveModel( return resolveModel(
this.config.getActiveModel(), this.config.getActiveModel(),
this.config.getGemini31LaunchedSync?.() ?? false, this.config.getGemini31LaunchedSync?.() ?? false,
false,
this.config.getHasAccessToPreviewModel?.() ?? true,
this.config,
); );
} }
@@ -171,6 +171,9 @@ export async function createContentGenerator(
config.authType === AuthType.USE_GEMINI || config.authType === AuthType.USE_GEMINI ||
config.authType === AuthType.USE_VERTEX_AI || config.authType === AuthType.USE_VERTEX_AI ||
((await gcConfig.getGemini31Launched?.()) ?? false), ((await gcConfig.getGemini31Launched?.()) ?? false),
false,
gcConfig.getHasAccessToPreviewModel?.() ?? true,
gcConfig,
); );
const customHeadersEnv = const customHeadersEnv =
process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined; process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined;
+10 -1
View File
@@ -525,7 +525,13 @@ export class GeminiChat {
const useGemini3_1 = const useGemini3_1 =
(await this.context.config.getGemini31Launched?.()) ?? false; (await this.context.config.getGemini31Launched?.()) ?? false;
// Default to the last used model (which respects arguments/availability selection) // Default to the last used model (which respects arguments/availability selection)
let modelToUse = resolveModel(lastModelToUse, useGemini3_1); let modelToUse = resolveModel(
lastModelToUse,
useGemini3_1,
false,
this.context.config.getHasAccessToPreviewModel?.() ?? true,
this.context.config,
);
// If the active model has changed (e.g. due to a fallback updating the config), // If the active model has changed (e.g. due to a fallback updating the config),
// we switch to the new active model. // we switch to the new active model.
@@ -533,6 +539,9 @@ export class GeminiChat {
modelToUse = resolveModel( modelToUse = resolveModel(
this.context.config.getActiveModel(), this.context.config.getActiveModel(),
useGemini3_1, useGemini3_1,
false,
this.context.config.getHasAccessToPreviewModel?.() ?? true,
this.context.config,
); );
} }
@@ -62,6 +62,9 @@ export class PromptProvider {
const desiredModel = resolveModel( const desiredModel = resolveModel(
context.config.getActiveModel(), context.config.getActiveModel(),
context.config.getGemini31LaunchedSync?.() ?? false, context.config.getGemini31LaunchedSync?.() ?? false,
false,
context.config.getHasAccessToPreviewModel?.() ?? true,
context.config,
); );
const isModernModel = supportsModernFeatures(desiredModel); const isModernModel = supportsModernFeatures(desiredModel);
const activeSnippets = isModernModel ? snippets : legacySnippets; const activeSnippets = isModernModel ? snippets : legacySnippets;
@@ -239,6 +242,9 @@ export class PromptProvider {
const desiredModel = resolveModel( const desiredModel = resolveModel(
context.config.getActiveModel(), context.config.getActiveModel(),
context.config.getGemini31LaunchedSync?.() ?? false, context.config.getGemini31LaunchedSync?.() ?? false,
false,
context.config.getHasAccessToPreviewModel?.() ?? true,
context.config,
); );
const isModernModel = supportsModernFeatures(desiredModel); const isModernModel = supportsModernFeatures(desiredModel);
const activeSnippets = isModernModel ? snippets : legacySnippets; const activeSnippets = isModernModel ? snippets : legacySnippets;
@@ -180,6 +180,8 @@ export class ClassifierStrategy implements RoutingStrategy {
routerResponse.model_choice, routerResponse.model_choice,
useGemini3_1, useGemini3_1,
useCustomToolModel, useCustomToolModel,
config.getHasAccessToPreviewModel?.() ?? true,
config,
); );
return { return {
@@ -26,6 +26,9 @@ export class DefaultStrategy implements TerminalStrategy {
const defaultModel = resolveModel( const defaultModel = resolveModel(
config.getModel(), config.getModel(),
config.getGemini31LaunchedSync?.() ?? false, config.getGemini31LaunchedSync?.() ?? false,
false,
config.getHasAccessToPreviewModel?.() ?? true,
config,
); );
return { return {
model: defaultModel, model: defaultModel,
@@ -28,6 +28,9 @@ export class FallbackStrategy implements RoutingStrategy {
const resolvedModel = resolveModel( const resolvedModel = resolveModel(
requestedModel, requestedModel,
config.getGemini31LaunchedSync?.() ?? false, config.getGemini31LaunchedSync?.() ?? false,
false,
config.getHasAccessToPreviewModel?.() ?? true,
config,
); );
const service = config.getModelAvailabilityService(); const service = config.getModelAvailabilityService();
const snapshot = service.snapshot(resolvedModel); const snapshot = service.snapshot(resolvedModel);
@@ -156,6 +156,8 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
modelAlias, modelAlias,
useGemini3_1, useGemini3_1,
useCustomToolModel, useCustomToolModel,
config.getHasAccessToPreviewModel?.() ?? true,
config,
); );
const latencyMs = Date.now() - startTime; const latencyMs = Date.now() - startTime;
@@ -38,6 +38,9 @@ export class OverrideStrategy implements RoutingStrategy {
model: resolveModel( model: resolveModel(
overrideModel, overrideModel,
config.getGemini31LaunchedSync?.() ?? false, config.getGemini31LaunchedSync?.() ?? false,
false,
config.getHasAccessToPreviewModel?.() ?? true,
config,
), ),
metadata: { metadata: {
source: this.name, source: this.name,
@@ -59,9 +59,8 @@ export interface ModelDefinition {
tier?: string; // 'pro' | 'flash' | 'flash-lite' | 'custom' | 'auto' tier?: string; // 'pro' | 'flash' | 'flash-lite' | 'custom' | 'auto'
family?: string; // The gemini family, e.g. 'gemini-3' | 'gemini-2' family?: string; // The gemini family, e.g. 'gemini-3' | 'gemini-2'
isPreview?: boolean; isPreview?: boolean;
// Specifies which view the model should appear in. If unset, the model will // Specifies whether the model should be visible in the dialog.
// not appear in the dialog. isVisible?: boolean;
dialogLocation?: 'main' | 'manual';
/** A short description of the model for the dialog. */ /** A short description of the model for the dialog. */
dialogDescription?: string; dialogDescription?: string;
features?: { features?: {
@@ -73,12 +72,45 @@ export interface ModelDefinition {
}; };
} }
// A model resolution is a mapping from a model name to a list of conditions
// that can be used to resolve the model to a model ID.
export interface ModelResolution {
// The default model ID to use when no conditions are met.
default: string;
// A list of conditions that can be used to resolve the model.
contexts?: Array<{
// The condition to check for.
condition: ResolutionCondition;
// The model ID to use when the condition is met.
target: string;
}>;
}
/** The actual state of the current session. */
export interface ResolutionContext {
useGemini3_1?: boolean;
useCustomTools?: boolean;
hasAccessToPreview?: boolean;
requestedModel?: string;
}
/** The requirements defined in the registry. */
export interface ResolutionCondition {
useGemini3_1?: boolean;
useCustomTools?: boolean;
hasAccessToPreview?: boolean;
/** Matches if the current model is in this list. */
requestedModels?: string[];
}
export interface ModelConfigServiceConfig { export interface ModelConfigServiceConfig {
aliases?: Record<string, ModelConfigAlias>; aliases?: Record<string, ModelConfigAlias>;
customAliases?: Record<string, ModelConfigAlias>; customAliases?: Record<string, ModelConfigAlias>;
overrides?: ModelConfigOverride[]; overrides?: ModelConfigOverride[];
customOverrides?: ModelConfigOverride[]; customOverrides?: ModelConfigOverride[];
modelDefinitions?: Record<string, ModelDefinition>; modelDefinitions?: Record<string, ModelDefinition>;
modelIdResolutions?: Record<string, ModelResolution>;
classifierIdResolutions?: Record<string, ModelResolution>;
} }
const MAX_ALIAS_CHAIN_DEPTH = 100; const MAX_ALIAS_CHAIN_DEPTH = 100;
@@ -121,6 +153,74 @@ export class ModelConfigService {
return this.config.modelDefinitions ?? {}; return this.config.modelDefinitions ?? {};
} }
private matches(
condition: ResolutionCondition,
context: ResolutionContext,
): boolean {
return Object.entries(condition).every(([key, value]) => {
if (value === undefined) return true;
switch (key) {
case 'useGemini3_1':
return value === context.useGemini3_1;
case 'useCustomTools':
return value === context.useCustomTools;
case 'hasAccessToPreview':
return value === context.hasAccessToPreview;
case 'requestedModels':
return (
Array.isArray(value) &&
!!context.requestedModel &&
value.includes(context.requestedModel)
);
default:
return false;
}
});
}
// Resolves a model ID to a concrete model ID based on the provided context.
resolveModelId(
requestedName: string,
context: ResolutionContext = {},
): string {
const resolution = this.config.modelIdResolutions?.[requestedName];
if (!resolution) {
return requestedName;
}
for (const ctx of resolution.contexts ?? []) {
if (this.matches(ctx.condition, context)) {
return ctx.target;
}
}
return resolution.default;
}
// Resolves a classifier model ID to a concrete model ID based on the provided context.
resolveClassifierModelId(
tier: string,
requestedModel: string,
context: ResolutionContext = {},
): string {
const resolution = this.config.classifierIdResolutions?.[tier];
const fullContext: ResolutionContext = { ...context, requestedModel };
if (!resolution) {
// Fallback to regular model resolution if no classifier-specific rule exists
return this.resolveModelId(tier, fullContext);
}
for (const ctx of resolution.contexts ?? []) {
if (this.matches(ctx.condition, fullContext)) {
return ctx.target;
}
}
return resolution.default;
}
registerRuntimeModelConfig(aliasName: string, alias: ModelConfigAlias): void { registerRuntimeModelConfig(aliasName: string, alias: ModelConfigAlias): void {
this.runtimeAliases[aliasName] = alias; this.runtimeAliases[aliasName] = alias;
} }
File diff suppressed because one or more lines are too long