Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/base/spyingChatMLFetcher.ts
13389 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import { Raw } from '@vscode/prompt-tsx';
6
import type { CancellationToken } from 'vscode';
7
import { AbstractChatMLFetcher } from '../../src/extension/prompt/node/chatMLFetcher';
8
import { IChatMLFetcher, IFetchMLOptions } from '../../src/platform/chat/common/chatMLFetcher';
9
import { ChatResponses } from '../../src/platform/chat/common/commonTypes';
10
import { IConversationOptions } from '../../src/platform/chat/common/conversationOptions';
11
import { roleToString } from '../../src/platform/chat/common/globalStringUtils';
12
import { FinishedCallback, ICopilotToolCall } from '../../src/platform/networking/common/fetch';
13
import { APIUsage } from '../../src/platform/networking/common/openai';
14
import { TaskQueue } from '../../src/util/common/async';
15
import { coalesce } from '../../src/util/vs/base/common/arrays';
16
import { isDisposable } from '../../src/util/vs/base/common/lifecycle';
17
import { StopWatch } from '../../src/util/vs/base/common/stopwatch';
18
import { SyncDescriptor } from '../../src/util/vs/platform/instantiation/common/descriptors';
19
import { IInstantiationService } from '../../src/util/vs/platform/instantiation/common/instantiation';
20
import { InterceptedRequest, ISerialisedChatResponse } from '../simulation/shared/sharedTypes';
21
import { CacheInfo, TestRunCacheInfo } from '../testExecutor';
22
import { ResponseWithMeta } from './cachingChatMLFetcher';
23
24
export class FetchRequestCollector {
25
public readonly _interceptedRequests: InterceptedRequest[] = [];
26
27
public get interceptedRequests(): readonly InterceptedRequest[] {
28
return this._interceptedRequests;
29
}
30
31
private readonly _pendingRequests = new TaskQueue();
32
private readonly _scheduledRequests: Promise<void>[] = [];
33
34
public addInterceptedRequest(requestPromise: Promise<InterceptedRequest>): void {
35
this._scheduledRequests.push(this._pendingRequests.schedule(async () => {
36
try {
37
const request = await requestPromise;
38
this._interceptedRequests.push(request);
39
} catch (err) {
40
// ignore errors here- the error will be thrown out of the ChatMLFetcher and handled
41
}
42
}));
43
}
44
45
/**
46
* Intercepted requests are async. This method waits for all pending requests to complete.
47
*/
48
public async complete(): Promise<void> {
49
await Promise.all(this._scheduledRequests);
50
}
51
52
public get contentFilterCount(): number {
53
return this.interceptedRequests.filter(x => x.response.type === 'filtered').length;
54
}
55
56
public get usage(): APIUsage {
57
// Have to extract this to give it an explicit type or TS is confused
58
const initial: APIUsage = { completion_tokens: 0, prompt_tokens: 0, total_tokens: 0, prompt_tokens_details: { cached_tokens: 0 } };
59
return this.interceptedRequests.reduce((p, c): APIUsage => {
60
const initialUsage: APIUsage = { completion_tokens: 0, prompt_tokens: 0, total_tokens: 0, prompt_tokens_details: { cached_tokens: 0 } };
61
const cUsage = c.response.usage || initialUsage;
62
return {
63
completion_tokens: p.completion_tokens + cUsage.completion_tokens,
64
prompt_tokens: p.prompt_tokens + cUsage.prompt_tokens,
65
total_tokens: p.total_tokens + cUsage.total_tokens,
66
prompt_tokens_details: {
67
cached_tokens: (p.prompt_tokens_details?.cached_tokens ?? 0) + (cUsage.prompt_tokens_details?.cached_tokens ?? 0),
68
}
69
};
70
}, initial);
71
}
72
73
public get averageRequestDuration(): number {
74
const requestDurations = coalesce(this.interceptedRequests.map(r => r.response.cacheMetadata?.requestDuration));
75
return requestDurations.reduce((sum, duration) => sum + duration, 0) / requestDurations.length;
76
}
77
78
public get hasCacheMiss(): boolean {
79
return this.interceptedRequests.some(x => x.response.isCacheHit === false);
80
}
81
82
public get cacheInfo(): TestRunCacheInfo {
83
return coalesce(this.interceptedRequests.map(r => r.cacheKey)).map(key => ({ type: 'request', key } satisfies CacheInfo));
84
}
85
}
86
87
export class SpyingChatMLFetcher extends AbstractChatMLFetcher {
88
89
private readonly fetcher: IChatMLFetcher;
90
91
public get interceptedRequests(): readonly InterceptedRequest[] {
92
return this.requestCollector.interceptedRequests;
93
}
94
95
public get contentFilterCount(): number {
96
return this.requestCollector.contentFilterCount;
97
}
98
99
constructor(
100
public readonly requestCollector: FetchRequestCollector,
101
fetcherDesc: SyncDescriptor<IChatMLFetcher>,
102
@IInstantiationService instantiationService: IInstantiationService,
103
@IConversationOptions options: IConversationOptions,
104
) {
105
super(options);
106
this.fetcher = instantiationService.createInstance(fetcherDesc);
107
}
108
109
public override dispose(): void {
110
super.dispose();
111
if (isDisposable(this.fetcher)) {
112
this.fetcher.dispose();
113
}
114
}
115
116
override async fetchMany(opts: IFetchMLOptions, token: CancellationToken): Promise<ChatResponses> {
117
118
const toolCalls: ICopilotToolCall[] = [];
119
const captureToolCallsCb: FinishedCallback = async (text, idx, delta) => {
120
if (delta.copilotToolCalls) {
121
toolCalls.push(...delta.copilotToolCalls);
122
}
123
if (opts.finishedCb) {
124
return opts.finishedCb(text, idx, delta);
125
}
126
};
127
128
const respPromise = this.fetcher.fetchMany({ ...opts, finishedCb: captureToolCallsCb }, token);
129
130
const sw = new StopWatch(false);
131
this.requestCollector.addInterceptedRequest(respPromise.then(resp => {
132
let cacheKey: string | undefined;
133
if (typeof (resp as ResponseWithMeta).cacheKey === 'string') {
134
cacheKey = (resp as ResponseWithMeta).cacheKey;
135
}
136
(resp as ISerialisedChatResponse).copilotFunctionCalls = toolCalls;
137
return new InterceptedRequest(opts.messages.map(message => {
138
return {
139
role: roleToString(message.role),
140
content: message.content,
141
tool_call_id: message.role === Raw.ChatRole.Tool ? message.toolCallId : undefined,
142
tool_calls: message.role === Raw.ChatRole.Assistant ? message.toolCalls : undefined,
143
name: message.name,
144
};
145
}), opts.requestOptions, resp, cacheKey, opts.endpoint.model, sw.elapsed());
146
}));
147
148
return await respPromise;
149
}
150
}
151
152