Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/vscode-node/requestLoggerImpl.ts
13399 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 { RequestMetadata, RequestType } from '@vscode/copilot-api';
7
import { HTMLTracer, IChatEndpointInfo, RenderPromptResult } from '@vscode/prompt-tsx';
8
import { CancellationToken, DocumentLink, DocumentLinkProvider, ExtendedLanguageModelToolResult, LanguageModelDataPart, LanguageModelPromptTsxPart, LanguageModelTextPart, LanguageModelToolResult2, languages, Range, TextDocument, Uri, workspace } from 'vscode';
9
import { IChatDebugFileLoggerService } from '../../../platform/chat/common/chatDebugFileLoggerService';
10
import { ChatFetchResponseType } from '../../../platform/chat/common/commonTypes';
11
import { ConfigKey, IConfigurationService, XTabProviderId } from '../../../platform/configuration/common/configurationService';
12
import { IModelAPIResponse } from '../../../platform/endpoint/common/endpointProvider';
13
import { getAllStatefulMarkersAndIndicies } from '../../../platform/endpoint/common/statefulMarkerContainer';
14
import { ILogService } from '../../../platform/log/common/logService';
15
import { messageToMarkdown } from '../../../platform/log/common/messageStringify';
16
import { ContextManagementResponse } from '../../../platform/networking/common/anthropic';
17
import { IResponseDelta, isOpenAiFunctionTool } from '../../../platform/networking/common/fetch';
18
import { IEndpointBody } from '../../../platform/networking/common/networking';
19
import { CapturingToken } from '../../../platform/requestLogger/common/capturingToken';
20
import { ChatRequestScheme, ILoggedElementInfo, ILoggedRequestInfo, ILoggedToolCall, LoggedInfo, LoggedInfoKind, LoggedRequest, LoggedRequestKind, resolveMarkdownContent } from '../../../platform/requestLogger/common/requestLogger';
21
import { AbstractRequestLogger } from '../../../platform/requestLogger/node/requestLogger';
22
import { ThinkingData } from '../../../platform/thinking/common/thinking';
23
import { createFencedCodeBlock } from '../../../util/common/markdown';
24
import { assertNever } from '../../../util/vs/base/common/assert';
25
import { Codicon } from '../../../util/vs/base/common/codicons';
26
import { Emitter } from '../../../util/vs/base/common/event';
27
import { Iterable } from '../../../util/vs/base/common/iterator';
28
import { IDisposable } from '../../../util/vs/base/common/lifecycle';
29
import { generateUuid } from '../../../util/vs/base/common/uuid';
30
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
31
import { renderDataPartToString, renderToolResultToStringNoBudget } from './requestLoggerToolResult';
32
import { WorkspaceEditRecorder } from './workspaceEditRecorder';
33
34
// Utility function to process deltas into a message string
35
function processDeltasToMessage(deltas: IResponseDelta[]): string {
36
return deltas.map((d, i) => {
37
let text: string = '';
38
if (d.text) {
39
text += d.text;
40
}
41
42
// Can include other parts as needed
43
if (d.copilotToolCalls) {
44
if (i > 0) {
45
text += '\n';
46
}
47
48
text += d.copilotToolCalls.map(c => {
49
let argsStr = c.arguments;
50
try {
51
const parsedArgs = JSON.parse(c.arguments);
52
argsStr = JSON.stringify(parsedArgs, undefined, 2)
53
.replace(/(?<!\\)\\n/g, '\n')
54
.replace(/(?<!\\)\\t/g, '\t');
55
} catch (e) { }
56
return `๐Ÿ› ๏ธ ${c.name} (${c.id}) ${argsStr}`;
57
}).join('\n');
58
}
59
60
// Handle context management
61
if (d.contextManagement) {
62
if (i > 0 || text.length > 0) {
63
text += '\n';
64
}
65
66
const totalClearedTokens = (d.contextManagement as ContextManagementResponse)?.applied_edits?.reduce(
67
(sum: number, edit) => sum + (edit.cleared_input_tokens || 0),
68
0
69
) || 0;
70
const totalClearedToolUses = (d.contextManagement as ContextManagementResponse)?.applied_edits?.reduce(
71
(sum: number, edit) => sum + (edit.cleared_tool_uses || 0),
72
0
73
) || 0;
74
const totalClearedThinkingTurns = (d.contextManagement as ContextManagementResponse)?.applied_edits?.reduce(
75
(sum: number, edit) => sum + (edit.cleared_thinking_turns || 0),
76
0
77
) || 0;
78
79
const details: string[] = [];
80
if (totalClearedTokens > 0) {
81
details.push(`${totalClearedTokens} tokens`);
82
}
83
if (totalClearedToolUses > 0) {
84
details.push(`${totalClearedToolUses} tool uses`);
85
}
86
if (totalClearedThinkingTurns > 0) {
87
details.push(`${totalClearedThinkingTurns} thinking turns`);
88
}
89
90
if (details.length > 0) {
91
text += `๐Ÿงน Context cleared: ${details.join(', ')}`;
92
}
93
}
94
95
return text;
96
}).join('');
97
}
98
99
// Implementation classes with toJson methods
100
class LoggedElementInfo implements ILoggedElementInfo {
101
public readonly kind = LoggedInfoKind.Element;
102
103
constructor(
104
public readonly id: string,
105
public readonly name: string,
106
public readonly tokens: number,
107
public readonly maxTokens: number,
108
public readonly trace: HTMLTracer,
109
public readonly token: CapturingToken | undefined
110
) { }
111
112
toJSON(): object {
113
return {
114
id: this.id,
115
kind: 'element',
116
name: this.name,
117
tokens: this.tokens,
118
maxTokens: this.maxTokens
119
};
120
}
121
}
122
123
class LoggedRequestInfo implements ILoggedRequestInfo {
124
public readonly kind = LoggedInfoKind.Request;
125
126
constructor(
127
public readonly id: string,
128
public readonly entry: LoggedRequest,
129
public readonly token: CapturingToken | undefined
130
) { }
131
132
toJSON(): object {
133
const baseInfo = {
134
id: this.id,
135
kind: 'request',
136
type: this.entry.type,
137
name: this.entry.debugName
138
};
139
140
if (this.entry.type === LoggedRequestKind.MarkdownContentRequest) {
141
return {
142
...baseInfo,
143
startTime: new Date(this.entry.startTimeMs).toISOString(),
144
content: resolveMarkdownContent(this.entry)
145
};
146
}
147
148
// Handle stateful marker like _renderRequestToMarkdown does
149
let lastResponseId: { marker: string; modelId: string } | undefined;
150
if (!this.entry.chatParams.ignoreStatefulMarker) {
151
const statefulMarker = Iterable.first(getAllStatefulMarkersAndIndicies(this.entry.chatParams.messages));
152
if (statefulMarker) {
153
lastResponseId = {
154
marker: statefulMarker.statefulMarker.marker,
155
modelId: statefulMarker.statefulMarker.modelId
156
};
157
}
158
}
159
160
// Build response data based on entry type
161
let responseData;
162
let errorInfo;
163
164
if (this.entry.type === LoggedRequestKind.ChatMLSuccess) {
165
responseData = {
166
type: 'success',
167
message: this.entry.result.value
168
};
169
} else if (this.entry.type === LoggedRequestKind.ChatMLFailure) {
170
if (this.entry.result.type === ChatFetchResponseType.Length) {
171
responseData = {
172
type: 'truncated',
173
message: this.entry.result.truncatedValue
174
};
175
} else {
176
errorInfo = {
177
type: 'failure',
178
reason: this.entry.result.reason
179
};
180
}
181
} else if (this.entry.type === LoggedRequestKind.ChatMLCancelation) {
182
errorInfo = {
183
type: 'canceled'
184
};
185
}
186
187
const metadata = {
188
url: typeof this.entry.chatEndpoint.urlOrRequestMetadata === 'string' ?
189
this.entry.chatEndpoint.urlOrRequestMetadata : undefined,
190
requestType: typeof this.entry.chatEndpoint.urlOrRequestMetadata === 'object' ?
191
this.entry.chatEndpoint.urlOrRequestMetadata?.type : undefined,
192
model: this.entry.chatParams.model,
193
maxPromptTokens: this.entry.chatEndpoint.modelMaxPromptTokens,
194
maxResponseTokens: this.entry.chatParams.body?.max_tokens ?? this.entry.chatParams.body?.max_output_tokens ?? this.entry.chatParams.body?.max_completion_tokens,
195
location: this.entry.chatParams.location,
196
reasoning: this.entry.chatParams.body?.reasoning,
197
intent: this.entry.chatParams.intent,
198
startTime: this.entry.startTime?.toISOString(),
199
endTime: this.entry.endTime?.toISOString(),
200
duration: this.entry.endTime && this.entry.startTime ?
201
this.entry.endTime.getTime() - this.entry.startTime.getTime() : undefined,
202
ourRequestId: this.entry.chatParams.ourRequestId,
203
lastResponseId: lastResponseId,
204
requestId: this.entry.type === LoggedRequestKind.ChatMLSuccess || this.entry.type === LoggedRequestKind.ChatMLFailure ? this.entry.result.requestId : undefined,
205
serverRequestId: this.entry.type === LoggedRequestKind.ChatMLSuccess || this.entry.type === LoggedRequestKind.ChatMLFailure ? this.entry.result.serverRequestId : undefined,
206
timeToFirstToken: this.entry.type === LoggedRequestKind.ChatMLSuccess ? this.entry.timeToFirstToken : undefined,
207
usage: this.entry.type === LoggedRequestKind.ChatMLSuccess ? this.entry.usage : undefined,
208
tools: this.entry.chatParams.body?.tools,
209
};
210
211
const requestMessages = {
212
messages: this.entry.chatParams.messages,
213
prediction: this.entry.chatParams.body?.prediction
214
};
215
216
const response = responseData || errorInfo ? {
217
...responseData,
218
...errorInfo
219
} : undefined;
220
221
return {
222
...baseInfo,
223
metadata: metadata,
224
requestMessages: requestMessages,
225
response: response
226
};
227
}
228
}
229
230
class LoggedToolCall implements ILoggedToolCall {
231
public readonly kind = LoggedInfoKind.ToolCall;
232
233
constructor(
234
public readonly id: string,
235
public readonly name: string,
236
public readonly args: unknown,
237
public readonly response: LanguageModelToolResult2,
238
public readonly token: any | undefined,
239
public readonly time: number,
240
public readonly thinking?: ThinkingData,
241
public readonly edits?: { path: string; edits: string }[],
242
public readonly toolMetadata?: unknown,
243
) { }
244
245
async toJSON(): Promise<object> {
246
const responseData: string[] = [];
247
for (const content of this.response.content) {
248
if (content instanceof LanguageModelTextPart) {
249
responseData.push(content.value);
250
} else if (content instanceof LanguageModelDataPart) {
251
responseData.push(renderDataPartToString(content));
252
} else if (content instanceof LanguageModelPromptTsxPart) {
253
responseData.push(await renderToolResultToStringNoBudget(content));
254
}
255
}
256
257
const thinking = this.thinking?.text ? {
258
id: this.thinking.id,
259
text: Array.isArray(this.thinking.text) ? this.thinking.text.join('\n') : this.thinking.text
260
} : undefined;
261
262
return {
263
id: this.id,
264
kind: 'toolCall',
265
tool: this.name,
266
args: this.args,
267
time: new Date(this.time).toISOString(),
268
response: responseData,
269
thinking: thinking,
270
edits: this.edits ? this.edits.map(edit => ({ path: edit.path, edits: JSON.parse(edit.edits) })) : undefined,
271
toolMetadata: this.toolMetadata
272
};
273
}
274
}
275
276
export class RequestLogger extends AbstractRequestLogger {
277
278
private _didRegisterLinkProvider = false;
279
private readonly _entries: LoggedInfo[] = [];
280
private readonly _entryDisposables = new Map<string, IDisposable>();
281
private _workspaceEditRecorder: WorkspaceEditRecorder | undefined;
282
private readonly _onDidChangeDocument = this._register(new Emitter<Uri>());
283
284
constructor(
285
@ILogService private readonly _logService: ILogService,
286
@IConfigurationService private readonly _configService: IConfigurationService,
287
@IInstantiationService private readonly _instantiationService: IInstantiationService,
288
@IChatDebugFileLoggerService private readonly _chatDebugFileLoggerService: IChatDebugFileLoggerService,
289
) {
290
super();
291
292
293
this._register(workspace.registerTextDocumentContentProvider(ChatRequestScheme.chatRequestScheme, {
294
onDidChange: this._onDidChangeDocument.event,
295
provideTextDocumentContent: (uri) => {
296
const parseResult = ChatRequestScheme.parseUri(uri.toString());
297
if (!parseResult) { return `Invalid URI: ${uri}`; }
298
299
const { data: uriData, format } = parseResult;
300
const entry = uriData.kind === 'latest' ? this._entries.at(-1) : this._entries.find(e => e.id === uriData.id);
301
if (!entry) { return `Request not found`; }
302
303
if (format === 'json') {
304
return this._renderToJson(entry);
305
} else if (format === 'rawrequest') {
306
return this._renderRawRequestToJson(entry);
307
} else {
308
// Existing markdown logic
309
switch (entry.kind) {
310
case LoggedInfoKind.Element:
311
return 'Not available';
312
case LoggedInfoKind.ToolCall:
313
return this._renderToolCallToMarkdown(entry);
314
case LoggedInfoKind.Request:
315
return this._renderRequestToMarkdown(entry.id, entry.entry);
316
default:
317
assertNever(entry);
318
}
319
}
320
}
321
}));
322
}
323
324
public getRequests(): LoggedInfo[] {
325
return [...this._entries];
326
}
327
328
public getRequestById(id: string): LoggedInfo | undefined {
329
return this._entries.find(e => e.id === id);
330
}
331
332
private _onDidChangeRequests = this._register(new Emitter<void>());
333
public readonly onDidChangeRequests = this._onDidChangeRequests.event;
334
335
public override logModelListCall(id: string, requestMetadata: RequestMetadata, models: IModelAPIResponse[]): void {
336
this._chatDebugFileLoggerService.setModelSnapshot(models);
337
this.addEntry({
338
type: LoggedRequestKind.MarkdownContentRequest,
339
debugName: 'modelList',
340
startTimeMs: Date.now(),
341
icon: Codicon.fileCode,
342
markdownContent: this._renderModelListToMarkdown(id, requestMetadata, models),
343
isConversationRequest: false
344
});
345
}
346
347
public override logContentExclusionRules(repos: string[], rules: { patterns: string[]; ifAnyMatch: string[]; ifNoneMatch: string[] }[], durationMs: number): void {
348
this.addEntry({
349
type: LoggedRequestKind.MarkdownContentRequest,
350
debugName: 'contentExclusion',
351
startTimeMs: Date.now(),
352
icon: Codicon.shield,
353
markdownContent: this._renderContentExclusionToMarkdown(repos, rules, durationMs),
354
isConversationRequest: false
355
});
356
}
357
358
public override logToolCall(id: string, name: string, args: unknown, response: LanguageModelToolResult2, thinking?: ThinkingData): void {
359
const edits = this._workspaceEditRecorder?.getEditsAndReset();
360
// Extract toolMetadata from response if it exists
361
const toolMetadata = 'toolMetadata' in response ? (response as ExtendedLanguageModelToolResult).toolMetadata : undefined;
362
this._addEntry(new LoggedToolCall(
363
id,
364
name,
365
args,
366
response,
367
this.currentRequest,
368
Date.now(),
369
thinking,
370
edits,
371
toolMetadata
372
));
373
}
374
375
/** Start tracking edits made to the workspace for every tool call. */
376
public override enableWorkspaceEditTracing(): void {
377
if (!this._workspaceEditRecorder) {
378
this._workspaceEditRecorder = this._instantiationService.createInstance(WorkspaceEditRecorder);
379
}
380
}
381
382
public override disableWorkspaceEditTracing(): void {
383
if (this._workspaceEditRecorder) {
384
this._workspaceEditRecorder.dispose();
385
this._workspaceEditRecorder = undefined;
386
}
387
}
388
389
public override addPromptTrace(elementName: string, endpoint: IChatEndpointInfo, result: RenderPromptResult, trace: HTMLTracer): void {
390
const id = generateUuid().substring(0, 8);
391
this._addEntry(new LoggedElementInfo(id, elementName, result.tokenCount, endpoint.modelMaxPromptTokens, trace, this.currentRequest))
392
.catch(e => this._logService.error(e));
393
}
394
395
public addEntry(entry: LoggedRequest): void {
396
const id = generateUuid().substring(0, 8);
397
if (!this._shouldLog(entry)) {
398
return;
399
}
400
this._addEntry(new LoggedRequestInfo(id, entry, this.currentRequest))
401
.then(ok => {
402
if (ok) {
403
this._ensureLinkProvider();
404
405
// Subscribe to live entry changes for dynamic content/icon refresh
406
if (entry.type === LoggedRequestKind.MarkdownContentRequest && entry.onDidChange) {
407
let treeRefreshTimeout: ReturnType<typeof setTimeout> | undefined;
408
const subscription = entry.onDidChange(() => {
409
// Always update the virtual document immediately for streaming content
410
this._onDidChangeDocument.fire(Uri.parse(ChatRequestScheme.buildUri({ kind: 'request', id })));
411
412
// Also refresh the "latest" document if this is the most recent entry
413
if (this._entries.at(-1)?.id === id) {
414
this._onDidChangeDocument.fire(Uri.parse(ChatRequestScheme.buildUri({ kind: 'latest' })));
415
}
416
417
// Throttle tree refreshes to avoid frequent expensive updates on streaming changes
418
if (treeRefreshTimeout !== undefined) {
419
clearTimeout(treeRefreshTimeout);
420
}
421
treeRefreshTimeout = setTimeout(() => {
422
this._onDidChangeRequests.fire();
423
treeRefreshTimeout = undefined;
424
}, 200);
425
});
426
this._entryDisposables.set(id, subscription);
427
}
428
429
let extraData: string;
430
if (entry.type === LoggedRequestKind.MarkdownContentRequest) {
431
extraData = 'markdown';
432
} else {
433
const status = entry.type === LoggedRequestKind.ChatMLCancelation ? 'cancelled' : entry.result.type;
434
let modelInfo = entry.chatEndpoint.model;
435
436
// Add resolved model if it differs from requested model
437
if (entry.type === LoggedRequestKind.ChatMLSuccess &&
438
entry.result.resolvedModel &&
439
entry.result.resolvedModel !== entry.chatEndpoint.model) {
440
modelInfo += ` -> ${entry.result.resolvedModel}`;
441
}
442
443
const duration = `${entry.endTime.getTime() - entry.startTime.getTime()}ms`;
444
extraData = `${status} | ${modelInfo} | ${duration} | [${entry.debugName}]`;
445
}
446
447
this._logService.info(`${ChatRequestScheme.buildUri({ kind: 'request', id: id })} | ${extraData}`);
448
}
449
})
450
.catch(e => this._logService.error(e));
451
}
452
453
private _shouldLog(entry: LoggedRequest) {
454
// don't log cancelled requests by XTabProviderId (because it triggers and cancels lots of requests)
455
if (entry.debugName === XTabProviderId &&
456
!this._configService.getConfig(ConfigKey.TeamInternal.InlineEditsLogCancelledRequests) &&
457
entry.type === LoggedRequestKind.ChatMLCancelation
458
) {
459
return false;
460
}
461
462
return true;
463
}
464
465
private _isFirst = true;
466
467
private async _addEntry(entry: LoggedInfo): Promise<boolean> {
468
if (this._isFirst) {
469
this._isFirst = false;
470
this._logService.info(`Latest entry: ${ChatRequestScheme.buildUri({ kind: 'latest' })}`);
471
}
472
473
474
this._entries.push(entry);
475
const maxEntries = this._configService.getConfig(ConfigKey.Advanced.RequestLoggerMaxEntries);
476
if (this._entries.length > maxEntries) {
477
const evicted = this._entries.shift();
478
if (evicted) {
479
this._entryDisposables.get(evicted.id)?.dispose();
480
this._entryDisposables.delete(evicted.id);
481
}
482
}
483
this._onDidChangeRequests.fire();
484
this._onDidChangeDocument.fire(Uri.parse(ChatRequestScheme.buildUri({ kind: 'latest' })));
485
return true;
486
}
487
488
private _ensureLinkProvider(): void {
489
if (this._didRegisterLinkProvider) {
490
return;
491
}
492
this._didRegisterLinkProvider = true;
493
494
const docLinkProvider = new (class implements DocumentLinkProvider {
495
provideDocumentLinks(
496
td: TextDocument,
497
ct: CancellationToken
498
): DocumentLink[] {
499
return ChatRequestScheme.findAllUris(td.getText()).map(u => new DocumentLink(
500
new Range(td.positionAt(u.range.start), td.positionAt(u.range.endExclusive)),
501
Uri.parse(u.uri)
502
));
503
}
504
})();
505
506
this._register(languages.registerDocumentLinkProvider(
507
{ scheme: 'output' },
508
docLinkProvider
509
));
510
}
511
512
private _renderMarkdownStyles(): string {
513
return `
514
<style>
515
[id^="system"], [id^="user"], [id^="assistant"] {
516
margin: 4px 0 4px 0;
517
}
518
519
.markdown-body > pre {
520
padding: 4px 16px;
521
}
522
</style>
523
`;
524
}
525
526
private async _renderToJson(entry: LoggedInfo) {
527
try {
528
const jsonObject = await entry.toJSON();
529
return JSON.stringify(jsonObject, null, 2);
530
} catch (error) {
531
return JSON.stringify({
532
id: entry.id,
533
kind: 'error',
534
error: error?.toString() || 'Unknown error',
535
timestamp: new Date().toISOString()
536
}, null, 2);
537
}
538
}
539
540
private async _renderToolCallToMarkdown(entry: ILoggedToolCall) {
541
const result: string[] = [];
542
result.push(`# Tool Call - ${entry.id}`);
543
result.push(``);
544
545
result.push(`## Request`);
546
result.push(`~~~`);
547
548
let args: string;
549
if (typeof entry.args === 'string') {
550
try {
551
args = JSON.stringify(JSON.parse(entry.args), undefined, 2)
552
.replace(/\\n/g, '\n')
553
.replace(/(?!=\\)\\t/g, '\t');
554
} catch {
555
args = entry.args;
556
}
557
} else {
558
args = JSON.stringify(entry.args, undefined, 2);
559
}
560
561
result.push(`id : ${entry.id}`);
562
result.push(`tool : ${entry.name}`);
563
result.push(`args : ${args}`);
564
result.push(`~~~`);
565
566
result.push(`## Response`);
567
568
for (const content of entry.response.content) {
569
result.push(`~~~`);
570
if (content instanceof LanguageModelTextPart) {
571
result.push(content.value);
572
} else if (content instanceof LanguageModelDataPart) {
573
result.push(renderDataPartToString(content));
574
} else if (content instanceof LanguageModelPromptTsxPart) {
575
result.push(await renderToolResultToStringNoBudget(content));
576
}
577
result.push(`~~~`);
578
}
579
580
if (entry.thinking?.text) {
581
result.push(`## Thinking`);
582
if (entry.thinking.id) {
583
result.push(`thinkingId: ${entry.thinking.id}`);
584
}
585
result.push(`~~~`);
586
result.push(Array.isArray(entry.thinking.text) ? entry.thinking.text.join('\n') : entry.thinking.text);
587
result.push(`~~~`);
588
}
589
590
return result.join('\n');
591
}
592
593
private _renderRequestToMarkdown(id: string, entry: LoggedRequest): string {
594
if (entry.type === LoggedRequestKind.MarkdownContentRequest) {
595
return resolveMarkdownContent(entry);
596
}
597
598
const result: string[] = [];
599
result.push(`> ๐Ÿšจ Note: This log may contain personal information such as the contents of your files or terminal output. Please review the contents carefully before sharing.`);
600
result.push(`# ${entry.debugName} - ${id}`);
601
result.push(``);
602
603
// Just some other options to track
604
// TODO Probably we should just extract every item on the body and format it as below, instead of doing this one-by-one
605
const otherOptions: Record<string, string | number | boolean> = {};
606
for (const opt of ['temperature', 'stream', 'store'] satisfies (keyof IEndpointBody)[]) {
607
if (entry.chatParams.body?.[opt] !== undefined) {
608
otherOptions[opt] = entry.chatParams.body[opt];
609
}
610
}
611
612
const durationMs = entry.endTime.getTime() - entry.startTime.getTime();
613
const tocItems: string[] = [];
614
tocItems.push(`- [Request Messages](#request-messages)`);
615
tocItems.push(` - [System](#system)`);
616
tocItems.push(` - [User](#user)`);
617
if (!!entry.chatParams.body?.prediction) {
618
tocItems.push(`- [Prediction](#prediction)`);
619
}
620
tocItems.push(`- [Response](#response)`);
621
622
if (tocItems.length) {
623
for (const item of tocItems) {
624
result.push(item);
625
}
626
result.push(``);
627
}
628
629
result.push(`## Metadata`);
630
result.push(`<pre><code>`);
631
632
if (typeof entry.chatEndpoint.urlOrRequestMetadata === 'string') {
633
result.push(`url : ${entry.chatEndpoint.urlOrRequestMetadata}`);
634
} else if (entry.chatEndpoint.urlOrRequestMetadata) {
635
result.push(`requestType : ${entry.chatEndpoint.urlOrRequestMetadata?.type}`);
636
}
637
result.push(`model : ${entry.chatParams.model}`);
638
result.push(`maxPromptTokens : ${entry.chatEndpoint.modelMaxPromptTokens}`);
639
result.push(`maxResponseTokens: ${entry.chatParams.body?.max_tokens ?? entry.chatParams.body?.max_output_tokens ?? entry.chatParams.body?.max_completion_tokens}`);
640
result.push(`location : ${entry.chatParams.location}`);
641
result.push(`otherOptions : ${JSON.stringify(otherOptions)}`);
642
if (entry.chatParams.body?.reasoning) {
643
result.push(`reasoning : ${JSON.stringify(entry.chatParams.body.reasoning)}`);
644
}
645
result.push(`intent : ${entry.chatParams.intent}`);
646
result.push(`startTime : ${entry.startTime.toJSON()}`);
647
result.push(`endTime : ${entry.endTime.toJSON()}`);
648
result.push(`duration : ${durationMs}ms`);
649
result.push(`ourRequestId : ${entry.chatParams.ourRequestId}`);
650
651
const ignoreStatefulMarker = entry.chatParams.ignoreStatefulMarker;
652
if (!ignoreStatefulMarker) {
653
const statefulMarker = Iterable.first(getAllStatefulMarkersAndIndicies(entry.chatParams.messages));
654
if (statefulMarker) {
655
result.push(`lastResponseId : ${statefulMarker.statefulMarker.marker} using ${statefulMarker.statefulMarker.modelId}`);
656
}
657
}
658
659
if (entry.type === LoggedRequestKind.ChatMLSuccess) {
660
result.push(`requestId : ${entry.result.requestId}`);
661
result.push(`serverRequestId : ${entry.result.serverRequestId}`);
662
result.push(`timeToFirstToken : ${entry.timeToFirstToken}ms`);
663
result.push(`resolved model : ${entry.result.resolvedModel}`);
664
result.push(`usage : ${JSON.stringify(entry.usage)}`);
665
} else if (entry.type === LoggedRequestKind.ChatMLFailure) {
666
result.push(`requestId : ${entry.result.requestId}`);
667
result.push(`serverRequestId : ${entry.result.serverRequestId}`);
668
}
669
if (entry.chatParams.body?.tools) {
670
const toolNames = entry.chatParams.body.tools.map(t => {
671
if (isOpenAiFunctionTool(t)) {
672
return t.function.name;
673
}
674
if ('name' in t) {
675
return t.name;
676
}
677
return t.type;
678
});
679
const numToolsString = `(${toolNames.length})`;
680
result.push(
681
`<details>`,
682
`<summary>tools ${numToolsString}${' '.repeat(9 - numToolsString.length)}: ${toolNames.join(', ')}</summary>${JSON.stringify(entry.chatParams.body.tools, undefined, 4)}`,
683
`</details>`
684
);
685
}
686
if (entry.customMetadata) {
687
for (const [key, value] of Object.entries(entry.customMetadata)) {
688
if (value !== undefined) {
689
const paddedKey = key.padEnd(16);
690
result.push(`${paddedKey} : ${value}`);
691
}
692
}
693
}
694
result.push(`</code></pre>`);
695
696
result.push(`## Request Messages`);
697
for (const message of entry.chatParams.messages) {
698
result.push(messageToMarkdown(message, ignoreStatefulMarker));
699
}
700
if (typeof entry.chatParams.body?.prediction?.content === 'string') {
701
result.push(`## Prediction`);
702
result.push(createFencedCodeBlock('markdown', entry.chatParams.body.prediction.content, false));
703
}
704
result.push(``);
705
706
if (entry.type === LoggedRequestKind.ChatMLSuccess) {
707
result.push(``);
708
result.push(`## Response`);
709
if (entry.deltas?.length) {
710
result.push(this._renderDeltasToMarkdown('assistant', entry.deltas));
711
} else {
712
const messages = entry.result.value;
713
let message: string = '';
714
if (Array.isArray(messages)) {
715
if (messages.length === 1) {
716
message = messages[0];
717
} else {
718
message = `${messages.map(v => `<<${v}>>`).join(', ')}`;
719
}
720
}
721
result.push(this._renderStringMessageToMarkdown('assistant', message));
722
}
723
} else if (entry.type === LoggedRequestKind.ChatMLFailure) {
724
result.push(``);
725
result.push(`<a id="response"></a>`);
726
if (entry.result.type === ChatFetchResponseType.Length) {
727
result.push(`## Response (truncated)`);
728
result.push(this._renderStringMessageToMarkdown('assistant', entry.result.truncatedValue));
729
} else {
730
result.push(`## FAILED: ${entry.result.reason}`);
731
}
732
} else if (entry.type === LoggedRequestKind.ChatMLCancelation) {
733
result.push(``);
734
result.push(`<a id="response"></a>`);
735
result.push(`## CANCELED`);
736
}
737
738
result.push(this._renderMarkdownStyles());
739
740
return result.join('\n');
741
}
742
743
private _renderStringMessageToMarkdown(role: string, message: string): string {
744
const capitalizedRole = role.charAt(0).toUpperCase() + role.slice(1);
745
return `### ${capitalizedRole}\n${createFencedCodeBlock('markdown', message)}\n`;
746
}
747
748
private _renderDeltasToMarkdown(role: string, deltas: IResponseDelta[]): string {
749
const capitalizedRole = role.charAt(0).toUpperCase() + role.slice(1);
750
const message = processDeltasToMessage(deltas);
751
return `### ${capitalizedRole}\n~~~md\n${message}\n~~~\n`;
752
}
753
754
private _renderModelListToMarkdown(requestId: string, requestMetadata: RequestMetadata, models: IModelAPIResponse[]): string {
755
const result: string[] = [];
756
result.push(`# Model List Request`);
757
result.push(``);
758
759
result.push(`## Metadata`);
760
result.push(`~~~`);
761
result.push(`requestId : ${requestId}`);
762
result.push(`requestType : ${requestMetadata?.type || 'unknown'}`);
763
result.push(`isModelLab : ${(requestMetadata as { type: string; isModelLab?: boolean }) ? 'yes' : 'no'}`);
764
if (requestMetadata.type === RequestType.ListModel) {
765
result.push(`requestedModel : ${(requestMetadata as { type: string; modelId: string })?.modelId || 'unknown'}`);
766
}
767
result.push(`modelsCount : ${models.length}`);
768
result.push(`~~~`);
769
770
if (models.length > 0) {
771
result.push(`## Available Models (Raw API Response)`);
772
result.push(``);
773
result.push(`\`\`\`json`);
774
result.push(JSON.stringify(models, null, 2));
775
result.push(`\`\`\``);
776
result.push(``);
777
778
// Keep a brief summary for quick reference
779
result.push(`## Summary`);
780
result.push(`~~~`);
781
result.push(`Total models : ${models.length}`);
782
result.push(`Chat models : ${models.filter(m => m.capabilities.type === 'chat').length}`);
783
result.push(`Completion models: ${models.filter(m => m.capabilities.type === 'completion').length}`);
784
result.push(`Premium models : ${models.filter(m => m.billing?.is_premium).length}`);
785
result.push(`Preview models : ${models.filter(m => m.preview).length}`);
786
result.push(`Default chat : ${models.find(m => m.is_chat_default)?.id || 'none'}`);
787
result.push(`Fallback chat : ${models.find(m => m.is_chat_fallback)?.id || 'none'}`);
788
result.push(`~~~`);
789
}
790
791
result.push(this._renderMarkdownStyles());
792
793
return result.join('\n');
794
}
795
796
private _renderContentExclusionToMarkdown(repos: string[], rules: { patterns: string[]; ifAnyMatch: string[]; ifNoneMatch: string[] }[], durationMs: number): string {
797
const result: string[] = [];
798
result.push(`# Content Exclusion Rules`);
799
result.push(``);
800
801
const totals = rules.reduce((sum, r) => {
802
sum.patterns += r.patterns.length;
803
sum.ifAnyMatch += r.ifAnyMatch.length;
804
sum.ifNoneMatch += r.ifNoneMatch.length;
805
return sum;
806
}, { patterns: 0, ifAnyMatch: 0, ifNoneMatch: 0 });
807
808
result.push(`## Metadata`);
809
result.push(`~~~`);
810
result.push(`fetchTime : ${durationMs}ms`);
811
result.push(`repoCount : ${repos.length}`);
812
result.push(`totalGlobRules : ${totals.patterns}`);
813
result.push(`totalIfAnyMatch : ${totals.ifAnyMatch}`);
814
result.push(`totalIfNoneMatch : ${totals.ifNoneMatch}`);
815
result.push(`~~~`);
816
817
for (let i = 0; i < repos.length; i++) {
818
const repo = repos[i];
819
const repoRules = rules[i];
820
result.push(``);
821
result.push(`## ${repo || '(non-git files)'}`);
822
823
if (repoRules.patterns.length === 0 && repoRules.ifAnyMatch.length === 0 && repoRules.ifNoneMatch.length === 0) {
824
result.push(`_No rules_`);
825
continue;
826
}
827
828
if (repoRules.patterns.length > 0) {
829
result.push(`### Glob Patterns (${repoRules.patterns.length})`);
830
result.push(`~~~`);
831
for (const pattern of repoRules.patterns) {
832
result.push(pattern);
833
}
834
result.push(`~~~`);
835
}
836
837
if (repoRules.ifAnyMatch.length > 0) {
838
result.push(`### ifAnyMatch Regex (${repoRules.ifAnyMatch.length})`);
839
result.push(`~~~`);
840
for (const pattern of repoRules.ifAnyMatch) {
841
result.push(pattern);
842
}
843
result.push(`~~~`);
844
}
845
846
if (repoRules.ifNoneMatch.length > 0) {
847
result.push(`### ifNoneMatch Regex (${repoRules.ifNoneMatch.length})`);
848
result.push(`~~~`);
849
for (const pattern of repoRules.ifNoneMatch) {
850
result.push(pattern);
851
}
852
result.push(`~~~`);
853
}
854
}
855
856
result.push(this._renderMarkdownStyles());
857
858
return result.join('\n');
859
}
860
861
private _renderRawRequestToJson(entry: LoggedInfo): string {
862
if (entry.kind !== LoggedInfoKind.Request) {
863
return 'Not available';
864
}
865
866
const req = entry.entry;
867
if (req.type === LoggedRequestKind.MarkdownContentRequest || !req.chatParams.body) {
868
return 'Not available';
869
}
870
871
try {
872
return JSON.stringify(req.chatParams.body, null, 2);
873
} catch (e) {
874
return `Failed to render body: ${e}`;
875
}
876
}
877
}
878
879