Path: blob/main/extensions/copilot/src/extension/intents/test/node/toolCallingLoopUsage.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Raw } from '@vscode/prompt-tsx';6import { afterEach, beforeEach, describe, expect, it } from 'vitest';7import type { ChatRequest, LanguageModelChat, LanguageModelToolInformation } from 'vscode';8import { ChatFetchResponseType, ChatResponse } from '../../../../platform/chat/common/commonTypes';9import { toTextPart } from '../../../../platform/chat/common/globalStringUtils';10import { ITestingServicesAccessor } from '../../../../platform/test/node/services';11import { ChatResponseStreamImpl } from '../../../../util/common/chatResponseStreamImpl';12import { CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';13import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';14import { generateUuid } from '../../../../util/vs/base/common/uuid';15import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';16import { Conversation, Turn } from '../../../prompt/common/conversation';17import { IBuildPromptContext } from '../../../prompt/common/intents';18import { IBuildPromptResult, nullRenderPromptResult } from '../../../prompt/node/intents';19import { createExtensionUnitTestingServices } from '../../../test/node/services';20import { IToolCallingLoopOptions, ToolCallingLoop } from '../../node/toolCallingLoop';2122class UsageCapturingStream extends ChatResponseStreamImpl {23public readonly usages: Array<{ promptTokens: number; completionTokens: number }>;2425constructor() {26const usages: Array<{ promptTokens: number; completionTokens: number }> = [];27super(28() => { },29() => { },30undefined,31undefined,32undefined,33() => Promise.resolve(undefined),34(usage) => {35usages.push({36promptTokens: usage.promptTokens,37completionTokens: usage.completionTokens38});39}40);41this.usages = usages;42}43}4445class UsageTestToolCallingLoop extends ToolCallingLoop<IToolCallingLoopOptions> {46protected override async buildPrompt(_buildPromptContext: IBuildPromptContext): Promise<IBuildPromptResult> {47return {48...nullRenderPromptResult(),49messages: [{ role: Raw.ChatRole.User, content: [toTextPart('hello world')] }],50};51}5253protected override async getAvailableTools(): Promise<LanguageModelToolInformation[]> {54return [];55}5657protected override async fetch(): Promise<ChatResponse> {58return {59type: ChatFetchResponseType.Success,60value: 'test-response',61requestId: 'request-id',62serverRequestId: undefined,63usage: {64prompt_tokens: 100,65completion_tokens: 20,66total_tokens: 12067},68resolvedModel: 'gpt-4.1'69};70}71}7273const chatPanelLocation: ChatRequest['location'] = 1;7475function createMockChatRequest(overrides: Partial<ChatRequest> = {}): ChatRequest {76return {77prompt: 'test prompt',78command: undefined,79references: [],80location: chatPanelLocation,81location2: undefined,82attempt: 0,83enableCommandDetection: false,84isParticipantDetected: false,85toolReferences: [],86toolInvocationToken: {} as ChatRequest['toolInvocationToken'],87model: { family: 'test' } as LanguageModelChat,88tools: new Map(),89id: generateUuid(),90sessionId: generateUuid(),91sessionResource: {} as ChatRequest['sessionResource'],92hasHooksEnabled: false,93...overrides,94} satisfies ChatRequest;95}9697function createConversation(prompt: string): Conversation {98return new Conversation(generateUuid(), [99new Turn(generateUuid(), { type: 'user', message: prompt })100]);101}102103describe('ToolCallingLoop usage reporting', () => {104let disposables: DisposableStore;105let accessor: ITestingServicesAccessor;106let instantiationService: IInstantiationService;107let tokenSource: CancellationTokenSource;108109beforeEach(() => {110disposables = new DisposableStore();111const serviceCollection = disposables.add(createExtensionUnitTestingServices());112accessor = serviceCollection.createTestingAccessor();113instantiationService = accessor.get(IInstantiationService);114tokenSource = new CancellationTokenSource();115disposables.add(tokenSource);116});117118afterEach(() => {119accessor.dispose();120disposables.dispose();121});122123it('reports usage for regular requests', async () => {124const request = createMockChatRequest();125const loop = instantiationService.createInstance(126UsageTestToolCallingLoop,127{128conversation: createConversation(request.prompt),129toolCallLimit: 1,130request,131}132);133disposables.add(loop);134const stream = new UsageCapturingStream();135136await loop.runOne(stream, 0, tokenSource.token);137138expect(stream.usages).toEqual([{ promptTokens: 100, completionTokens: 20 }]);139});140141it('does not report usage for subagent requests', async () => {142const request = createMockChatRequest({143subAgentInvocationId: 'subagent-usage-test',144subAgentName: 'search'145});146const loop = instantiationService.createInstance(147UsageTestToolCallingLoop,148{149conversation: createConversation(request.prompt),150toolCallLimit: 1,151request,152}153);154disposables.add(loop);155const stream = new UsageCapturingStream();156157await loop.runOne(stream, 0, tokenSource.token);158159expect(stream.usages).toHaveLength(0);160});161});162163164