Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions src/extension/mcp/vscode-node/mcpToolCallingLoop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ export class McpToolCallingLoop extends ToolCallingLoop<IMcpToolCallingLoopOptio
}

private async getEndpoint(request: ChatRequest) {
let endpoint = await this.endpointProvider.getChatEndpoint(this.options.request);
if (!endpoint.supportsToolCalls) {
endpoint = await this.endpointProvider.getChatEndpoint('gpt-4.1');
}
return endpoint;
return await this.endpointProvider.getChatEndpoint(this.options.request);
}

protected async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress<ChatResponseReferencePart | ChatResponseProgressPart>, token: CancellationToken): Promise<IBuildPromptResult> {
Expand Down
6 changes: 1 addition & 5 deletions src/extension/prompt/node/codebaseToolCalling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,7 @@ export class CodebaseToolCallingLoop extends ToolCallingLoop<ICodebaseToolCallin
}

private async getEndpoint(request: ChatRequest) {
let endpoint = await this.endpointProvider.getChatEndpoint(this.options.request);
if (!endpoint.supportsToolCalls) {
endpoint = await this.endpointProvider.getChatEndpoint('gpt-4.1');
}
return endpoint;
return await this.endpointProvider.getChatEndpoint(this.options.request);
}

protected async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress<ChatResponseReferencePart | ChatResponseProgressPart>, token: CancellationToken): Promise<IBuildPromptResult> {
Expand Down
6 changes: 1 addition & 5 deletions src/extension/prompt/node/searchSubagentToolCallingLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@ export class SearchSubagentToolCallingLoop extends ToolCallingLoop<ISearchSubage
}

private async getEndpoint(request: ChatRequest) {
let endpoint = await this.endpointProvider.getChatEndpoint(this.options.request);
if (!endpoint.supportsToolCalls) {
endpoint = await this.endpointProvider.getChatEndpoint('gpt-4.1');
}
return endpoint;
return await this.endpointProvider.getChatEndpoint(this.options.request);
}

protected async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress<ChatResponseReferencePart | ChatResponseProgressPart>, token: CancellationToken): Promise<IBuildPromptResult> {
Expand Down
113 changes: 113 additions & 0 deletions src/extension/prompt/node/test/searchSubagentToolCallingLoop.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';
import { IChatEndpoint } from '../../../../platform/networking/common/networking';
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { createExtensionUnitTestingServices } from '../../../test/node/services';
import { Conversation, Turn } from '../../common/conversation';
import { ISearchSubagentToolCallingLoopOptions, SearchSubagentToolCallingLoop } from '../searchSubagentToolCallingLoop';

describe('SearchSubagentToolCallingLoop', () => {
let store: DisposableStore;
let instantiationService: IInstantiationService;
let endpointProvider: IEndpointProvider;
let mockEndpoint: IChatEndpoint;

beforeEach(() => {
store = new DisposableStore();

// Create testing services
const serviceCollection = store.add(createExtensionUnitTestingServices(store));
const accessor = serviceCollection.createTestingAccessor();
instantiationService = accessor.get(IInstantiationService);

// Create mock endpoint that simulates Gemini 3 Pro with supportsToolCalls = false
mockEndpoint = {
family: 'gemini',
model: 'gemini-3-pro-preview',
name: 'Gemini 3 Pro',
version: '1.0',
maxOutputTokens: 8192,
modelMaxPromptTokens: 100000,
supportsToolCalls: false, // This simulates the bug scenario
supportsVision: true,
supportsPrediction: false,
showInModelPicker: true,
urlOrRequestMetadata: 'https://test-endpoint',
tokenizer: 0,
isDefault: false,
isFallback: false,
isPremium: false,
multiplier: 1,
} as IChatEndpoint;

// Get and override the endpoint provider to return our mock endpoint
endpointProvider = accessor.get(IEndpointProvider);
endpointProvider.getChatEndpoint = vi.fn(async (_requestOrModel: any) => {
// Always return the same endpoint - this ensures we don't fallback to GPT-4.1
return mockEndpoint;
});
});

it('should use the requested model even when supportsToolCalls is false', async () => {
// Arrange
const options: ISearchSubagentToolCallingLoopOptions = {
toolCallLimit: 4,
conversation: new Conversation('test-session', [
new Turn('turn-1', { type: 'user', message: 'test query' })
]),
request: {} as any,
location: 7 as any, // ChatLocation.Panel
promptText: 'test query',
};

const loop = instantiationService.createInstance(SearchSubagentToolCallingLoop, options);

// Act - call the private getEndpoint method via reflection to test the behavior
const getEndpoint = (loop as any).getEndpoint.bind(loop);
const endpoint = await getEndpoint(options.request);

// Assert
// The endpoint should be our mock Gemini endpoint, not GPT-4.1
expect(endpoint.model).toBe('gemini-3-pro-preview');
expect(endpoint.family).toBe('gemini');

// Verify that getChatEndpoint was called with the original request
expect(endpointProvider.getChatEndpoint).toHaveBeenCalledWith(options.request);

// Verify that it was NOT called with 'gpt-4.1' (the old fallback behavior)
const calls = (endpointProvider.getChatEndpoint as any).mock.calls;
expect(calls.every((call: any) => call[0] !== 'gpt-4.1')).toBe(true);
});

it('should use the requested model with supportsToolCalls true', async () => {
// Arrange - Update mock endpoint to support tool calls
mockEndpoint.supportsToolCalls = true;

const options: ISearchSubagentToolCallingLoopOptions = {
toolCallLimit: 4,
conversation: new Conversation('test-session', [
new Turn('turn-1', { type: 'user', message: 'test query' })
]),
request: {} as any,
location: 7 as any, // ChatLocation.Panel
promptText: 'test query',
};

const loop = instantiationService.createInstance(SearchSubagentToolCallingLoop, options);

// Act
const getEndpoint = (loop as any).getEndpoint.bind(loop);
const endpoint = await getEndpoint(options.request);

// Assert
expect(endpoint.model).toBe('gemini-3-pro-preview');
expect(endpoint.family).toBe('gemini');
expect(endpoint.supportsToolCalls).toBe(true);
});
});