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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -569,6 +569,9 @@ export class GeminiClient {
return resolveModel(
this.config.getActiveModel(),
this.config.getGemini31LaunchedSync?.() ?? false,
false,
this.config.getHasAccessToPreviewModel?.() ?? true,
this.config,
);
}

View File

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

View File

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

View File

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

View File

@@ -180,6 +180,8 @@ export class ClassifierStrategy implements RoutingStrategy {
routerResponse.model_choice,
useGemini3_1,
useCustomToolModel,
config.getHasAccessToPreviewModel?.() ?? true,
config,
);
return {

View File

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

View File

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

View File

@@ -156,6 +156,8 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
modelAlias,
useGemini3_1,
useCustomToolModel,
config.getHasAccessToPreviewModel?.() ?? true,
config,
);
const latencyMs = Date.now() - startTime;

View File

@@ -38,6 +38,9 @@ export class OverrideStrategy implements RoutingStrategy {
model: resolveModel(
overrideModel,
config.getGemini31LaunchedSync?.() ?? false,
false,
config.getHasAccessToPreviewModel?.() ?? true,
config,
),
metadata: {
source: this.name,

View File

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