Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/requestLogger/test/node/testRequestLogger.ts
13405 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
6
import type { RequestMetadata } from '@vscode/copilot-api';
7
import type { HTMLTracer, IChatEndpointInfo, RenderPromptResult } from '@vscode/prompt-tsx';
8
import type { LanguageModelToolResult2 } from 'vscode';
9
import { Emitter, Event } from '../../../../util/vs/base/common/event';
10
import { generateUuid } from '../../../../util/vs/base/common/uuid';
11
import { IModelAPIResponse } from '../../../endpoint/common/endpointProvider';
12
import { ThinkingData } from '../../../thinking/common/thinking';
13
import { CapturingToken } from '../../common/capturingToken';
14
import { ILoggedRequestInfo, LoggedInfo, LoggedInfoKind, LoggedRequest, LoggedRequestKind, resolveMarkdownContent } from '../../common/requestLogger';
15
import { AbstractRequestLogger } from '../../node/requestLogger';
16
17
/**
18
* A test implementation of IRequestLogger that stores logged requests for verification in tests.
19
* Unlike NullRequestLogger, this actually stores entries so they can be validated.
20
*/
21
export class TestRequestLogger extends AbstractRequestLogger {
22
private readonly _entries: LoggedInfo[] = [];
23
private readonly _onDidChangeRequests = new Emitter<void>();
24
public readonly onDidChangeRequests: Event<void> = this._onDidChangeRequests.event;
25
26
public override addPromptTrace(elementName: string, endpoint: IChatEndpointInfo, result: RenderPromptResult, trace: HTMLTracer): void {
27
const id = generateUuid().substring(0, 8);
28
this._entries.push(new TestLoggedElementInfo(id, elementName, result.tokenCount, endpoint.modelMaxPromptTokens, trace, this.currentRequest));
29
this._onDidChangeRequests.fire();
30
}
31
32
public addEntry(entry: LoggedRequest): void {
33
const id = generateUuid().substring(0, 8);
34
this._entries.push(new TestLoggedRequestInfo(id, entry, this.currentRequest));
35
this._onDidChangeRequests.fire();
36
}
37
38
public override getRequests(): LoggedInfo[] {
39
return [...this._entries];
40
}
41
42
public override getRequestById(id: string): LoggedInfo | undefined {
43
return this._entries.find(e => e.id === id);
44
}
45
46
public override logModelListCall(id: string, requestMetadata: RequestMetadata, models: IModelAPIResponse[]): void {
47
this.addEntry({
48
type: LoggedRequestKind.MarkdownContentRequest,
49
debugName: 'modelList',
50
startTimeMs: Date.now(),
51
icon: undefined,
52
markdownContent: `Model list call: ${models.length} models`,
53
isConversationRequest: false
54
});
55
}
56
57
public override logToolCall(id: string, name: string, args: unknown, response: LanguageModelToolResult2, thinking?: ThinkingData): void {
58
this._entries.push(new TestLoggedToolCall(id, name, args, response, this.currentRequest, Date.now(), thinking));
59
this._onDidChangeRequests.fire();
60
}
61
62
/**
63
* Clear all logged entries (useful between tests).
64
*/
65
public clear(): void {
66
this._entries.length = 0;
67
this._onDidChangeRequests.fire();
68
}
69
}
70
71
class TestLoggedElementInfo {
72
public readonly kind = LoggedInfoKind.Element;
73
74
constructor(
75
public readonly id: string,
76
public readonly name: string,
77
public readonly tokens: number,
78
public readonly maxTokens: number,
79
public readonly trace: HTMLTracer,
80
public readonly token: CapturingToken | undefined
81
) { }
82
83
toJSON(): object {
84
return {
85
id: this.id,
86
kind: 'element',
87
name: this.name,
88
tokens: this.tokens,
89
maxTokens: this.maxTokens
90
};
91
}
92
}
93
94
class TestLoggedRequestInfo implements ILoggedRequestInfo {
95
public readonly kind = LoggedInfoKind.Request;
96
97
constructor(
98
public readonly id: string,
99
public readonly entry: LoggedRequest,
100
public readonly token: CapturingToken | undefined
101
) { }
102
103
toJSON(): object {
104
const baseInfo = {
105
id: this.id,
106
kind: 'request',
107
type: this.entry.type,
108
name: this.entry.debugName
109
};
110
111
if (this.entry.type === LoggedRequestKind.MarkdownContentRequest) {
112
return {
113
...baseInfo,
114
startTime: new Date(this.entry.startTimeMs).toISOString(),
115
content: resolveMarkdownContent(this.entry)
116
};
117
}
118
119
// Handle ChatML request types (Success, Failure, Cancellation)
120
// These all have startTime/endTime as Date objects
121
if (this.entry.type === LoggedRequestKind.ChatMLSuccess ||
122
this.entry.type === LoggedRequestKind.ChatMLFailure ||
123
this.entry.type === LoggedRequestKind.ChatMLCancelation) {
124
125
const metadata = {
126
model: this.entry.chatParams?.model,
127
location: this.entry.chatParams?.location,
128
startTime: this.entry.startTime.toISOString(),
129
endTime: this.entry.endTime.toISOString(),
130
duration: this.entry.endTime.getTime() - this.entry.startTime.getTime(),
131
maxResponseTokens: this.entry.chatParams?.body?.max_tokens ?? this.entry.chatParams?.body?.max_output_tokens,
132
};
133
134
// Build response data matching the real LoggedRequestInfo.toJSON() format
135
let responseData;
136
let errorInfo;
137
138
if (this.entry.type === LoggedRequestKind.ChatMLSuccess) {
139
responseData = {
140
type: 'success',
141
message: this.entry.result.value
142
};
143
} else if (this.entry.type === LoggedRequestKind.ChatMLFailure) {
144
errorInfo = {
145
type: 'failure',
146
reason: this.entry.result.reason
147
};
148
} else if (this.entry.type === LoggedRequestKind.ChatMLCancelation) {
149
errorInfo = {
150
type: 'canceled'
151
};
152
}
153
154
const response = responseData || errorInfo ? {
155
...responseData,
156
...errorInfo
157
} : undefined;
158
159
return {
160
...baseInfo,
161
metadata,
162
response,
163
isConversationRequest: this.entry.isConversationRequest
164
};
165
}
166
167
// Fallback for any unknown types
168
return baseInfo;
169
}
170
}
171
172
class TestLoggedToolCall {
173
public readonly kind = LoggedInfoKind.ToolCall;
174
public readonly toolMetadata: unknown;
175
176
constructor(
177
public readonly id: string,
178
public readonly name: string,
179
public readonly args: unknown,
180
public readonly response: LanguageModelToolResult2,
181
public readonly token: CapturingToken | undefined,
182
public readonly time: number,
183
public readonly thinking?: ThinkingData,
184
) {
185
// Extract toolMetadata from response if it exists
186
this.toolMetadata = 'toolMetadata' in response ? (response as { toolMetadata?: unknown }).toolMetadata : undefined;
187
}
188
189
async toJSON(): Promise<object> {
190
return {
191
id: this.id,
192
kind: 'toolCall',
193
tool: this.name,
194
args: this.args,
195
time: new Date(this.time).toISOString(),
196
};
197
}
198
}
199
200