mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-31 00:11:11 -07:00
Add support for dynamic model Resolution to ModelConfigService (#22578)
This commit is contained in:
@@ -688,7 +688,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
@@ -698,6 +698,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"isVisible": false,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
@@ -707,7 +708,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
@@ -717,7 +718,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"tier": "flash",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": true
|
||||
@@ -727,7 +728,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"tier": "pro",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
@@ -737,7 +738,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"tier": "flash",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
@@ -747,7 +748,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"tier": "flash-lite",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
@@ -756,6 +757,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"auto": {
|
||||
"tier": "auto",
|
||||
"isPreview": true,
|
||||
"isVisible": false,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
@@ -764,6 +766,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"pro": {
|
||||
"tier": "pro",
|
||||
"isPreview": false,
|
||||
"isVisible": false,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
@@ -772,6 +775,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"flash": {
|
||||
"tier": "flash",
|
||||
"isPreview": false,
|
||||
"isVisible": false,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
@@ -780,6 +784,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"flash-lite": {
|
||||
"tier": "flash-lite",
|
||||
"isPreview": false,
|
||||
"isVisible": false,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
@@ -789,7 +794,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"displayName": "Auto (Gemini 3)",
|
||||
"tier": "auto",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "main",
|
||||
"isVisible": true,
|
||||
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash",
|
||||
"features": {
|
||||
"thinking": true,
|
||||
@@ -800,7 +805,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"displayName": "Auto (Gemini 2.5)",
|
||||
"tier": "auto",
|
||||
"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",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
@@ -812,6 +817,184 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **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.overrides`** (object):
|
||||
|
||||
@@ -1053,6 +1053,34 @@ const SETTINGS_SCHEMA = {
|
||||
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'] },
|
||||
family: { type: 'string' },
|
||||
isPreview: { type: 'boolean' },
|
||||
dialogLocation: { enum: ['main', 'manual'] },
|
||||
isVisible: { type: 'boolean' },
|
||||
dialogDescription: { type: 'string' },
|
||||
features: {
|
||||
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 {
|
||||
|
||||
@@ -981,6 +981,14 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
...DEFAULT_MODEL_CONFIGS.modelDefinitions,
|
||||
...modelConfigServiceConfig.modelDefinitions,
|
||||
};
|
||||
const mergedModelIdResolutions = {
|
||||
...DEFAULT_MODEL_CONFIGS.modelIdResolutions,
|
||||
...modelConfigServiceConfig.modelIdResolutions,
|
||||
};
|
||||
const mergedClassifierIdResolutions = {
|
||||
...DEFAULT_MODEL_CONFIGS.classifierIdResolutions,
|
||||
...modelConfigServiceConfig.classifierIdResolutions,
|
||||
};
|
||||
|
||||
modelConfigServiceConfig = {
|
||||
// Preserve other user settings like customAliases
|
||||
@@ -992,6 +1000,8 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
modelConfigServiceConfig.overrides ?? DEFAULT_MODEL_CONFIGS.overrides,
|
||||
// Use the merged model definitions
|
||||
modelDefinitions: mergedModelDefinitions,
|
||||
modelIdResolutions: mergedModelIdResolutions,
|
||||
classifierIdResolutions: mergedClassifierIdResolutions,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -255,76 +255,81 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
tier: 'pro',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
dialogLocation: 'manual',
|
||||
isVisible: true,
|
||||
features: { thinking: true, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-3.1-pro-preview-customtools': {
|
||||
tier: 'pro',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
isVisible: false,
|
||||
features: { thinking: true, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-3-pro-preview': {
|
||||
tier: 'pro',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
dialogLocation: 'manual',
|
||||
isVisible: true,
|
||||
features: { thinking: true, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-3-flash-preview': {
|
||||
tier: 'flash',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
dialogLocation: 'manual',
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-2.5-pro': {
|
||||
tier: 'pro',
|
||||
family: 'gemini-2.5',
|
||||
isPreview: false,
|
||||
dialogLocation: 'manual',
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'gemini-2.5-flash': {
|
||||
tier: 'flash',
|
||||
family: 'gemini-2.5',
|
||||
isPreview: false,
|
||||
dialogLocation: 'manual',
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'gemini-2.5-flash-lite': {
|
||||
tier: 'flash-lite',
|
||||
family: 'gemini-2.5',
|
||||
isPreview: false,
|
||||
dialogLocation: 'manual',
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
// Aliases
|
||||
auto: {
|
||||
tier: 'auto',
|
||||
isPreview: true,
|
||||
isVisible: false,
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
pro: {
|
||||
tier: 'pro',
|
||||
isPreview: false,
|
||||
isVisible: false,
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
flash: {
|
||||
tier: 'flash',
|
||||
isPreview: false,
|
||||
isVisible: false,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'flash-lite': {
|
||||
tier: 'flash-lite',
|
||||
isPreview: false,
|
||||
isVisible: false,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'auto-gemini-3': {
|
||||
displayName: 'Auto (Gemini 3)',
|
||||
tier: 'auto',
|
||||
isPreview: true,
|
||||
dialogLocation: 'main',
|
||||
isVisible: true,
|
||||
dialogDescription:
|
||||
'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash',
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
@@ -333,10 +338,117 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
displayName: 'Auto (Gemini 2.5)',
|
||||
tier: 'auto',
|
||||
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',
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -60,6 +60,90 @@ describe('Dynamic Configuration Parity', () => {
|
||||
'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', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = getDisplayString(model, legacyConfig);
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
* 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.
|
||||
*/
|
||||
@@ -20,6 +27,17 @@ export interface IModelConfigService {
|
||||
};
|
||||
}
|
||||
| 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,
|
||||
useCustomToolModel: boolean = false,
|
||||
hasAccessToPreview: boolean = true,
|
||||
config?: ModelCapabilityContext,
|
||||
): string {
|
||||
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
|
||||
return config.modelConfigService.resolveModelId(requestedModel, {
|
||||
useGemini3_1,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
});
|
||||
}
|
||||
|
||||
let resolved: string;
|
||||
switch (requestedModel) {
|
||||
case PREVIEW_GEMINI_MODEL:
|
||||
@@ -144,6 +171,9 @@ export function resolveModel(
|
||||
*
|
||||
* @param requestedModel The current requested model (e.g. auto-gemini-2.5).
|
||||
* @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.
|
||||
*/
|
||||
export function resolveClassifierModel(
|
||||
@@ -151,7 +181,21 @@ export function resolveClassifierModel(
|
||||
modelAlias: string,
|
||||
useGemini3_1: boolean = false,
|
||||
useCustomToolModel: boolean = false,
|
||||
hasAccessToPreview: boolean = true,
|
||||
config?: ModelCapabilityContext,
|
||||
): string {
|
||||
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
|
||||
return config.modelConfigService.resolveClassifierModelId(
|
||||
modelAlias,
|
||||
requestedModel,
|
||||
{
|
||||
useGemini3_1,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (modelAlias === GEMINI_MODEL_ALIAS_FLASH) {
|
||||
if (
|
||||
requestedModel === DEFAULT_GEMINI_MODEL_AUTO ||
|
||||
@@ -169,6 +213,7 @@ export function resolveClassifierModel(
|
||||
}
|
||||
return resolveModel(requestedModel, useGemini3_1, useCustomToolModel);
|
||||
}
|
||||
|
||||
export function getDisplayString(
|
||||
model: string,
|
||||
config?: ModelCapabilityContext,
|
||||
@@ -289,7 +334,7 @@ export function isCustomModel(
|
||||
config?: ModelCapabilityContext,
|
||||
): boolean {
|
||||
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
|
||||
const resolved = resolveModel(model);
|
||||
const resolved = resolveModel(model, false, false, true, config);
|
||||
return (
|
||||
config.modelConfigService.getModelDefinition(resolved)?.tier ===
|
||||
'custom' || !resolved.startsWith('gemini-')
|
||||
|
||||
@@ -569,6 +569,9 @@ export class GeminiClient {
|
||||
return resolveModel(
|
||||
this.config.getActiveModel(),
|
||||
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_VERTEX_AI ||
|
||||
((await gcConfig.getGemini31Launched?.()) ?? false),
|
||||
false,
|
||||
gcConfig.getHasAccessToPreviewModel?.() ?? true,
|
||||
gcConfig,
|
||||
);
|
||||
const customHeadersEnv =
|
||||
process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined;
|
||||
|
||||
@@ -525,7 +525,13 @@ export class GeminiChat {
|
||||
const useGemini3_1 =
|
||||
(await this.context.config.getGemini31Launched?.()) ?? false;
|
||||
// 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),
|
||||
// we switch to the new active model.
|
||||
@@ -533,6 +539,9 @@ export class GeminiChat {
|
||||
modelToUse = resolveModel(
|
||||
this.context.config.getActiveModel(),
|
||||
useGemini3_1,
|
||||
false,
|
||||
this.context.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
this.context.config,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ export class PromptProvider {
|
||||
const desiredModel = resolveModel(
|
||||
context.config.getActiveModel(),
|
||||
context.config.getGemini31LaunchedSync?.() ?? false,
|
||||
false,
|
||||
context.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
context.config,
|
||||
);
|
||||
const isModernModel = supportsModernFeatures(desiredModel);
|
||||
const activeSnippets = isModernModel ? snippets : legacySnippets;
|
||||
@@ -239,6 +242,9 @@ export class PromptProvider {
|
||||
const desiredModel = resolveModel(
|
||||
context.config.getActiveModel(),
|
||||
context.config.getGemini31LaunchedSync?.() ?? false,
|
||||
false,
|
||||
context.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
context.config,
|
||||
);
|
||||
const isModernModel = supportsModernFeatures(desiredModel);
|
||||
const activeSnippets = isModernModel ? snippets : legacySnippets;
|
||||
|
||||
@@ -180,6 +180,8 @@ export class ClassifierStrategy implements RoutingStrategy {
|
||||
routerResponse.model_choice,
|
||||
useGemini3_1,
|
||||
useCustomToolModel,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -26,6 +26,9 @@ export class DefaultStrategy implements TerminalStrategy {
|
||||
const defaultModel = resolveModel(
|
||||
config.getModel(),
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
false,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
);
|
||||
return {
|
||||
model: defaultModel,
|
||||
|
||||
@@ -28,6 +28,9 @@ export class FallbackStrategy implements RoutingStrategy {
|
||||
const resolvedModel = resolveModel(
|
||||
requestedModel,
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
false,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
);
|
||||
const service = config.getModelAvailabilityService();
|
||||
const snapshot = service.snapshot(resolvedModel);
|
||||
|
||||
@@ -156,6 +156,8 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
|
||||
modelAlias,
|
||||
useGemini3_1,
|
||||
useCustomToolModel,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
);
|
||||
|
||||
const latencyMs = Date.now() - startTime;
|
||||
|
||||
@@ -38,6 +38,9 @@ export class OverrideStrategy implements RoutingStrategy {
|
||||
model: resolveModel(
|
||||
overrideModel,
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
false,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
),
|
||||
metadata: {
|
||||
source: this.name,
|
||||
|
||||
@@ -59,9 +59,8 @@ export interface ModelDefinition {
|
||||
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';
|
||||
// Specifies whether the model should be visible in the dialog.
|
||||
isVisible?: boolean;
|
||||
/** A short description of the model for the dialog. */
|
||||
dialogDescription?: string;
|
||||
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 {
|
||||
aliases?: Record<string, ModelConfigAlias>;
|
||||
customAliases?: Record<string, ModelConfigAlias>;
|
||||
overrides?: ModelConfigOverride[];
|
||||
customOverrides?: ModelConfigOverride[];
|
||||
modelDefinitions?: Record<string, ModelDefinition>;
|
||||
modelIdResolutions?: Record<string, ModelResolution>;
|
||||
classifierIdResolutions?: Record<string, ModelResolution>;
|
||||
}
|
||||
|
||||
const MAX_ALIAS_CHAIN_DEPTH = 100;
|
||||
@@ -121,6 +153,74 @@ export class ModelConfigService {
|
||||
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 {
|
||||
this.runtimeAliases[aliasName] = alias;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user