Add ModelDefinitions to ModelConfigService

This commit is contained in:
Kevin Wang
2026-03-13 08:25:03 +00:00
parent b6beab9480
commit bdbb996a2e
16 changed files with 403 additions and 41 deletions
+1
View File
@@ -857,6 +857,7 @@ export async function loadCliConfig(
disableLLMCorrection: settings.tools?.disableLLMCorrection,
rawOutput: argv.rawOutput,
acceptRawOutputRisk: argv.acceptRawOutputRisk,
dynamicModelConfiguration: settings.experimental?.dynamicModelConfiguration,
modelConfigServiceConfig: settings.modelConfigs,
// TODO: loading of hooks based on workspace trust
enableHooks: settings.hooksConfig.enabled,
+43
View File
@@ -1029,6 +1029,20 @@ const SETTINGS_SCHEMA = {
'Apply specific configuration overrides based on matches, with a primary key of model (or alias). The most specific match will be used.',
showInDialog: false,
},
modelDefinitions: {
type: 'object',
label: 'Model Definitions',
category: 'Model',
requiresRestart: true,
default: DEFAULT_MODEL_CONFIGS.modelDefinitions,
description:
'Registry of model metadata, including tier, family, and features.',
showInDialog: false,
additionalProperties: {
type: 'object',
ref: 'ModelDefinition',
},
},
},
},
@@ -1900,6 +1914,16 @@ const SETTINGS_SCHEMA = {
'Enable web fetch behavior that bypasses LLM summarization.',
showInDialog: true,
},
dynamicModelConfiguration: {
type: 'boolean',
label: 'Dynamic Model Configuration',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable dynamic model configuration (definitions, resolutions, and chains) via settings.',
showInDialog: false,
},
gemmaModelRouter: {
type: 'object',
label: 'Gemma Model Router',
@@ -2716,6 +2740,25 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
},
},
},
ModelDefinition: {
type: 'object',
description: 'Model metadata registry entry.',
properties: {
displayName: { type: 'string' },
tier: { enum: ['pro', 'flash', 'flash-lite', 'custom', 'auto'] },
family: { type: 'string' },
isPreview: { type: 'boolean' },
dialogLocation: { enum: ['main', 'manual'] },
dialogDescription: { type: 'string' },
features: {
type: 'object',
properties: {
thinking: { type: 'boolean' },
multimodalToolUse: { type: 'boolean' },
},
},
},
},
};
export function getSettingsSchema(): SettingsSchemaType {
@@ -27,6 +27,7 @@ import {
} from '../utils/displayUtils.js';
import { computeSessionStats } from '../utils/computeStats.js';
import {
type Config,
type RetrieveUserQuotaResponse,
isActiveModel,
getDisplayString,
@@ -88,13 +89,16 @@ const Section: React.FC<SectionProps> = ({ title, children }) => (
// Logic for building the unified list of table rows
const buildModelRows = (
models: Record<string, ModelMetrics>,
config: Config,
quotas?: RetrieveUserQuotaResponse,
useGemini3_1 = false,
useCustomToolModel = false,
) => {
const getBaseModelName = (name: string) => name.replace('-001', '');
const usedModelNames = new Set(
Object.keys(models).map(getBaseModelName).map(getDisplayString),
Object.keys(models)
.map(getBaseModelName)
.map((name) => getDisplayString(name, config)),
);
// 1. Models with active usage
@@ -104,7 +108,7 @@ const buildModelRows = (
const inputTokens = metrics.tokens.input;
return {
key: name,
modelName: getDisplayString(modelName),
modelName: getDisplayString(modelName, config),
requests: metrics.api.totalRequests,
cachedTokens: cachedTokens.toLocaleString(),
inputTokens: inputTokens.toLocaleString(),
@@ -121,11 +125,11 @@ const buildModelRows = (
(b) =>
b.modelId &&
isActiveModel(b.modelId, useGemini3_1, useCustomToolModel) &&
!usedModelNames.has(getDisplayString(b.modelId)),
!usedModelNames.has(getDisplayString(b.modelId, config)),
)
.map((bucket) => ({
key: bucket.modelId!,
modelName: getDisplayString(bucket.modelId!),
modelName: getDisplayString(bucket.modelId!, config),
requests: '-',
cachedTokens: '-',
inputTokens: '-',
@@ -139,6 +143,7 @@ const buildModelRows = (
const ModelUsageTable: React.FC<{
models: Record<string, ModelMetrics>;
config: Config;
quotas?: RetrieveUserQuotaResponse;
cacheEfficiency: number;
totalCachedTokens: number;
@@ -150,6 +155,7 @@ const ModelUsageTable: React.FC<{
useCustomToolModel?: boolean;
}> = ({
models,
config,
quotas,
cacheEfficiency,
totalCachedTokens,
@@ -162,7 +168,13 @@ const ModelUsageTable: React.FC<{
}) => {
const { stdout } = useStdout();
const terminalWidth = stdout?.columns ?? 84;
const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel);
const rows = buildModelRows(
models,
config,
quotas,
useGemini3_1,
useCustomToolModel,
);
if (rows.length === 0) {
return null;
@@ -676,6 +688,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
</Section>
<ModelUsageTable
models={models}
config={config}
quotas={quotas}
cacheEfficiency={computed.cacheEfficiency}
totalCachedTokens={computed.totalCachedTokens}
@@ -109,7 +109,7 @@ export const BrowserAgentDefinition = (
): LocalAgentDefinition<typeof BrowserTaskResultSchema> => {
// Use Preview Flash model if the main model is any of the preview models.
// If the main model is not a preview model, use the default flash model.
const model = isPreviewModel(config.getModel())
const model = isPreviewModel(config.getModel(), config)
? PREVIEW_GEMINI_FLASH_MODEL
: DEFAULT_GEMINI_FLASH_MODEL;
@@ -54,19 +54,21 @@ export function resolvePolicyChain(
useCustomToolModel,
hasAccessToPreview,
);
const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false;
const isAutoConfigured = isAutoModel(configuredModel);
const isAutoPreferred = preferredModel
? isAutoModel(preferredModel, config)
: false;
const isAutoConfigured = isAutoModel(configuredModel, config);
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
chain = getFlashLitePolicyChain();
} else if (
isGemini3Model(resolvedModel) ||
isGemini3Model(resolvedModel, config) ||
isAutoPreferred ||
isAutoConfigured
) {
if (hasAccessToPreview) {
const previewEnabled =
isGemini3Model(resolvedModel) ||
isGemini3Model(resolvedModel, config) ||
preferredModel === PREVIEW_GEMINI_MODEL_AUTO ||
configuredModel === PREVIEW_GEMINI_MODEL_AUTO;
chain = getModelPolicyChain({
+33 -13
View File
@@ -601,6 +601,7 @@ export interface ConfigParameters {
disableYoloMode?: boolean;
rawOutput?: boolean;
acceptRawOutputRisk?: boolean;
dynamicModelConfiguration?: boolean;
modelConfigServiceConfig?: ModelConfigServiceConfig;
enableHooks?: boolean;
enableHooksUI?: boolean;
@@ -799,6 +800,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly disableYoloMode: boolean;
private readonly rawOutput: boolean;
private readonly acceptRawOutputRisk: boolean;
private readonly dynamicModelConfiguration: boolean;
private pendingIncludeDirectories: string[];
private readonly enableHooks: boolean;
private readonly enableHooksUI: boolean;
@@ -987,7 +989,7 @@ export class Config implements McpContext, AgentLoopContext {
this.truncateToolOutputThreshold =
params.truncateToolOutputThreshold ??
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD;
this.useWriteTodos = isPreviewModel(this.model)
this.useWriteTodos = isPreviewModel(this.model, this)
? false
: (params.useWriteTodos ?? true);
this.workspacePoliciesDir = params.workspacePoliciesDir;
@@ -1062,6 +1064,7 @@ export class Config implements McpContext, AgentLoopContext {
this.disableYoloMode = params.disableYoloMode ?? false;
this.rawOutput = params.rawOutput ?? false;
this.acceptRawOutputRisk = params.acceptRawOutputRisk ?? false;
this.dynamicModelConfiguration = params.dynamicModelConfiguration ?? false;
if (params.hooks) {
this.hooks = params.hooks;
@@ -1110,19 +1113,28 @@ export class Config implements McpContext, AgentLoopContext {
// TODO(12593): Fix the settings loading logic to properly merge defaults and
// remove this hack.
let modelConfigServiceConfig = params.modelConfigServiceConfig;
const defaultsToApply: Partial<ModelConfigServiceConfig> = {};
if (modelConfigServiceConfig) {
if (!modelConfigServiceConfig.aliases) {
modelConfigServiceConfig = {
...modelConfigServiceConfig,
aliases: DEFAULT_MODEL_CONFIGS.aliases,
};
defaultsToApply.aliases = DEFAULT_MODEL_CONFIGS.aliases;
}
if (!modelConfigServiceConfig.overrides) {
modelConfigServiceConfig = {
...modelConfigServiceConfig,
overrides: DEFAULT_MODEL_CONFIGS.overrides,
};
defaultsToApply.overrides = DEFAULT_MODEL_CONFIGS.overrides;
}
if (
!modelConfigServiceConfig.modelDefinitions ||
Object.keys(modelConfigServiceConfig.modelDefinitions).length === 0
) {
defaultsToApply.modelDefinitions =
DEFAULT_MODEL_CONFIGS.modelDefinitions;
}
}
if (Object.keys(defaultsToApply).length > 0) {
modelConfigServiceConfig = {
...modelConfigServiceConfig,
...defaultsToApply,
};
}
this.modelConfigService = new ModelConfigService(
@@ -1325,7 +1337,10 @@ export class Config implements McpContext, AgentLoopContext {
// Only reset when we have explicit "no access" (hasAccessToPreviewModel === false).
// When null (quota not fetched) or true, we preserve the saved model.
if (isPreviewModel(this.model) && this.hasAccessToPreviewModel === false) {
if (
isPreviewModel(this.model, this) &&
this.hasAccessToPreviewModel === false
) {
this.setModel(DEFAULT_GEMINI_MODEL_AUTO);
}
@@ -1581,7 +1596,7 @@ export class Config implements McpContext, AgentLoopContext {
const isPreview =
model === PREVIEW_GEMINI_MODEL_AUTO ||
isPreviewModel(this.getActiveModel());
isPreviewModel(this.getActiveModel(), this);
const proModel = isPreview ? PREVIEW_GEMINI_MODEL : DEFAULT_GEMINI_MODEL;
const flashModel = isPreview
? PREVIEW_GEMINI_FLASH_MODEL
@@ -1779,8 +1794,9 @@ export class Config implements McpContext, AgentLoopContext {
}
const hasAccess =
quota.buckets?.some((b) => b.modelId && isPreviewModel(b.modelId)) ??
false;
quota.buckets?.some(
(b) => b.modelId && isPreviewModel(b.modelId, this),
) ?? false;
this.setHasAccessToPreviewModel(hasAccess);
return quota;
} catch (e) {
@@ -2172,6 +2188,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.acceptRawOutputRisk;
}
getExperimentalDynamicModelConfiguration(): boolean {
return this.dynamicModelConfiguration;
}
getPendingIncludeDirectories(): string[] {
return this.pendingIncludeDirectories;
}
@@ -249,4 +249,94 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
},
],
modelDefinitions: {
// Concrete Models
'gemini-3.1-pro-preview': {
tier: 'pro',
family: 'gemini-3',
isPreview: true,
dialogLocation: 'manual',
features: { thinking: true, multimodalToolUse: true },
},
'gemini-3.1-pro-preview-customtools': {
tier: 'pro',
family: 'gemini-3',
isPreview: true,
features: { thinking: true, multimodalToolUse: true },
},
'gemini-3-pro-preview': {
tier: 'pro',
family: 'gemini-3',
isPreview: true,
dialogLocation: 'manual',
features: { thinking: true, multimodalToolUse: true },
},
'gemini-3-flash-preview': {
tier: 'flash',
family: 'gemini-3',
isPreview: true,
dialogLocation: 'manual',
features: { thinking: false, multimodalToolUse: true },
},
'gemini-2.5-pro': {
tier: 'pro',
family: 'gemini-2.5',
isPreview: false,
dialogLocation: 'manual',
features: { thinking: false, multimodalToolUse: false },
},
'gemini-2.5-flash': {
tier: 'flash',
family: 'gemini-2.5',
isPreview: false,
dialogLocation: 'manual',
features: { thinking: false, multimodalToolUse: false },
},
'gemini-2.5-flash-lite': {
tier: 'flash-lite',
family: 'gemini-2.5',
isPreview: false,
dialogLocation: 'manual',
features: { thinking: false, multimodalToolUse: false },
},
// Aliases
auto: {
tier: 'auto',
isPreview: true,
features: { thinking: true, multimodalToolUse: false },
},
pro: {
tier: 'pro',
isPreview: false,
features: { thinking: true, multimodalToolUse: false },
},
flash: {
tier: 'flash',
isPreview: false,
features: { thinking: false, multimodalToolUse: false },
},
'flash-lite': {
tier: 'flash-lite',
isPreview: false,
features: { thinking: false, multimodalToolUse: false },
},
'auto-gemini-3': {
displayName: 'Auto (Gemini 3)',
tier: 'auto',
isPreview: true,
dialogLocation: 'main',
dialogDescription:
'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash',
features: { thinking: true, multimodalToolUse: false },
},
'auto-gemini-2.5': {
displayName: 'Auto (Gemini 2.5)',
tier: 'auto',
isPreview: false,
dialogLocation: 'main',
dialogDescription:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
features: { thinking: false, multimodalToolUse: false },
},
},
};
+91
View File
@@ -36,6 +36,97 @@ import {
VALID_GEMINI_MODELS,
VALID_ALIASES,
} from './models.js';
import type { Config } from './config.js';
import { ModelConfigService } from '../services/modelConfigService.js';
import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
const modelConfigService = new ModelConfigService(DEFAULT_MODEL_CONFIGS);
const dynamicConfig = {
getExperimentalDynamicModelConfiguration: () => true,
modelConfigService,
} as unknown as Config;
const legacyConfig = {
getExperimentalDynamicModelConfiguration: () => false,
modelConfigService,
} as unknown as Config;
describe('Dynamic Configuration Parity', () => {
const modelsToTest = [
GEMINI_MODEL_ALIAS_AUTO,
GEMINI_MODEL_ALIAS_PRO,
GEMINI_MODEL_ALIAS_FLASH,
GEMINI_MODEL_ALIAS_FLASH_LITE,
PREVIEW_GEMINI_MODEL_AUTO,
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL,
DEFAULT_GEMINI_MODEL,
'custom-model',
];
it('getDisplayString should match legacy behavior', () => {
for (const model of modelsToTest) {
const legacy = getDisplayString(model, legacyConfig);
const dynamic = getDisplayString(model, dynamicConfig);
expect(dynamic).toBe(legacy);
}
});
it('isPreviewModel should match legacy behavior', () => {
const allModels = [
...modelsToTest,
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
];
for (const model of allModels) {
const legacy = isPreviewModel(model, legacyConfig);
const dynamic = isPreviewModel(model, dynamicConfig);
expect(dynamic).toBe(legacy);
}
});
it('isProModel should match legacy behavior', () => {
for (const model of modelsToTest) {
const legacy = isProModel(model, legacyConfig);
const dynamic = isProModel(model, dynamicConfig);
expect(dynamic).toBe(legacy);
}
});
it('isGemini3Model should match legacy behavior', () => {
for (const model of modelsToTest) {
const legacy = isGemini3Model(model, legacyConfig);
const dynamic = isGemini3Model(model, dynamicConfig);
expect(dynamic).toBe(legacy);
}
});
it('isGemini2Model should match legacy behavior', () => {
for (const model of modelsToTest) {
const legacy = isGemini2Model(model, legacyConfig);
const dynamic = isGemini2Model(model, dynamicConfig);
expect(dynamic).toBe(legacy);
}
});
it('isCustomModel should match legacy behavior', () => {
for (const model of modelsToTest) {
const legacy = isCustomModel(model, legacyConfig);
const dynamic = isCustomModel(model, dynamicConfig);
expect(dynamic).toBe(legacy);
}
});
it('supportsModernFeatures should match legacy behavior', () => {
for (const model of modelsToTest) {
const legacy = supportsModernFeatures(model, legacyConfig);
const dynamic = supportsModernFeatures(model, dynamicConfig);
expect(dynamic).toBe(legacy);
}
});
});
describe('isPreviewModel', () => {
it('should return true for preview models', () => {
+64 -10
View File
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from './config.js';
export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview';
export const PREVIEW_GEMINI_3_1_MODEL = 'gemini-3.1-pro-preview';
export const PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL =
@@ -148,7 +150,14 @@ export function resolveClassifierModel(
}
return resolveModel(requestedModel, useGemini3_1, useCustomToolModel);
}
export function getDisplayString(model: string) {
export function getDisplayString(model: string, config?: Config) {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
const definition = config.modelConfigService.getModelDefinition(model);
if (definition?.displayName) {
return definition.displayName;
}
}
switch (model) {
case PREVIEW_GEMINI_MODEL_AUTO:
return 'Auto (Gemini 3)';
@@ -169,9 +178,16 @@ export function getDisplayString(model: string) {
* Checks if the model is a preview model.
*
* @param model The model name to check.
* @param config Optional config object for dynamic model configuration.
* @returns True if the model is a preview model.
*/
export function isPreviewModel(model: string): boolean {
export function isPreviewModel(model: string, config?: Config): boolean {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
return (
config.modelConfigService.getModelDefinition(model)?.isPreview === true
);
}
return (
model === PREVIEW_GEMINI_MODEL ||
model === PREVIEW_GEMINI_3_1_MODEL ||
@@ -186,9 +202,13 @@ export function isPreviewModel(model: string): boolean {
* Checks if the model is a Pro model.
*
* @param model The model name to check.
* @param config Optional config object for dynamic model configuration.
* @returns True if the model is a Pro model.
*/
export function isProModel(model: string): boolean {
export function isProModel(model: string, config?: Config): boolean {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
return config.modelConfigService.getModelDefinition(model)?.tier === 'pro';
}
return model.toLowerCase().includes('pro');
}
@@ -196,9 +216,19 @@ export function isProModel(model: string): boolean {
* Checks if the model is a Gemini 3 model.
*
* @param model The model name to check.
* @param config Optional config object for dynamic model configuration.
* @returns True if the model is a Gemini 3 model.
*/
export function isGemini3Model(model: string): boolean {
export function isGemini3Model(model: string, config?: Config): boolean {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
// Legacy behavior resolves the model first.
const resolved = resolveModel(model);
return (
config.modelConfigService.getModelDefinition(resolved)?.family ===
'gemini-3'
);
}
const resolved = resolveModel(model);
return /^gemini-3(\.|-|$)/.test(resolved);
}
@@ -207,9 +237,17 @@ export function isGemini3Model(model: string): boolean {
* Checks if the model is a Gemini 2.x model.
*
* @param model The model name to check.
* @param config Optional config object for dynamic model configuration.
* @returns True if the model is a Gemini-2.x model.
*/
export function isGemini2Model(model: string): boolean {
export function isGemini2Model(model: string, config?: Config): boolean {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
// Legacy behavior does NOT resolve the model first for Gemini 2 check.
return (
config.modelConfigService.getModelDefinition(model)?.family ===
'gemini-2.5'
);
}
return /^gemini-2(\.|$)/.test(model);
}
@@ -217,9 +255,17 @@ export function isGemini2Model(model: string): boolean {
* Checks if the model is a "custom" model (not Gemini branded).
*
* @param model The model name to check.
* @param config Optional config object for dynamic model configuration.
* @returns True if the model is not a Gemini branded model.
*/
export function isCustomModel(model: string): boolean {
export function isCustomModel(model: string, config?: Config): boolean {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
const resolved = resolveModel(model);
return (
config.modelConfigService.getModelDefinition(resolved)?.tier ===
'custom' || !resolved.startsWith('gemini-')
);
}
const resolved = resolveModel(model);
return !resolved.startsWith('gemini-');
}
@@ -229,20 +275,28 @@ export function isCustomModel(model: string): boolean {
* This includes Gemini 3 models and any custom models.
*
* @param model The model name to check.
* @param config Optional config object for dynamic model configuration.
* @returns True if the model supports modern features like thoughts.
*/
export function supportsModernFeatures(model: string): boolean {
if (isGemini3Model(model)) return true;
return isCustomModel(model);
export function supportsModernFeatures(
model: string,
config?: Config,
): boolean {
if (isGemini3Model(model, config)) return true;
return isCustomModel(model, config);
}
/**
* Checks if the model is an auto model.
*
* @param model The model name to check.
* @param config Optional config object for dynamic model configuration.
* @returns True if the model is an auto model.
*/
export function isAutoModel(model: string): boolean {
export function isAutoModel(model: string, config?: Config): boolean {
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
return config.modelConfigService.getModelDefinition(model)?.tier === 'auto';
}
return (
model === GEMINI_MODEL_ALIAS_AUTO ||
model === PREVIEW_GEMINI_MODEL_AUTO ||
+5 -2
View File
@@ -421,7 +421,7 @@ export class GeminiChat {
: getRetryErrorType(error);
if (
(isContentError && isGemini2Model(model)) ||
(isContentError && isGemini2Model(model, this.config)) ||
(isRetryable && !signal.aborted)
) {
// The issue requests exactly 3 retries (4 attempts) for API errors during stream iteration.
@@ -547,7 +547,10 @@ export class GeminiChat {
abortSignal,
};
let contentsToUse: Content[] = supportsModernFeatures(modelToUse)
let contentsToUse: Content[] = supportsModernFeatures(
modelToUse,
this.config,
)
? [...contentsForPreviewModel]
: [...requestContents];
+2 -2
View File
@@ -61,7 +61,7 @@ export class PromptProvider {
config.getActiveModel(),
config.getGemini31LaunchedSync?.() ?? false,
);
const isModernModel = supportsModernFeatures(desiredModel);
const isModernModel = supportsModernFeatures(desiredModel, config);
const activeSnippets = isModernModel ? snippets : legacySnippets;
const contextFilenames = getAllGeminiMdFilenames();
@@ -232,7 +232,7 @@ export class PromptProvider {
config.getActiveModel(),
config.getGemini31LaunchedSync?.() ?? false,
);
const isModernModel = supportsModernFeatures(desiredModel);
const isModernModel = supportsModernFeatures(desiredModel, config);
const activeSnippets = isModernModel ? snippets : legacySnippets;
return activeSnippets.getCompressionPrompt(config.getApprovedPlanPath());
}
@@ -36,7 +36,7 @@ export class ApprovalModeStrategy implements RoutingStrategy {
const model = context.requestedModel ?? config.getModel();
// This strategy only applies to "auto" models.
if (!isAutoModel(model)) {
if (!isAutoModel(model, config)) {
return null;
}
@@ -139,7 +139,7 @@ export class ClassifierStrategy implements RoutingStrategy {
const model = context.requestedModel ?? config.getModel();
if (
(await config.getNumericalRoutingEnabled()) &&
isGemini3Model(model)
isGemini3Model(model, config)
) {
return null;
}
@@ -109,7 +109,7 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
return null;
}
if (!isGemini3Model(model)) {
if (!isGemini3Model(model, config)) {
return null;
}
@@ -29,7 +29,7 @@ export class OverrideStrategy implements RoutingStrategy {
const overrideModel = context.requestedModel ?? config.getModel();
// If the model is 'auto' we should pass to the next strategy.
if (isAutoModel(overrideModel)) {
if (isAutoModel(overrideModel, config)) {
return null;
}
@@ -51,11 +51,34 @@ export interface ModelConfigAlias {
modelConfig: ModelConfig;
}
// A model definition is a mapping from a model name to a list of features
// that the model supports. Model names can be either direct model IDs
// (gemini-2.5-pro) or aliases (auto).
export interface ModelDefinition {
displayName?: string;
tier?: string; // 'pro' | 'flash' | 'flash-lite' | 'custom' | 'auto'
family?: string; // The gemini family, e.g. 'gemini-3' | 'gemini-2'
isPreview?: boolean;
// Specifies which view the model should appear in. If unset, the model will
// not appear in the dialog.
dialogLocation?: 'main' | 'manual';
/** A short description of the model for the dialog. */
dialogDescription?: string;
features?: {
// Whether the model supports thinking.
thinking?: boolean;
// Whether the model supports mutlimodal function responses. This is
// supported in Gemini 3.
multimodalToolUse?: boolean;
};
}
export interface ModelConfigServiceConfig {
aliases?: Record<string, ModelConfigAlias>;
customAliases?: Record<string, ModelConfigAlias>;
overrides?: ModelConfigOverride[];
customOverrides?: ModelConfigOverride[];
modelDefinitions?: Record<string, ModelDefinition>;
}
const MAX_ALIAS_CHAIN_DEPTH = 100;
@@ -76,6 +99,28 @@ export class ModelConfigService {
// TODO(12597): Process config to build a typed alias hierarchy.
constructor(private readonly config: ModelConfigServiceConfig) {}
getModelDefinition(modelId: string): ModelDefinition | undefined {
const definition = this.config.modelDefinitions?.[modelId];
if (definition) {
return definition;
}
// For unknown models, return an implicit custom definition to match legacy behavior.
if (!modelId.startsWith('gemini-')) {
return {
tier: 'custom',
family: 'custom',
features: {},
};
}
return undefined;
}
getModelDefinitions(): Record<string, ModelDefinition> {
return this.config.modelDefinitions ?? {};
}
registerRuntimeModelConfig(aliasName: string, alias: ModelConfigAlias): void {
this.runtimeAliases[aliasName] = alias;
}