Skip to content

Commit b947d0b

Browse files
committed
Refactor RunSubagentTool to enforce multiplier constraints on model selection and update related tests
1 parent 394d71b commit b947d0b

File tree

2 files changed

+106
-61
lines changed

2 files changed

+106
-61
lines changed

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

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
121121

122122
properties.model = {
123123
type: 'string',
124-
description: 'Optional model for the subagent. Use a fast model for simple lookups and searches, or a reasoning model for complex analysis. Format: "Model Name (Vendor)" - vendor is usually "copilot". If not provided, uses the current model.',
124+
description: 'Optional model for the subagent. Format: "Model Name (Vendor)", vendor is usually "copilot". Only use to enforce a specific model.',
125125
};
126126

127127
const required: string[] = ['prompt', 'description'];
@@ -424,25 +424,77 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
424424
}
425425

426426
/**
427-
* Returns the list of available model qualified names suitable for agent mode.
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.
428429
*/
429-
private getAvailableModelNames(): string[] {
430-
return this.languageModelsService.getLanguageModelIds()
431-
.map(id => this.languageModelsService.lookupLanguageModel(id))
432-
.filter((m): m is ILanguageModelChatMetadata =>
433-
!!m
434-
&& ILanguageModelChatMetadata.suitableForAgentMode(m)
435-
&& m.isUserSelectable !== false
436-
&& !m.targetChatSessionType
437-
)
438-
.map(m => ILanguageModelChatMetadata.asQualifiedName(m));
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.';
439491
}
440492

441493
/**
442-
* Resolves the model to be used by a subagent, applying multiplier-based
443-
* fallback to avoid using a more expensive model than the main agent.
494+
* Resolves the model to be used by a subagent.
444495
* @param explicitModelQualifiedName Optional explicit model specified by the caller.
445-
* If provided and not found, throws an error with the list of available models.
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.
446498
*/
447499
private resolveSubagentModel(subagent: ICustomAgent | undefined, mainModelId: string | undefined, explicitModelQualifiedName?: string): { modeModelId: string | undefined; resolvedModelName: string | undefined } {
448500
let modeModelId = mainModelId;
@@ -456,9 +508,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
456508
explicitModelResolved = true;
457509
} else {
458510
// Model not found - throw error with available models
459-
const available = this.getAvailableModelNames();
460-
const availableList = available.length > 0 ? `Available models: ${available.join(', ')}` : 'No models available.';
461-
throw new Error(`Requested model '${explicitModelQualifiedName}' not found. ${availableList}`);
511+
throw new Error(`Requested model '${explicitModelQualifiedName}' not found. ${this.getAvailableModelsInfo(mainModelId)}`);
462512
}
463513
}
464514

@@ -476,16 +526,12 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
476526
}
477527
}
478528

