@@ -19,7 +19,7 @@ import { IProductService } from '../../../../../../platform/product/common/produ
1919import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js' ;
2020import { IChatProgress , IChatService } from '../../chatService/chatService.js' ;
2121import { ChatAgentLocation , ChatConfiguration , ChatModeKind , GeneralPurposeAgentName } from '../../constants.js' ;
22- import { ILanguageModelsService } from '../../languageModels.js' ;
22+ import { ILanguageModelChatMetadata , ILanguageModelsService } from '../../languageModels.js' ;
2323import { ChatModel , IChatRequestModeInstructions } from '../../model/chatModel.js' ;
2424import { IChatAgentRequest , IChatAgentResult , IChatAgentService } from '../../participants/chatAgents.js' ;
2525import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js' ;
@@ -57,6 +57,7 @@ export interface IRunSubagentToolInputParams {
5757 prompt : string ;
5858 description : string ;
5959 agentName ?: string ;
60+ model ?: string ;
6061}
6162
6263export const RUN_SUBAGENT_MAX_NESTING_DEPTH = 5 ;
@@ -118,6 +119,11 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
118119 } ;
119120 }
120121
122+ properties . model = {
123+ type : 'string' ,
124+ description : 'Optional model for the subagent. Format: "Model Name (Vendor)", vendor is usually "copilot". Only use to enforce a specific model.' ,
125+ } ;
126+
121127 const required : string [ ] = [ 'prompt' , 'description' ] ;
122128 if ( generalPurposeAgentEnabled ) {
123129 required . push ( 'agentName' ) ;
@@ -192,7 +198,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
192198 resolvedModelName = cached . resolvedModelName ;
193199 } else {
194200 // Fallback: resolve the model here if prepare didn't cache it
195- const resolved = this . resolveSubagentModel ( subagent , invocation . modelId ) ;
201+ const resolved = this . resolveSubagentModel ( subagent , invocation . modelId , args . model ) ;
196202 modeModelId = resolved . modeModelId ;
197203 resolvedModelName = resolved . resolvedModelName ;
198204 }
@@ -226,14 +232,16 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
226232 throw new Error ( `Requested agent '${ subAgentName } ' not found.${ baseHint } ${ gpHint } ` ) ;
227233 }
228234 } else {
229- // No subagent name - clean up any cached entry and resolve model name from main model
235+ // No subagent name - clean up any cached entry and resolve model from explicit parameter or main model
230236 const cached = this . _resolvedModels . get ( invocation . callId ) ;
231237 if ( cached ) {
232238 this . _resolvedModels . delete ( invocation . callId ) ;
239+ modeModelId = cached . modeModelId ;
233240 resolvedModelName = cached . resolvedModelName ;
234241 } else {
235- const resolvedModelMetadata = modeModelId ? this . languageModelsService . lookupLanguageModel ( modeModelId ) : undefined ;
236- resolvedModelName = resolvedModelMetadata ?. name ;
242+ const resolved = this . resolveSubagentModel ( undefined , invocation . modelId , args . model ) ;
243+ modeModelId = resolved . modeModelId ;
244+ resolvedModelName = resolved . resolvedModelName ;
237245 }
238246 }
239247
@@ -416,36 +424,114 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
416424 }
417425
418426 /**
419- * Resolves the model to be used by a subagent, applying multiplier-based
420- * fallback to avoid using a more expensive model than the main agent .
427+ * Checks if a model exceeds the main model's cost tier based on multiplier.
428+ * @returns An object with `exceeds: true` and a reason string if blocked, or `exceeds: false` if allowed .
421429 */
422- private resolveSubagentModel ( subagent : ICustomAgent | undefined , mainModelId : string | undefined ) : { modeModelId : string | undefined ; resolvedModelName : string | undefined } {
430+ private checkMultiplierConstraint ( modelId : string , mainModelId : string | undefined ) : { exceeds : false } | { exceeds : true ; reason : string } {
431+ if ( ! mainModelId || modelId === mainModelId ) {
432+ return { exceeds : false } ;
433+ }
434+
435+ const mainModelMetadata = this . languageModelsService . lookupLanguageModel ( mainModelId ) ;
436+ const modelMetadata = this . languageModelsService . lookupLanguageModel ( modelId ) ;
437+ const mainMultiplier = mainModelMetadata ?. multiplierNumeric ;
438+ const modelMultiplier = modelMetadata ?. multiplierNumeric ;
439+
440+ if ( mainMultiplier !== undefined && modelMultiplier !== undefined && modelMultiplier > mainMultiplier ) {
441+ return {
442+ exceeds : true ,
443+ reason : `exceeds the current model's cost tier (${ modelMultiplier } x vs ${ mainMultiplier } x)`
444+ } ;
445+ }
446+
447+ return { exceeds : false } ;
448+ }
449+
450+ /**
451+ * Returns information about available models for error messages.
452+ * Includes which models are unavailable due to multiplier restrictions.
453+ */
454+ private getAvailableModelsInfo ( mainModelId : string | undefined ) : string {
455+ const models = this . languageModelsService . getLanguageModelIds ( )
456+ . map ( id => ( { id, metadata : this . languageModelsService . lookupLanguageModel ( id ) } ) )
457+ . filter ( ( m ) : m is { id : string ; metadata : ILanguageModelChatMetadata } =>
458+ ! ! m . metadata
459+ && ILanguageModelChatMetadata . suitableForAgentMode ( m . metadata )
460+ && m . metadata . isUserSelectable !== false
461+ && ! m . metadata . targetChatSessionType
462+ ) ;
463+
464+ if ( models . length === 0 ) {
465+ return 'No models available.' ;
466+ }
467+
468+ const available : string [ ] = [ ] ;
469+ const unavailableDueToMultiplier : string [ ] = [ ] ;
470+
471+ for ( const { id, metadata } of models ) {
472+ const qualifiedName = ILanguageModelChatMetadata . asQualifiedName ( metadata ) ;
473+ const check = this . checkMultiplierConstraint ( id , mainModelId ) ;
474+
475+ if ( check . exceeds ) {
476+ unavailableDueToMultiplier . push ( qualifiedName ) ;
477+ } else {
478+ available . push ( qualifiedName ) ;
479+ }
480+ }
481+
482+ const parts : string [ ] = [ ] ;
483+ if ( available . length > 0 ) {
484+ parts . push ( `Available models: ${ available . join ( ', ' ) } ` ) ;
485+ }
486+ if ( unavailableDueToMultiplier . length > 0 ) {
487+ parts . push ( `Unavailable (exceeds current model's cost tier): ${ unavailableDueToMultiplier . join ( ', ' ) } ` ) ;
488+ }
489+
490+ return parts . join ( '. ' ) || 'No models available.' ;
491+ }
492+
493+ /**
494+ * Resolves the model to be used by a subagent.
495+ * @param explicitModelQualifiedName Optional explicit model specified by the caller.
496+ * If provided and not found or not allowed, throws an error with available models.
497+ * @throws Error if the requested model is not found or exceeds the main model's cost tier.
498+ */
499+ private resolveSubagentModel ( subagent : ICustomAgent | undefined , mainModelId : string | undefined , explicitModelQualifiedName ?: string ) : { modeModelId : string | undefined ; resolvedModelName : string | undefined } {
423500 let modeModelId = mainModelId ;
501+ let explicitModelResolved = false ;
502+
503+ // Explicit model parameter takes highest priority
504+ if ( explicitModelQualifiedName ) {
505+ const lm = this . languageModelsService . lookupLanguageModelByQualifiedName ( explicitModelQualifiedName ) ;
506+ if ( lm ?. identifier ) {
507+ modeModelId = lm . identifier ;
508+ explicitModelResolved = true ;
509+ } else {
510+ // Model not found - throw error with available models
511+ throw new Error ( `Requested model '${ explicitModelQualifiedName } ' not found. ${ this . getAvailableModelsInfo ( mainModelId ) } ` ) ;
512+ }
513+ }
424514
425- if ( subagent ) {
515+ if ( subagent && ! explicitModelResolved ) {
426516 const modeModelQualifiedNames = subagent . model ;
427517 if ( modeModelQualifiedNames ) {
428518 // Find the actual model identifier from the qualified name(s)
429- outer: for ( const qualifiedName of modeModelQualifiedNames ) {
519+ for ( const qualifiedName of modeModelQualifiedNames ) {
430520 const lmByQualifiedName = this . languageModelsService . lookupLanguageModelByQualifiedName ( qualifiedName ) ;
431521 if ( lmByQualifiedName ?. identifier ) {
432522 modeModelId = lmByQualifiedName . identifier ;
433- break outer ;
523+ break ;
434524 }
435525 }
436526 }
527+ }
437528
438- // If the subagent's model has a larger multiplier than the main agent's model,
439- // fall back to the main agent's model to avoid using a more expensive model.
440- if ( modeModelId && modeModelId !== mainModelId ) {
441- const mainModelMetadata = mainModelId ? this . languageModelsService . lookupLanguageModel ( mainModelId ) : undefined ;
442- const subagentModelMetadata = this . languageModelsService . lookupLanguageModel ( modeModelId ) ;
443- const mainMultiplier = mainModelMetadata ?. multiplierNumeric ;
444- const subagentMultiplier = subagentModelMetadata ?. multiplierNumeric ;
445- if ( mainMultiplier !== undefined && subagentMultiplier !== undefined && subagentMultiplier > mainMultiplier ) {
446- this . logService . warn ( `[RunSubagentTool] Subagent '${ subagent . name } ' requested model '${ subagentModelMetadata ?. name } ' (multiplier: ${ subagentMultiplier } ) which has a larger multiplier than the main agent model '${ mainModelMetadata ?. name } ' (multiplier: ${ mainMultiplier } ). Falling back to the main agent model.` ) ;
447- modeModelId = mainModelId ;
448- }
529+ // Check multiplier constraint - throw error if requested model exceeds main model's cost tier
530+ if ( modeModelId ) {
531+ const check = this . checkMultiplierConstraint ( modeModelId , mainModelId ) ;
532+ if ( check . exceeds ) {
533+ const modelMetadata = this . languageModelsService . lookupLanguageModel ( modeModelId ) ;
534+ throw new Error ( `Requested model '${ modelMetadata ?. name } ' ${ check . reason } . ${ this . getAvailableModelsInfo ( mainModelId ) } ` ) ;
449535 }
450536 }
451537
@@ -463,7 +549,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
463549 const subagent = ( args . agentName && ! isGeneralPurpose && customAgentsEnabled ) ? await this . getSubAgentByName ( args . agentName ) : undefined ;
464550
465551 // Resolve the model early and cache it for invoke()
466- const resolved = this . resolveSubagentModel ( subagent , context . modelId ) ;
552+ const resolved = this . resolveSubagentModel ( subagent , context . modelId , args . model ) ;
467553 this . _resolvedModels . set ( context . toolCallId , resolved ) ;
468554
469555 return {
0 commit comments