Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/requestLogger/common/requestLogger.ts
13401 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 { HTMLTracer, IChatEndpointInfo, Raw, RenderPromptResult } from '@vscode/prompt-tsx';
8
import type { Event } from 'vscode';
9
import { ChatFetchError, ChatFetchResponseType, ChatLocation, ChatResponses, FetchSuccess } from '../../../platform/chat/common/commonTypes';
10
import { IResponseDelta, OptionalChatRequestParams } from '../../../platform/networking/common/fetch';
11
import { IChatEndpoint, IEndpointBody } from '../../../platform/networking/common/networking';
12
import { createServiceIdentifier } from '../../../util/common/services';
13
import { ThemeIcon } from '../../../util/vs/base/common/themables';
14
import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';
15
import type { LanguageModelToolResult2 } from '../../../vscodeTypes';
16
import type { IModelAPIResponse } from '../../endpoint/common/endpointProvider';
17
import { APIUsage } from '../../networking/common/openai';
18
import { ThinkingData } from '../../thinking/common/thinking';
19
import { CapturingToken } from '../common/capturingToken';
20
21
export type UriData = { kind: 'request'; id: string } | { kind: 'latest' };
22
23
export class ChatRequestScheme {
24
public static readonly chatRequestScheme = 'ccreq';
25
26
public static buildUri(data: UriData, format: 'markdown' | 'json' | 'rawrequest' = 'markdown'): string {
27
let extension: string;
28
if (format === 'markdown') {
29
extension = 'copilotmd';
30
} else if (format === 'json') {
31
extension = 'json';
32
} else { // rawrequest
33
extension = 'request.json';
34
}
35
if (data.kind === 'latest') {
36
return `${ChatRequestScheme.chatRequestScheme}:latest.${extension}`;
37
} else {
38
return `${ChatRequestScheme.chatRequestScheme}:${data.id}.${extension}`;
39
}
40
}
41
42
public static parseUri(uri: string): { data: UriData; format: 'markdown' | 'json' | 'rawrequest' } | undefined {
43
// Check for latest markdown
44
if (uri === this.buildUri({ kind: 'latest' }, 'markdown')) {
45
return { data: { kind: 'latest' }, format: 'markdown' };
46
}
47
// Check for latest JSON
48
if (uri === this.buildUri({ kind: 'latest' }, 'json')) {
49
return { data: { kind: 'latest' }, format: 'json' };
50
}
51
// Check for latest rawrequest
52
if (uri === this.buildUri({ kind: 'latest' }, 'rawrequest')) {
53
return { data: { kind: 'latest' }, format: 'rawrequest' };
54
}
55
56
// Check for specific request markdown
57
const mdMatch = uri.match(/ccreq:([^\s]+)\.copilotmd/);
58
if (mdMatch) {
59
return { data: { kind: 'request', id: mdMatch[1] }, format: 'markdown' };
60
}
61
62
// specific raw body json
63
const bodyJsonMatch = uri.match(/ccreq:([^\s]+)\.request\.json/);
64
if (bodyJsonMatch) {
65
return { data: { kind: 'request', id: bodyJsonMatch[1] }, format: 'rawrequest' };
66
}
67
68
// Check for specific request JSON
69
const jsonMatch = uri.match(/ccreq:([^\s]+)\.json/);
70
if (jsonMatch) {
71
return { data: { kind: 'request', id: jsonMatch[1] }, format: 'json' };
72
}
73
74
return undefined;
75
}
76
77
public static findAllUris(text: string): { uri: string; range: OffsetRange }[] {
78
const linkRE = /(ccreq:[^\s]+\.(copilotmd|json|request\.json))/g;
79
return [...text.matchAll(linkRE)].map(
80
(m) => {
81
const identifier = m[1];
82
return {
83
uri: identifier,
84
range: new OffsetRange(m.index!, m.index! + identifier.length)
85
};
86
}
87
);
88
}
89
}
90
91
export const enum LoggedInfoKind {
92
Element,
93
Request,
94
ToolCall,
95
}
96
97
export interface ILoggedElementInfo {
98
kind: LoggedInfoKind.Element;
99
id: string;
100
name: string;
101
tokens: number;
102
maxTokens: number;
103
trace: HTMLTracer;
104
token: CapturingToken | undefined;
105
toJSON(): object;
106
}
107
108
export interface ILoggedRequestInfo {
109
kind: LoggedInfoKind.Request;
110
id: string;
111
entry: LoggedRequest;
112
token: CapturingToken | undefined;
113
toJSON(): object;
114
}
115
116
export interface ILoggedToolCall {
117
kind: LoggedInfoKind.ToolCall;
118
id: string;
119
name: string;
120
args: unknown;
121
response: LanguageModelToolResult2;
122
token: CapturingToken | undefined;
123
time: number;
124
thinking?: ThinkingData;
125
toolMetadata?: unknown;
126
toJSON(): Promise<object>;
127
}
128
129
export interface ILoggedPendingRequest {
130
messages: Raw.ChatMessage[];
131
ourRequestId: string;
132
model: string;
133
location: ChatLocation;
134
intent?: string;
135
postOptions?: OptionalChatRequestParams;
136
body?: IEndpointBody;
137
ignoreStatefulMarker?: boolean;
138
isConversationRequest?: boolean;
139
/** Custom metadata to be displayed in the log document */
140
customMetadata?: Record<string, string | number | boolean | undefined>;
141
}
142
143
export type LoggedInfo = ILoggedElementInfo | ILoggedRequestInfo | ILoggedToolCall;
144
145
export const IRequestLogger = createServiceIdentifier<IRequestLogger>('IRequestLogger');
146
export interface IRequestLogger {
147
148
readonly _serviceBrand: undefined;
149
150
promptRendererTracing: boolean;
151
152
captureInvocation<T>(request: CapturingToken, fn: () => Promise<T>): Promise<T>;
153
154
logToolCall(id: string, name: string, args: unknown, response: LanguageModelToolResult2, thinking?: ThinkingData): void;
155
156
logModelListCall(requestId: string, requestMetadata: RequestMetadata, models: IModelAPIResponse[]): void;
157
158
logContentExclusionRules(repos: string[], rules: { patterns: string[]; ifAnyMatch: string[]; ifNoneMatch: string[] }[], durationMs: number): void;
159
160
logChatRequest(debugName: string, chatEndpoint: IChatEndpointLogInfo, chatParams: ILoggedPendingRequest): PendingLoggedChatRequest;
161
162
addPromptTrace(elementName: string, endpoint: IChatEndpointInfo, result: RenderPromptResult, trace: HTMLTracer): void;
163
addEntry(entry: LoggedRequest): void;
164
165
onDidChangeRequests: Event<void>;
166
getRequests(): LoggedInfo[];
167
getRequestById(id: string): LoggedInfo | undefined;
168
169
enableWorkspaceEditTracing(): void;
170
disableWorkspaceEditTracing(): void;
171
}
172
173
export const enum LoggedRequestKind {
174
ChatMLSuccess = 'ChatMLSuccess',
175
ChatMLFailure = 'ChatMLFailure',
176
ChatMLCancelation = 'ChatMLCancelation',
177
MarkdownContentRequest = 'MarkdownContentRequest',
178
}
179
180
export type IChatEndpointLogInfo = Partial<Pick<IChatEndpoint, 'model' | 'modelMaxPromptTokens' | 'urlOrRequestMetadata'>>;
181
182
export interface ILoggedChatMLRequest {
183
debugName: string;
184
chatEndpoint: IChatEndpointLogInfo;
185
chatParams: ILoggedPendingRequest;
186
startTime: Date;
187
endTime: Date;
188
isConversationRequest?: boolean;
189
/** Custom metadata to be displayed in the log document */
190
customMetadata?: Record<string, string | number | boolean | undefined>;
191
}
192
193
export interface ILoggedChatMLSuccessRequest extends ILoggedChatMLRequest {
194
type: LoggedRequestKind.ChatMLSuccess;
195
timeToFirstToken: number | undefined;
196
usage: APIUsage | undefined;
197
result: FetchSuccess<string[]>;
198
deltas?: IResponseDelta[];
199
}
200
201
export interface ILoggedChatMLFailureRequest extends ILoggedChatMLRequest {
202
type: LoggedRequestKind.ChatMLFailure;
203
timeToFirstToken: number | undefined;
204
result: ChatFetchError;
205
}
206
207
export interface ILoggedChatMLCancelationRequest extends ILoggedChatMLRequest {
208
type: LoggedRequestKind.ChatMLCancelation;
209
}
210
211
export interface IMarkdownContentRequest {
212
type: LoggedRequestKind.MarkdownContentRequest;
213
startTimeMs: number;
214
icon: ThemeIcon | undefined | (() => ThemeIcon | undefined);
215
debugName: string;
216
markdownContent: string | (() => string);
217
isConversationRequest?: boolean;
218
/**
219
* When set, the log tree and virtual document will refresh when this event fires.
220
* Used for "live" entries that update over time (e.g. in-progress NES requests).
221
*/
222
onDidChange?: Event<void>;
223
/**
224
* When set, determines whether this entry should be visible in the log tree.
225
* Used for live entries that may become hidden (e.g. skipped/cancelled NES requests).
226
*/
227
isVisible?: () => boolean;
228
}
229
230
export function resolveMarkdownContent(entry: IMarkdownContentRequest): string {
231
return typeof entry.markdownContent === 'function' ? entry.markdownContent() : entry.markdownContent;
232
}
233
234
export function resolveMarkdownIcon(entry: IMarkdownContentRequest): ThemeIcon | undefined {
235
return typeof entry.icon === 'function' ? entry.icon() : entry.icon;
236
}
237
238
export type LoggedRequest = (
239
ILoggedChatMLSuccessRequest
240
| ILoggedChatMLFailureRequest
241
| ILoggedChatMLCancelationRequest
242
| IMarkdownContentRequest
243
);
244
245
class AbstractPendingLoggedRequest {
246
protected _time: Date;
247
protected _timeToFirstToken: number | undefined = undefined;
248
249
constructor(
250
protected _logbook: IRequestLogger,
251
protected _debugName: string,
252
protected _chatEndpoint: IChatEndpointLogInfo,
253
protected _chatParams: ILoggedPendingRequest
254
) {
255
this._time = new Date();
256
}
257
258
markTimeToFirstToken(timeToFirstToken: number): void {
259
this._timeToFirstToken = timeToFirstToken;
260
}
261
262
resolveWithCancelation() {
263
this._logbook.addEntry({
264
type: LoggedRequestKind.ChatMLCancelation,
265
debugName: this._debugName,
266
chatEndpoint: this._chatEndpoint,
267
chatParams: this._chatParams,
268
startTime: this._time,
269
endTime: new Date(),
270
isConversationRequest: this._chatParams.isConversationRequest,
271
customMetadata: this._chatParams.customMetadata
272
});
273
}
274
}
275
276
export class PendingLoggedChatRequest extends AbstractPendingLoggedRequest {
277
constructor(
278
logbook: IRequestLogger,
279
debugName: string,
280
chatEndpoint: IChatEndpoint,
281
chatParams: ILoggedPendingRequest
282
) {
283
super(logbook, debugName, chatEndpoint, chatParams);
284
}
285
286
resolve(result: ChatResponses, deltas?: IResponseDelta[]): void {
287
if (result.type === ChatFetchResponseType.Success) {
288
this._logbook.addEntry({
289
type: LoggedRequestKind.ChatMLSuccess,
290
debugName: this._debugName,
291
usage: result.usage,
292
chatEndpoint: this._chatEndpoint,
293
chatParams: this._chatParams,
294
startTime: this._time,
295
endTime: new Date(),
296
timeToFirstToken: this._timeToFirstToken,
297
isConversationRequest: this._chatParams.isConversationRequest,
298
customMetadata: this._chatParams.customMetadata,
299
result,
300
deltas
301
});
302
} else {
303
this._logbook.addEntry({
304
type: result.type === ChatFetchResponseType.Canceled ? LoggedRequestKind.ChatMLCancelation : LoggedRequestKind.ChatMLFailure,
305
debugName: this._debugName,
306
chatEndpoint: this._chatEndpoint,
307
chatParams: this._chatParams,
308
startTime: this._time,
309
endTime: new Date(),
310
timeToFirstToken: this._timeToFirstToken,
311
isConversationRequest: this._chatParams.isConversationRequest,
312
customMetadata: this._chatParams.customMetadata,
313
result,
314
});
315
}
316
}
317
}
318
319