fix(acp) refactor(core,cli): centralize model discovery logic in ModelConfigService (#24392)

This commit is contained in:
Sri Pasumarthi
2026-04-01 11:03:30 -07:00
committed by GitHub
parent 16468a855d
commit 6b303a13eb
7 changed files with 290 additions and 94 deletions
+4
View File
@@ -2683,6 +2683,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.modelRouterService;
}
getModelConfigService(): ModelConfigService {
return this.modelConfigService;
}
getModelAvailabilityService(): ModelAvailabilityService {
return this.modelAvailabilityService;
}
+5
View File
@@ -49,6 +49,10 @@ export * from './scheduler/tool-executor.js';
export * from './scheduler/policy.js';
export * from './core/recordingContentGenerator.js';
// Export Routing
export * from './routing/routingStrategy.js';
export * from './routing/modelRouterService.js';
export * from './fallback/types.js';
export * from './fallback/handler.js';
@@ -132,6 +136,7 @@ export * from './services/FolderTrustDiscoveryService.js';
export * from './services/chatRecordingService.js';
export * from './services/fileSystemService.js';
export * from './services/sandboxedFileSystemService.js';
export * from './services/modelConfigService.js';
export * from './sandbox/windows/WindowsSandboxManager.js';
export * from './services/sessionSummaryUtils.js';
export * from './context/contextManager.js';
@@ -1018,4 +1018,41 @@ describe('ModelConfigService', () => {
expect(retry.generateContentConfig.temperature).toBe(1.0);
});
});
describe('getAvailableModelOptions', () => {
it('should filter out Pro models when hasAccessToProModel is false', () => {
const config: ModelConfigServiceConfig = {
modelDefinitions: {
'gemini-3-pro': { isVisible: true, tier: 'pro' },
'gemini-3-flash': { isVisible: true, tier: 'flash' },
},
};
const service = new ModelConfigService(config);
const options = service.getAvailableModelOptions({
hasAccessToProModel: false,
});
expect(options.map((o) => o.modelId)).not.toContain('gemini-3-pro');
expect(options.map((o) => o.modelId)).toContain('gemini-3-flash');
});
it('should include Pro models when hasAccessToProModel is true or undefined', () => {
const config: ModelConfigServiceConfig = {
modelDefinitions: {
'gemini-3-pro': { isVisible: true, tier: 'pro' },
},
};
const service = new ModelConfigService(config);
const optionsWithTrue = service.getAvailableModelOptions({
hasAccessToProModel: true,
});
expect(optionsWithTrue.map((o) => o.modelId)).toContain('gemini-3-pro');
const optionsWithUndefined = service.getAvailableModelOptions({});
expect(optionsWithUndefined.map((o) => o.modelId)).toContain(
'gemini-3-pro',
);
});
});
});
@@ -6,6 +6,12 @@
import type { GenerateContentConfig } from '@google/genai';
import type { ModelPolicy } from '../availability/modelPolicy.js';
import {
getDisplayString,
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
isProModel,
} from '../config/models.js';
// The primary key for the ModelConfig is the model string. However, we also
// support a secondary key to limit the override scope, typically an agent name.
@@ -93,6 +99,7 @@ export interface ResolutionContext {
useGemini3_1FlashLite?: boolean;
useCustomTools?: boolean;
hasAccessToPreview?: boolean;
hasAccessToProModel?: boolean;
requestedModel?: string;
}
@@ -135,6 +142,78 @@ export class ModelConfigService {
// TODO(12597): Process config to build a typed alias hierarchy.
constructor(private readonly config: ModelConfigServiceConfig) {}
/**
* Returns a standardized list of available model options based on the resolution context.
* This logic is shared across the TUI and ACP mode.
*/
getAvailableModelOptions(context: ResolutionContext): Array<{
modelId: string;
name: string;
description: string;
tier: string;
}> {
const definitions = this.config.modelDefinitions ?? {};
const shouldShowPreviewModels = context.hasAccessToPreview ?? false;
const useGemini31 = context.useGemini3_1 ?? false;
const useGemini31FlashLite = context.useGemini3_1FlashLite ?? false;
const mainOptions = Object.entries(definitions)
.filter(([_, m]) => {
if (m.isVisible !== true) return false;
if (m.isPreview && !shouldShowPreviewModels) return false;
if (m.tier !== 'auto') return false;
return true;
})
.map(([id, m]) => ({
modelId: id,
name: m.displayName ?? getDisplayString(id),
description:
id === 'auto-gemini-3' && useGemini31
? (m.dialogDescription ?? '').replace(
'gemini-3-pro',
'gemini-3.1-pro',
)
: (m.dialogDescription ?? ''),
tier: m.tier ?? 'auto',
}));
const manualOptions = Object.entries(definitions)
.filter(([id, m]) => {
if (m.isVisible !== true) return false;
if (m.isPreview && !shouldShowPreviewModels) return false;
if (m.tier === 'auto') return false;
if (context.hasAccessToProModel === false && isProModel(id))
return false;
if (id === PREVIEW_GEMINI_3_1_MODEL && !useGemini31) return false;
if (id === PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL && !useGemini31FlashLite)
return false;
return true;
})
.map(([id, m]) => {
const resolvedId = this.resolveModelId(id, context);
const titleId = this.resolveModelId(id, {
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
});
return {
modelId: resolvedId,
name: m.displayName ?? getDisplayString(titleId),
description: m.dialogDescription ?? '',
tier: m.tier ?? 'custom',
};
});
// Deduplicate manual options
const seen = new Set<string>();
const uniqueManualOptions = manualOptions.filter((option) => {
if (seen.has(option.modelId)) return false;
seen.add(option.modelId);
return true;
});
return [...mainOptions, ...uniqueManualOptions];
}
getModelDefinition(modelId: string): ModelDefinition | undefined {
const definition = this.config.modelDefinitions?.[modelId];
if (definition) {