Skip to content

Commit 0cd8f30

Browse files
authored
Merge branch 'main' into alexr00/honest-ferret
2 parents fbf92e8 + 63c4230 commit 0cd8f30

3 files changed

Lines changed: 359 additions & 43 deletions

File tree

extensions/copilot/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts

Lines changed: 109 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { IProductService } from '../../../../../../platform/product/common/produ
1919
import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js';
2020
import { IChatProgress, IChatService } from '../../chatService/chatService.js';
2121
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, GeneralPurposeAgentName } from '../../constants.js';
22-
import { ILanguageModelsService } from '../../languageModels.js';
22+
import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js';
2323
import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js';
2424
import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../../participants/chatAgents.js';
2525
import { 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

6263
export 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

Comments
 (0)