From 74239c66c39a21d6a39cec998ce9c3ab7c199452 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 9 Apr 2026 15:49:24 +0200 Subject: [PATCH 1/2] Add optional model parameter to runSubagent tool --- .../tools/builtinTools/runSubagentTool.ts | 82 +++++-- .../builtinTools/runSubagentTool.test.ts | 231 ++++++++++++++++++ 2 files changed, 292 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index f953732e3414f..78a1e09302b51 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -19,7 +19,7 @@ import { IProductService } from '../../../../../../platform/product/common/produ import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js'; import { IChatProgress, IChatService } from '../../chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, GeneralPurposeAgentName } from '../../constants.js'; -import { ILanguageModelsService } from '../../languageModels.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../../participants/chatAgents.js'; import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js'; @@ -57,6 +57,7 @@ export interface IRunSubagentToolInputParams { prompt: string; description: string; agentName?: string; + model?: string; } export const RUN_SUBAGENT_MAX_NESTING_DEPTH = 5; @@ -118,6 +119,11 @@ export class RunSubagentTool extends Disposable implements IToolImpl { }; } + properties.model = { + type: 'string', + 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.', + }; + const required: string[] = ['prompt', 'description']; if (generalPurposeAgentEnabled) { required.push('agentName'); @@ -192,7 +198,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { resolvedModelName = cached.resolvedModelName; } else { // Fallback: resolve the model here if prepare didn't cache it - const resolved = this.resolveSubagentModel(subagent, invocation.modelId); + const resolved = this.resolveSubagentModel(subagent, invocation.modelId, args.model); modeModelId = resolved.modeModelId; resolvedModelName = resolved.resolvedModelName; } @@ -226,14 +232,16 @@ export class RunSubagentTool extends Disposable implements IToolImpl { throw new Error(`Requested agent '${subAgentName}' not found.${baseHint}${gpHint}`); } } else { - // No subagent name - clean up any cached entry and resolve model name from main model + // No subagent name - clean up any cached entry and resolve model from explicit parameter or main model const cached = this._resolvedModels.get(invocation.callId); if (cached) { this._resolvedModels.delete(invocation.callId); + modeModelId = cached.modeModelId; resolvedModelName = cached.resolvedModelName; } else { - const resolvedModelMetadata = modeModelId ? this.languageModelsService.lookupLanguageModel(modeModelId) : undefined; - resolvedModelName = resolvedModelMetadata?.name; + const resolved = this.resolveSubagentModel(undefined, invocation.modelId, args.model); + modeModelId = resolved.modeModelId; + resolvedModelName = resolved.resolvedModelName; } } @@ -415,37 +423,69 @@ export class RunSubagentTool extends Disposable implements IToolImpl { return agents.find(agent => agent.name === name); } + /** + * Returns the list of available model qualified names suitable for agent mode. + */ + private getAvailableModelNames(): string[] { + return this.languageModelsService.getLanguageModelIds() + .map(id => this.languageModelsService.lookupLanguageModel(id)) + .filter((m): m is ILanguageModelChatMetadata => + !!m + && ILanguageModelChatMetadata.suitableForAgentMode(m) + && m.isUserSelectable !== false + && !m.targetChatSessionType + ) + .map(m => ILanguageModelChatMetadata.asQualifiedName(m)); + } + /** * Resolves the model to be used by a subagent, applying multiplier-based * fallback to avoid using a more expensive model than the main agent. + * @param explicitModelQualifiedName Optional explicit model specified by the caller. + * If provided and not found, throws an error with the list of available models. */ - private resolveSubagentModel(subagent: ICustomAgent | undefined, mainModelId: string | undefined): { modeModelId: string | undefined; resolvedModelName: string | undefined } { + private resolveSubagentModel(subagent: ICustomAgent | undefined, mainModelId: string | undefined, explicitModelQualifiedName?: string): { modeModelId: string | undefined; resolvedModelName: string | undefined } { let modeModelId = mainModelId; + let explicitModelResolved = false; + + // Explicit model parameter takes highest priority + if (explicitModelQualifiedName) { + const lm = this.languageModelsService.lookupLanguageModelByQualifiedName(explicitModelQualifiedName); + if (lm?.identifier) { + modeModelId = lm.identifier; + explicitModelResolved = true; + } else { + // Model not found - throw error with available models + const available = this.getAvailableModelNames(); + const availableList = available.length > 0 ? `Available models: ${available.join(', ')}` : 'No models available.'; + throw new Error(`Requested model '${explicitModelQualifiedName}' not found. ${availableList}`); + } + } - if (subagent) { + if (subagent && !explicitModelResolved) { const modeModelQualifiedNames = subagent.model; if (modeModelQualifiedNames) { // Find the actual model identifier from the qualified name(s) - outer: for (const qualifiedName of modeModelQualifiedNames) { + for (const qualifiedName of modeModelQualifiedNames) { const lmByQualifiedName = this.languageModelsService.lookupLanguageModelByQualifiedName(qualifiedName); if (lmByQualifiedName?.identifier) { modeModelId = lmByQualifiedName.identifier; - break outer; + break; } } } + } - // If the subagent's model has a larger multiplier than the main agent's model, - // fall back to the main agent's model to avoid using a more expensive model. - if (modeModelId && modeModelId !== mainModelId) { - const mainModelMetadata = mainModelId ? this.languageModelsService.lookupLanguageModel(mainModelId) : undefined; - const subagentModelMetadata = this.languageModelsService.lookupLanguageModel(modeModelId); - const mainMultiplier = mainModelMetadata?.multiplierNumeric; - const subagentMultiplier = subagentModelMetadata?.multiplierNumeric; - if (mainMultiplier !== undefined && subagentMultiplier !== undefined && subagentMultiplier > mainMultiplier) { - 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.`); - modeModelId = mainModelId; - } + // If the resolved model has a larger multiplier than the main agent's model, + // fall back to the main agent's model to avoid using a more expensive model. + if (modeModelId && modeModelId !== mainModelId) { + const mainModelMetadata = mainModelId ? this.languageModelsService.lookupLanguageModel(mainModelId) : undefined; + const subagentModelMetadata = this.languageModelsService.lookupLanguageModel(modeModelId); + const mainMultiplier = mainModelMetadata?.multiplierNumeric; + const subagentMultiplier = subagentModelMetadata?.multiplierNumeric; + if (mainMultiplier !== undefined && subagentMultiplier !== undefined && subagentMultiplier > mainMultiplier) { + 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.`); + modeModelId = mainModelId; } } @@ -463,7 +503,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const subagent = (args.agentName && !isGeneralPurpose && customAgentsEnabled) ? await this.getSubAgentByName(args.agentName) : undefined; // Resolve the model early and cache it for invoke() - const resolved = this.resolveSubagentModel(subagent, context.modelId); + const resolved = this.resolveSubagentModel(subagent, context.modelId, args.model); this._resolvedModels.set(context.toolCallId, resolved); return { diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts index 19fb1f155f8b3..58460e59dfbd8 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts @@ -341,6 +341,7 @@ suite('RunSubagentTool', () => { isDefaultForLocation: {}, modelPickerCategory: undefined, multiplierNumeric, + capabilities: { toolCalling: true }, }; } @@ -356,6 +357,9 @@ suite('RunSubagentTool', () => { } const mockLanguageModelsService: Partial = { + getLanguageModelIds() { + return Array.from(opts.models.keys()); + }, lookupLanguageModel(modelId: string) { return opts.models.get(modelId); }, @@ -599,6 +603,233 @@ suite('RunSubagentTool', () => { }); }); + suite('explicit model parameter', () => { + function createMetadata(name: string, multiplierNumeric?: number): ILanguageModelChatMetadata { + return { + extension: new ExtensionIdentifier('test.extension'), + name, + id: name.toLowerCase().replace(/\s+/g, '-'), + vendor: 'TestVendor', + version: '1.0', + family: 'test', + maxInputTokens: 128000, + maxOutputTokens: 8192, + isDefaultForLocation: {}, + modelPickerCategory: undefined, + multiplierNumeric, + capabilities: { toolCalling: true }, + }; + } + + function createTool(opts: { + models: Map; + qualifiedNameMap?: Map; + customAgents?: ICustomAgent[]; + }) { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const promptsService = new MockPromptsService(); + if (opts.customAgents) { + promptsService.setCustomModes(opts.customAgents); + } + + const mockLanguageModelsService: Partial = { + getLanguageModelIds() { + return Array.from(opts.models.keys()); + }, + lookupLanguageModel(modelId: string) { + return opts.models.get(modelId); + }, + lookupLanguageModelByQualifiedName(qualifiedName: string) { + return opts.qualifiedNameMap?.get(qualifiedName); + }, + }; + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + mockToolsService, + mockLanguageModelsService as ILanguageModelsService, + new NullLogService(), + new TestConfigurationService({ [ChatConfiguration.SubagentToolCustomAgents]: true }), + promptsService, + {} as IInstantiationService, + {} as IProductService, + )); + + return tool; + } + + function createAgent(name: string, modelQualifiedNames?: string[]): ICustomAgent { + return { + uri: URI.parse(`file:///test/${name}.md`), + name, + description: `Agent ${name}`, + tools: ['tool1'], + model: modelQualifiedNames, + agentInstructions: { content: 'test', toolReferences: [] }, + source: { storage: PromptsStorage.local }, + target: Target.Undefined, + visibility: { userInvocable: true, agentInvocable: true } + }; + } + + test('model property is included in tool schema without enum', () => { + const models = new Map([ + ['model-1', createMetadata('GPT-4o')], + ['model-2', createMetadata('Claude Sonnet')], + ]); + + const tool = createTool({ models }); + const toolData = tool.getToolData(); + + assert.ok(toolData.inputSchema?.properties?.model, 'model should be in schema'); + assert.strictEqual(toolData.inputSchema?.properties?.model?.type, 'string'); + // No enum should be present - validation happens at runtime + assert.strictEqual(toolData.inputSchema?.properties?.model?.enum, undefined, 'model should not have an enum'); + }); + + test('resolves explicit model parameter without agentName', async () => { + const mainMeta = createMetadata('GPT-4o', 1); + const explicitMeta = createMetadata('Claude Sonnet', 1); + const models = new Map([ + ['main-model-id', mainMeta], + ['explicit-model-id', explicitMeta], + ]); + const qualifiedNameMap = new Map([ + ['Claude Sonnet (TestVendor)', { metadata: explicitMeta, identifier: 'explicit-model-id' }], + ]); + + const tool = createTool({ models, qualifiedNameMap }); + + const result = await tool.prepareToolInvocation({ + parameters: { prompt: 'test', description: 'test task', model: 'Claude Sonnet (TestVendor)' }, + toolCallId: 'model-call-1', + modelId: 'main-model-id', + chatSessionResource: URI.parse('test://session'), + }, CancellationToken.None); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'test task', + agentName: undefined, + prompt: 'test', + modelName: 'Claude Sonnet', + }); + }); + + test('explicit model overrides agent configured model', async () => { + const mainMeta = createMetadata('GPT-4o', 1); + const agentMeta = createMetadata('Agent Model', 1); + const explicitMeta = createMetadata('Claude Sonnet', 1); + const models = new Map([ + ['main-model-id', mainMeta], + ['agent-model-id', agentMeta], + ['explicit-model-id', explicitMeta], + ]); + const qualifiedNameMap = new Map([ + ['Agent Model (TestVendor)', { metadata: agentMeta, identifier: 'agent-model-id' }], + ['Claude Sonnet (TestVendor)', { metadata: explicitMeta, identifier: 'explicit-model-id' }], + ]); + + const agent = createAgent('MyAgent', ['Agent Model (TestVendor)']); + const tool = createTool({ models, qualifiedNameMap, customAgents: [agent] }); + + const result = await tool.prepareToolInvocation({ + parameters: { prompt: 'test', description: 'test task', agentName: 'MyAgent', model: 'Claude Sonnet (TestVendor)' }, + toolCallId: 'model-call-2', + modelId: 'main-model-id', + chatSessionResource: URI.parse('test://session'), + }, CancellationToken.None); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'test task', + agentName: 'MyAgent', + prompt: 'test', + modelName: 'Claude Sonnet', + }); + }); + + test('falls back to main model when explicit model has higher multiplier', async () => { + const mainMeta = createMetadata('GPT-4o', 1); + const expensiveMeta = createMetadata('O3 Pro', 50); + const models = new Map([ + ['main-model-id', mainMeta], + ['expensive-model-id', expensiveMeta], + ]); + const qualifiedNameMap = new Map([ + ['O3 Pro (TestVendor)', { metadata: expensiveMeta, identifier: 'expensive-model-id' }], + ]); + + const tool = createTool({ models, qualifiedNameMap }); + + const result = await tool.prepareToolInvocation({ + parameters: { prompt: 'test', description: 'test task', model: 'O3 Pro (TestVendor)' }, + toolCallId: 'model-call-3', + modelId: 'main-model-id', + chatSessionResource: URI.parse('test://session'), + }, CancellationToken.None); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'test task', + agentName: undefined, + prompt: 'test', + modelName: 'GPT-4o', + }); + }); + + test('throws error with available models when explicit model is not found', async () => { + const mainMeta = createMetadata('GPT-4o', 1); + const otherMeta = createMetadata('Claude Sonnet', 1); + const models = new Map([ + ['main-model-id', mainMeta], + ['other-model-id', otherMeta], + ]); + + const tool = createTool({ models, qualifiedNameMap: new Map() }); + + await assert.rejects( + () => tool.prepareToolInvocation({ + parameters: { prompt: 'test', description: 'test task', model: 'Nonexistent Model (Vendor)' }, + toolCallId: 'model-call-4', + modelId: 'main-model-id', + chatSessionResource: URI.parse('test://session'), + }, CancellationToken.None), + (err: Error) => { + assert.ok(err.message.includes('Nonexistent Model (Vendor)')); + assert.ok(err.message.includes('not found')); + assert.ok(err.message.includes('Available models:')); + assert.ok(err.message.includes('GPT-4o (TestVendor)')); + assert.ok(err.message.includes('Claude Sonnet (TestVendor)')); + return true; + } + ); + }); + + test('throws error with no models message when no models are available', async () => { + const tool = createTool({ models: new Map(), qualifiedNameMap: new Map() }); + + await assert.rejects( + () => tool.prepareToolInvocation({ + parameters: { prompt: 'test', description: 'test task', model: 'Nonexistent Model (Vendor)' }, + toolCallId: 'model-call-5', + modelId: undefined, + chatSessionResource: URI.parse('test://session'), + }, CancellationToken.None), + (err: Error) => { + assert.ok(err.message.includes('Nonexistent Model (Vendor)')); + assert.ok(err.message.includes('not found')); + assert.ok(err.message.includes('No models available')); + return true; + } + ); + }); + }); + suite('nested subagent depth tracking', () => { /** * Creates a RunSubagentTool with mocked services suitable for invoke() testing. From dd9db6a220cb8a3d3172b790427450e1b3b1fbeb Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 9 Apr 2026 16:47:23 +0200 Subject: [PATCH 2/2] Refactor RunSubagentTool to enforce multiplier constraints on model selection and update related tests --- .../tools/builtinTools/runSubagentTool.ts | 102 +++++++++++++----- .../builtinTools/runSubagentTool.test.ts | 65 ++++++----- 2 files changed, 106 insertions(+), 61 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 78a1e09302b51..1e4990de7cda9 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -121,7 +121,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { properties.model = { type: 'string', - 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.', + description: 'Optional model for the subagent. Format: "Model Name (Vendor)", vendor is usually "copilot". Only use to enforce a specific model.', }; const required: string[] = ['prompt', 'description']; @@ -424,25 +424,77 @@ export class RunSubagentTool extends Disposable implements IToolImpl { } /** - * Returns the list of available model qualified names suitable for agent mode. + * Checks if a model exceeds the main model's cost tier based on multiplier. + * @returns An object with `exceeds: true` and a reason string if blocked, or `exceeds: false` if allowed. */ - private getAvailableModelNames(): string[] { - return this.languageModelsService.getLanguageModelIds() - .map(id => this.languageModelsService.lookupLanguageModel(id)) - .filter((m): m is ILanguageModelChatMetadata => - !!m - && ILanguageModelChatMetadata.suitableForAgentMode(m) - && m.isUserSelectable !== false - && !m.targetChatSessionType - ) - .map(m => ILanguageModelChatMetadata.asQualifiedName(m)); + private checkMultiplierConstraint(modelId: string, mainModelId: string | undefined): { exceeds: false } | { exceeds: true; reason: string } { + if (!mainModelId || modelId === mainModelId) { + return { exceeds: false }; + } + + const mainModelMetadata = this.languageModelsService.lookupLanguageModel(mainModelId); + const modelMetadata = this.languageModelsService.lookupLanguageModel(modelId); + const mainMultiplier = mainModelMetadata?.multiplierNumeric; + const modelMultiplier = modelMetadata?.multiplierNumeric; + + if (mainMultiplier !== undefined && modelMultiplier !== undefined && modelMultiplier > mainMultiplier) { + return { + exceeds: true, + reason: `exceeds the current model's cost tier (${modelMultiplier}x vs ${mainMultiplier}x)` + }; + } + + return { exceeds: false }; + } + + /** + * Returns information about available models for error messages. + * Includes which models are unavailable due to multiplier restrictions. + */ + private getAvailableModelsInfo(mainModelId: string | undefined): string { + const models = this.languageModelsService.getLanguageModelIds() + .map(id => ({ id, metadata: this.languageModelsService.lookupLanguageModel(id) })) + .filter((m): m is { id: string; metadata: ILanguageModelChatMetadata } => + !!m.metadata + && ILanguageModelChatMetadata.suitableForAgentMode(m.metadata) + && m.metadata.isUserSelectable !== false + && !m.metadata.targetChatSessionType + ); + + if (models.length === 0) { + return 'No models available.'; + } + + const available: string[] = []; + const unavailableDueToMultiplier: string[] = []; + + for (const { id, metadata } of models) { + const qualifiedName = ILanguageModelChatMetadata.asQualifiedName(metadata); + const check = this.checkMultiplierConstraint(id, mainModelId); + + if (check.exceeds) { + unavailableDueToMultiplier.push(qualifiedName); + } else { + available.push(qualifiedName); + } + } + + const parts: string[] = []; + if (available.length > 0) { + parts.push(`Available models: ${available.join(', ')}`); + } + if (unavailableDueToMultiplier.length > 0) { + parts.push(`Unavailable (exceeds current model's cost tier): ${unavailableDueToMultiplier.join(', ')}`); + } + + return parts.join('. ') || 'No models available.'; } /** - * Resolves the model to be used by a subagent, applying multiplier-based - * fallback to avoid using a more expensive model than the main agent. + * Resolves the model to be used by a subagent. * @param explicitModelQualifiedName Optional explicit model specified by the caller. - * If provided and not found, throws an error with the list of available models. + * If provided and not found or not allowed, throws an error with available models. + * @throws Error if the requested model is not found or exceeds the main model's cost tier. */ private resolveSubagentModel(subagent: ICustomAgent | undefined, mainModelId: string | undefined, explicitModelQualifiedName?: string): { modeModelId: string | undefined; resolvedModelName: string | undefined } { let modeModelId = mainModelId; @@ -456,9 +508,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { explicitModelResolved = true; } else { // Model not found - throw error with available models - const available = this.getAvailableModelNames(); - const availableList = available.length > 0 ? `Available models: ${available.join(', ')}` : 'No models available.'; - throw new Error(`Requested model '${explicitModelQualifiedName}' not found. ${availableList}`); + throw new Error(`Requested model '${explicitModelQualifiedName}' not found. ${this.getAvailableModelsInfo(mainModelId)}`); } } @@ -476,16 +526,12 @@ export class RunSubagentTool extends Disposable implements IToolImpl { } } - // If the resolved model has a larger multiplier than the main agent's model, - // fall back to the main agent's model to avoid using a more expensive model. - if (modeModelId && modeModelId !== mainModelId) { - const mainModelMetadata = mainModelId ? this.languageModelsService.lookupLanguageModel(mainModelId) : undefined; - const subagentModelMetadata = this.languageModelsService.lookupLanguageModel(modeModelId); - const mainMultiplier = mainModelMetadata?.multiplierNumeric; - const subagentMultiplier = subagentModelMetadata?.multiplierNumeric; - if (mainMultiplier !== undefined && subagentMultiplier !== undefined && subagentMultiplier > mainMultiplier) { - 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.`); - modeModelId = mainModelId; + // Check multiplier constraint - throw error if requested model exceeds main model's cost tier + if (modeModelId) { + const check = this.checkMultiplierConstraint(modeModelId, mainModelId); + if (check.exceeds) { + const modelMetadata = this.languageModelsService.lookupLanguageModel(modeModelId); + throw new Error(`Requested model '${modelMetadata?.name}' ${check.reason}. ${this.getAvailableModelsInfo(mainModelId)}`); } } diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts index 58460e59dfbd8..5a08e498bdfdc 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts @@ -397,7 +397,7 @@ suite('RunSubagentTool', () => { }; } - test('falls back to main model when subagent model has higher multiplier', async () => { + test('throws error when subagent model has higher multiplier', async () => { const mainMeta = createMetadata('GPT-4o', 1); const expensiveMeta = createMetadata('O3 Pro', 50); const models = new Map([ @@ -411,22 +411,21 @@ suite('RunSubagentTool', () => { const agent = createAgent('ExpensiveAgent', ['O3 Pro (TestVendor)']); const tool = createTool({ models, qualifiedNameMap, customAgents: [agent] }); - const result = await tool.prepareToolInvocation({ - parameters: { prompt: 'test', description: 'test task', agentName: 'ExpensiveAgent' }, - toolCallId: 'call-1', - modelId: 'main-model-id', - chatSessionResource: URI.parse('test://session'), - }, CancellationToken.None); - - assert.ok(result); - // Should fall back to the main model's name, not the expensive model - assert.deepStrictEqual(result.toolSpecificData, { - kind: 'subagent', - description: 'test task', - agentName: 'ExpensiveAgent', - prompt: 'test', - modelName: 'GPT-4o', - }); + await assert.rejects( + () => tool.prepareToolInvocation({ + parameters: { prompt: 'test', description: 'test task', agentName: 'ExpensiveAgent' }, + toolCallId: 'call-1', + modelId: 'main-model-id', + chatSessionResource: URI.parse('test://session'), + }, CancellationToken.None), + (err: Error) => { + assert.ok(err.message.includes('O3 Pro')); + assert.ok(err.message.includes('exceeds')); + assert.ok(err.message.includes('cost tier')); + assert.ok(err.message.includes('Unavailable')); + return true; + } + ); }); test('uses subagent model when it has equal multiplier', async () => { @@ -752,7 +751,7 @@ suite('RunSubagentTool', () => { }); }); - test('falls back to main model when explicit model has higher multiplier', async () => { + test('throws error when explicit model has higher multiplier', async () => { const mainMeta = createMetadata('GPT-4o', 1); const expensiveMeta = createMetadata('O3 Pro', 50); const models = new Map([ @@ -765,21 +764,21 @@ suite('RunSubagentTool', () => { const tool = createTool({ models, qualifiedNameMap }); - const result = await tool.prepareToolInvocation({ - parameters: { prompt: 'test', description: 'test task', model: 'O3 Pro (TestVendor)' }, - toolCallId: 'model-call-3', - modelId: 'main-model-id', - chatSessionResource: URI.parse('test://session'), - }, CancellationToken.None); - - assert.ok(result); - assert.deepStrictEqual(result.toolSpecificData, { - kind: 'subagent', - description: 'test task', - agentName: undefined, - prompt: 'test', - modelName: 'GPT-4o', - }); + await assert.rejects( + () => tool.prepareToolInvocation({ + parameters: { prompt: 'test', description: 'test task', model: 'O3 Pro (TestVendor)' }, + toolCallId: 'model-call-3', + modelId: 'main-model-id', + chatSessionResource: URI.parse('test://session'), + }, CancellationToken.None), + (err: Error) => { + assert.ok(err.message.includes('O3 Pro')); + assert.ok(err.message.includes('exceeds')); + assert.ok(err.message.includes('cost tier')); + assert.ok(err.message.includes('Unavailable')); + return true; + } + ); }); test('throws error with available models when explicit model is not found', async () => {