479-
// If the resolved model has a larger multiplier than the main agent's model,
480-
// fall back to the main agent's model to avoid using a more expensive model.
481-
if (modeModelId && modeModelId !== mainModelId) {
482-
const mainModelMetadata = mainModelId ? this.languageModelsService.lookupLanguageModel(mainModelId) : undefined;
483-
const subagentModelMetadata = this.languageModelsService.lookupLanguageModel(modeModelId);
484-
const mainMultiplier = mainModelMetadata?.multiplierNumeric;
485-
const subagentMultiplier = subagentModelMetadata?.multiplierNumeric;
486-
if (mainMultiplier !== undefined && subagentMultiplier !== undefined && subagentMultiplier > mainMultiplier) {
487-
this.logService.warn(`[RunSubagentTool] Requested model '${subagentModelMetadata?.name}' (multiplier: ${subagentMultiplier}) has a larger multiplier than the main agent model '${mainModelMetadata?.name}' (multiplier: ${mainMultiplier}). Falling back to the main agent model.`);
488-
modeModelId = mainModelId;
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)}`);
489535
}
490536
}
491537

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

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ suite('RunSubagentTool', () => {
397397
};
398398
}
399399

400-
test('falls back to main model when subagent model has higher multiplier', async () => {
400+
test('throws error when subagent model has higher multiplier', async () => {
401401
const mainMeta = createMetadata('GPT-4o', 1);
402402
const expensiveMeta = createMetadata('O3 Pro', 50);
403403
const models = new Map([
@@ -411,22 +411,21 @@ suite('RunSubagentTool', () => {
411411
const agent = createAgent('ExpensiveAgent', ['O3 Pro (TestVendor)']);
412412
const tool = createTool({ models, qualifiedNameMap, customAgents: [agent] });
413413

414-
const result = await tool.prepareToolInvocation({
415-
parameters: { prompt: 'test', description: 'test task', agentName: 'ExpensiveAgent' },
416-
toolCallId: 'call-1',
417-
modelId: 'main-model-id',
418-
chatSessionResource: URI.parse('test://session'),
419-
}, CancellationToken.None);
420-
421-
assert.ok(result);
422-
// Should fall back to the main model's name, not the expensive model
423-
assert.deepStrictEqual(result.toolSpecificData, {
424-
kind: 'subagent',
425-
description: 'test task',
426-
agentName: 'ExpensiveAgent',
427-
prompt: 'test',
428-
modelName: 'GPT-4o',
429-
});
414+
await assert.rejects(
415+
() => tool.prepareToolInvocation({
416+
parameters: { prompt: 'test', description: 'test task', agentName: 'ExpensiveAgent' },
417+
toolCallId: 'call-1',
418+
modelId: 'main-model-id',
419+
chatSessionResource: URI.parse('test://session'),
420+
}, CancellationToken.None),
421+
(err: Error) => {
422+
assert.ok(err.message.includes('O3 Pro'));
423+
assert.ok(err.message.includes('exceeds'));
424+
assert.ok(err.message.includes('cost tier'));
425+
assert.ok(err.message.includes('Unavailable'));
426+
return true;
427+
}
428+
);
430429
});
431430

432431
test('uses subagent model when it has equal multiplier', async () => {
@@ -752,7 +751,7 @@ suite('RunSubagentTool', () => {
752751
});
753752
});
754753

755-
test('falls back to main model when explicit model has higher multiplier', async () => {
754+
test('throws error when explicit model has higher multiplier', async () => {
756755
const mainMeta = createMetadata('GPT-4o', 1);
757756
const expensiveMeta = createMetadata('O3 Pro', 50);
758757
const models = new Map([
@@ -765,21 +764,21 @@ suite('RunSubagentTool', () => {
765764

766765
const tool = createTool({ models, qualifiedNameMap });
767766

768-
const result = await tool.prepareToolInvocation({
769-
parameters: { prompt: 'test', description: 'test task', model: 'O3 Pro (TestVendor)' },
770-
toolCallId: 'model-call-3',
771-
modelId: 'main-model-id',
772-
chatSessionResource: URI.parse('test://session'),
773-
}, CancellationToken.None);
774-
775-
assert.ok(result);
776-
assert.deepStrictEqual(result.toolSpecificData, {
777-
kind: 'subagent',
778-
description: 'test task',
779-
agentName: undefined,
780-
prompt: 'test',
781-
modelName: 'GPT-4o',
782-
});
767+
await assert.rejects(
768+
() => tool.prepareToolInvocation({
769+
parameters: { prompt: 'test', description: 'test task', model: 'O3 Pro (TestVendor)' },
770+
toolCallId: 'model-call-3',
771+
modelId: 'main-model-id',
772+
chatSessionResource: URI.parse('test://session'),
773+
}, CancellationToken.None),
774+
(err: Error) => {
775+
assert.ok(err.message.includes('O3 Pro'));
776+
assert.ok(err.message.includes('exceeds'));
777+
assert.ok(err.message.includes('cost tier'));
778+
assert.ok(err.message.includes('Unavailable'));
779+
return true;
780+
}
781+
);
783782
});
784783

785784
test('throws error with available models when explicit model is not found', async () => {

0 commit comments

Comments
 (0)