Path: blob/main/extensions/copilot/src/extension/prompt/vscode-node/requestLoggerImpl.ts
13399 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 { RequestMetadata, RequestType } from '@vscode/copilot-api';6import { HTMLTracer, IChatEndpointInfo, RenderPromptResult } from '@vscode/prompt-tsx';7import { CancellationToken, DocumentLink, DocumentLinkProvider, ExtendedLanguageModelToolResult, LanguageModelDataPart, LanguageModelPromptTsxPart, LanguageModelTextPart, LanguageModelToolResult2, languages, Range, TextDocument, Uri, workspace } from 'vscode';8import { IChatDebugFileLoggerService } from '../../../platform/chat/common/chatDebugFileLoggerService';9import { ChatFetchResponseType } from '../../../platform/chat/common/commonTypes';10import { ConfigKey, IConfigurationService, XTabProviderId } from '../../../platform/configuration/common/configurationService';11import { IModelAPIResponse } from '../../../platform/endpoint/common/endpointProvider';12import { getAllStatefulMarkersAndIndicies } from '../../../platform/endpoint/common/statefulMarkerContainer';13import { ILogService } from '../../../platform/log/common/logService';14import { messageToMarkdown } from '../../../platform/log/common/messageStringify';15import { ContextManagementResponse } from '../../../platform/networking/common/anthropic';16import { IResponseDelta, isOpenAiFunctionTool } from '../../../platform/networking/common/fetch';17import { IEndpointBody } from '../../../platform/networking/common/networking';18import { CapturingToken } from '../../../platform/requestLogger/common/capturingToken';19import { ChatRequestScheme, ILoggedElementInfo, ILoggedRequestInfo, ILoggedToolCall, LoggedInfo, LoggedInfoKind, LoggedRequest, LoggedRequestKind, resolveMarkdownContent } from '../../../platform/requestLogger/common/requestLogger';20import { AbstractRequestLogger } from '../../../platform/requestLogger/node/requestLogger';21import { ThinkingData } from '../../../platform/thinking/common/thinking';22import { createFencedCodeBlock } from '../../../util/common/markdown';23import { assertNever } from '../../../util/vs/base/common/assert';24import { Codicon } from '../../../util/vs/base/common/codicons';25import { Emitter } from '../../../util/vs/base/common/event';26import { Iterable } from '../../../util/vs/base/common/iterator';27import { IDisposable } from '../../../util/vs/base/common/lifecycle';28import { generateUuid } from '../../../util/vs/base/common/uuid';29import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';30import { renderDataPartToString, renderToolResultToStringNoBudget } from './requestLoggerToolResult';31import { WorkspaceEditRecorder } from './workspaceEditRecorder';3233// Utility function to process deltas into a message string34function processDeltasToMessage(deltas: IResponseDelta[]): string {35return deltas.map((d, i) => {36let text: string = '';37if (d.text) {38text += d.text;39}4041// Can include other parts as needed42if (d.copilotToolCalls) {43if (i > 0) {44text += '\n';45}4647text += d.copilotToolCalls.map(c => {48let argsStr = c.arguments;49try {50const parsedArgs = JSON.parse(c.arguments);51argsStr = JSON.stringify(parsedArgs, undefined, 2)52.replace(/(?<!\\)\\n/g, '\n')53.replace(/(?<!\\)\\t/g, '\t');54} catch (e) { }55return `๐ ๏ธ ${c.name} (${c.id}) ${argsStr}`;56}).join('\n');57}5859// Handle context management60if (d.contextManagement) {61if (i > 0 || text.length > 0) {62text += '\n';63}6465const totalClearedTokens = (d.contextManagement as ContextManagementResponse)?.applied_edits?.reduce(66(sum: number, edit) => sum + (edit.cleared_input_tokens || 0),67068) || 0;69const totalClearedToolUses = (d.contextManagement as ContextManagementResponse)?.applied_edits?.reduce(70(sum: number, edit) => sum + (edit.cleared_tool_uses || 0),71072) || 0;73const totalClearedThinkingTurns = (d.contextManagement as ContextManagementResponse)?.applied_edits?.reduce(74(sum: number, edit) => sum + (edit.cleared_thinking_turns || 0),75076) || 0;7778const details: string[] = [];79if (totalClearedTokens > 0) {80details.push(`${totalClearedTokens} tokens`);81}82if (totalClearedToolUses > 0) {83details.push(`${totalClearedToolUses} tool uses`);84}85if (totalClearedThinkingTurns > 0) {86details.push(`${totalClearedThinkingTurns} thinking turns`);87}8889if (details.length > 0) {90text += `๐งน Context cleared: ${details.join(', ')}`;91}92}9394return text;95}).join('');96}9798// Implementation classes with toJson methods99class LoggedElementInfo implements ILoggedElementInfo {100public readonly kind = LoggedInfoKind.Element;101102constructor(103public readonly id: string,104public readonly name: string,105public readonly tokens: number,106public readonly maxTokens: number,107public readonly trace: HTMLTracer,108public readonly token: CapturingToken | undefined109) { }110111toJSON(): object {112return {113id: this.id,114kind: 'element',115name: this.name,116tokens: this.tokens,117maxTokens: this.maxTokens118};119}120}121122class LoggedRequestInfo implements ILoggedRequestInfo {123public readonly kind = LoggedInfoKind.Request;124125constructor(126public readonly id: string,127public readonly entry: LoggedRequest,128public readonly token: CapturingToken | undefined129) { }130131toJSON(): object {132const baseInfo = {133id: this.id,134kind: 'request',135type: this.entry.type,136name: this.entry.debugName137};138139if (this.entry.type === LoggedRequestKind.MarkdownContentRequest) {140return {141...baseInfo,142startTime: new Date(this.entry.startTimeMs).toISOString(),143content: resolveMarkdownContent(this.entry)144};145}146147// Handle stateful marker like _renderRequestToMarkdown does148let lastResponseId: { marker: string; modelId: string } | undefined;149if (!this.entry.chatParams.ignoreStatefulMarker) {150const statefulMarker = Iterable.first(getAllStatefulMarkersAndIndicies(this.entry.chatParams.messages));151if (statefulMarker) {152lastResponseId = {153marker: statefulMarker.statefulMarker.marker,154modelId: statefulMarker.statefulMarker.modelId155};156}157}158159// Build response data based on entry type160let responseData;161let errorInfo;162163if (this.entry.type === LoggedRequestKind.ChatMLSuccess) {164responseData = {165type: 'success',166message: this.entry.result.value167};168} else if (this.entry.type === LoggedRequestKind.ChatMLFailure) {169if (this.entry.result.type === ChatFetchResponseType.Length) {170responseData = {171type: 'truncated',172message: this.entry.result.truncatedValue173};174} else {175errorInfo = {176type: 'failure',177reason: this.entry.result.reason178};179}180} else if (this.entry.type === LoggedRequestKind.ChatMLCancelation) {181errorInfo = {182type: 'canceled'183};184}185186const metadata = {187url: typeof this.entry.chatEndpoint.urlOrRequestMetadata === 'string' ?188this.entry.chatEndpoint.urlOrRequestMetadata : undefined,189requestType: typeof this.entry.chatEndpoint.urlOrRequestMetadata === 'object' ?190this.entry.chatEndpoint.urlOrRequestMetadata?.type : undefined,191model: this.entry.chatParams.model,192maxPromptTokens: this.entry.chatEndpoint.modelMaxPromptTokens,193maxResponseTokens: this.entry.chatParams.body?.max_tokens ?? this.entry.chatParams.body?.max_output_tokens ?? this.entry.chatParams.body?.max_completion_tokens,194location: this.entry.chatParams.location,195reasoning: this.entry.chatParams.body?.reasoning,196intent: this.entry.chatParams.intent,197startTime: this.entry.startTime?.toISOString(),198endTime: this.entry.endTime?.toISOString(),199duration: this.entry.endTime && this.entry.startTime ?200this.entry.endTime.getTime() - this.entry.startTime.getTime() : undefined,201ourRequestId: this.entry.chatParams.ourRequestId,202lastResponseId: lastResponseId,203requestId: this.entry.type === LoggedRequestKind.ChatMLSuccess || this.entry.type === LoggedRequestKind.ChatMLFailure ? this.entry.result.requestId : undefined,204serverRequestId: this.entry.type === LoggedRequestKind.ChatMLSuccess || this.entry.type === LoggedRequestKind.ChatMLFailure ? this.entry.result.serverRequestId : undefined,205timeToFirstToken: this.entry.type === LoggedRequestKind.ChatMLSuccess ? this.entry.timeToFirstToken : undefined,206usage: this.entry.type === LoggedRequestKind.ChatMLSuccess ? this.entry.usage : undefined,207tools: this.entry.chatParams.body?.tools,208};209210const requestMessages = {211messages: this.entry.chatParams.messages,212prediction: this.entry.chatParams.body?.prediction213};214215const response = responseData || errorInfo ? {216...responseData,217...errorInfo218} : undefined;219220return {221...baseInfo,222metadata: metadata,223requestMessages: requestMessages,224response: response225};226}227}228229class LoggedToolCall implements ILoggedToolCall {230public readonly kind = LoggedInfoKind.ToolCall;231232constructor(233public readonly id: string,234public readonly name: string,235public readonly args: unknown,236public readonly response: LanguageModelToolResult2,237public readonly token: any | undefined,238public readonly time: number,239public readonly thinking?: ThinkingData,240public readonly edits?: { path: string; edits: string }[],241public readonly toolMetadata?: unknown,242) { }243244async toJSON(): Promise<object> {245const responseData: string[] = [];246for (const content of this.response.content) {247if (content instanceof LanguageModelTextPart) {248responseData.push(content.value);249} else if (content instanceof LanguageModelDataPart) {250responseData.push(renderDataPartToString(content));251} else if (content instanceof LanguageModelPromptTsxPart) {252responseData.push(await renderToolResultToStringNoBudget(content));253}254}255256const thinking = this.thinking?.text ? {257id: this.thinking.id,258text: Array.isArray(this.thinking.text) ? this.thinking.text.join('\n') : this.thinking.text259} : undefined;260261return {262id: this.id,263kind: 'toolCall',264tool: this.name,265args: this.args,266time: new Date(this.time).toISOString(),267response: responseData,268thinking: thinking,269edits: this.edits ? this.edits.map(edit => ({ path: edit.path, edits: JSON.parse(edit.edits) })) : undefined,270toolMetadata: this.toolMetadata271};272}273}274275export class RequestLogger extends AbstractRequestLogger {276277private _didRegisterLinkProvider = false;278private readonly _entries: LoggedInfo[] = [];279private readonly _entryDisposables = new Map<string, IDisposable>();280private _workspaceEditRecorder: WorkspaceEditRecorder | undefined;281private readonly _onDidChangeDocument = this._register(new Emitter<Uri>());282283constructor(284@ILogService private readonly _logService: ILogService,285@IConfigurationService private readonly _configService: IConfigurationService,286@IInstantiationService private readonly _instantiationService: IInstantiationService,287@IChatDebugFileLoggerService private readonly _chatDebugFileLoggerService: IChatDebugFileLoggerService,288) {289super();290291292this._register(workspace.registerTextDocumentContentProvider(ChatRequestScheme.chatRequestScheme, {293onDidChange: this._onDidChangeDocument.event,294provideTextDocumentContent: (uri) => {295const parseResult = ChatRequestScheme.parseUri(uri.toString());296if (!parseResult) { return `Invalid URI: ${uri}`; }297298const { data: uriData, format } = parseResult;299const entry = uriData.kind === 'latest' ? this._entries.at(-1) : this._entries.find(e => e.id === uriData.id);300if (!entry) { return `Request not found`; }301302if (format === 'json') {303return this._renderToJson(entry);304} else if (format === 'rawrequest') {305return this._renderRawRequestToJson(entry);306} else {307// Existing markdown logic308switch (entry.kind) {309case LoggedInfoKind.Element:310return 'Not available';311case LoggedInfoKind.ToolCall:312return this._renderToolCallToMarkdown(entry);313case LoggedInfoKind.Request:314return this._renderRequestToMarkdown(entry.id, entry.entry);315default:316assertNever(entry);317}318}319}320}));321}322323public getRequests(): LoggedInfo[] {324return [...this._entries];325}326327public getRequestById(id: string): LoggedInfo | undefined {328return this._entries.find(e => e.id === id);329}330331private _onDidChangeRequests = this._register(new Emitter<void>());332public readonly onDidChangeRequests = this._onDidChangeRequests.event;333334public override logModelListCall(id: string, requestMetadata: RequestMetadata, models: IModelAPIResponse[]): void {335this._chatDebugFileLoggerService.setModelSnapshot(models);336this.addEntry({337type: LoggedRequestKind.MarkdownContentRequest,338debugName: 'modelList',339startTimeMs: Date.now(),340icon: Codicon.fileCode,341markdownContent: this._renderModelListToMarkdown(id, requestMetadata, models),342isConversationRequest: false343});344}345346public override logContentExclusionRules(repos: string[], rules: { patterns: string[]; ifAnyMatch: string[]; ifNoneMatch: string[] }[], durationMs: number): void {347this.addEntry({348type: LoggedRequestKind.MarkdownContentRequest,349debugName: 'contentExclusion',350startTimeMs: Date.now(),351icon: Codicon.shield,352markdownContent: this._renderContentExclusionToMarkdown(repos, rules, durationMs),353isConversationRequest: false354});355}356357public override logToolCall(id: string, name: string, args: unknown, response: LanguageModelToolResult2, thinking?: ThinkingData): void {358const edits = this._workspaceEditRecorder?.getEditsAndReset();359// Extract toolMetadata from response if it exists360const toolMetadata = 'toolMetadata' in response ? (response as ExtendedLanguageModelToolResult).toolMetadata : undefined;361this._addEntry(new LoggedToolCall(362id,363name,364args,365response,366this.currentRequest,367Date.now(),368thinking,369edits,370toolMetadata371));372}373374/** Start tracking edits made to the workspace for every tool call. */375public override enableWorkspaceEditTracing(): void {376if (!this._workspaceEditRecorder) {377this._workspaceEditRecorder = this._instantiationService.createInstance(WorkspaceEditRecorder);378}379}380381public override disableWorkspaceEditTracing(): void {382if (this._workspaceEditRecorder) {383this._workspaceEditRecorder.dispose();384this._workspaceEditRecorder = undefined;385}386}387388public override addPromptTrace(elementName: string, endpoint: IChatEndpointInfo, result: RenderPromptResult, trace: HTMLTracer): void {389const id = generateUuid().substring(0, 8);390this._addEntry(new LoggedElementInfo(id, elementName, result.tokenCount, endpoint.modelMaxPromptTokens, trace, this.currentRequest))391.catch(e => this._logService.error(e));392}393394public addEntry(entry: LoggedRequest): void {395const id = generateUuid().substring(0, 8);396if (!this._shouldLog(entry)) {397return;398}399this._addEntry(new LoggedRequestInfo(id, entry, this.currentRequest))400.then(ok => {401if (ok) {402this._ensureLinkProvider();403404// Subscribe to live entry changes for dynamic content/icon refresh405if (entry.type === LoggedRequestKind.MarkdownContentRequest && entry.onDidChange) {406let treeRefreshTimeout: ReturnType<typeof setTimeout> | undefined;407const subscription = entry.onDidChange(() => {408// Always update the virtual document immediately for streaming content409this._onDidChangeDocument.fire(Uri.parse(ChatRequestScheme.buildUri({ kind: 'request', id })));410411// Also refresh the "latest" document if this is the most recent entry412if (this._entries.at(-1)?.id === id) {413this._onDidChangeDocument.fire(Uri.parse(ChatRequestScheme.buildUri({ kind: 'latest' })));414}415416// Throttle tree refreshes to avoid frequent expensive updates on streaming changes417if (treeRefreshTimeout !== undefined) {418clearTimeout(treeRefreshTimeout);419}420treeRefreshTimeout = setTimeout(() => {421this._onDidChangeRequests.fire();422treeRefreshTimeout = undefined;423}, 200);424});425this._entryDisposables.set(id, subscription);426}427428let extraData: string;429if (entry.type === LoggedRequestKind.MarkdownContentRequest) {430extraData = 'markdown';431} else {432const status = entry.type === LoggedRequestKind.ChatMLCancelation ? 'cancelled' : entry.result.type;433let modelInfo = entry.chatEndpoint.model;434435// Add resolved model if it differs from requested model436if (entry.type === LoggedRequestKind.ChatMLSuccess &&437entry.result.resolvedModel &&438entry.result.resolvedModel !== entry.chatEndpoint.model) {439modelInfo += ` -> ${entry.result.resolvedModel}`;440}441442const duration = `${entry.endTime.getTime() - entry.startTime.getTime()}ms`;443extraData = `${status} | ${modelInfo} | ${duration} | [${entry.debugName}]`;444}445446this._logService.info(`${ChatRequestScheme.buildUri({ kind: 'request', id: id })} | ${extraData}`);447}448})449.catch(e => this._logService.error(e));450}451452private _shouldLog(entry: LoggedRequest) {453// don't log cancelled requests by XTabProviderId (because it triggers and cancels lots of requests)454if (entry.debugName === XTabProviderId &&455!this._configService.getConfig(ConfigKey.TeamInternal.InlineEditsLogCancelledRequests) &&456entry.type === LoggedRequestKind.ChatMLCancelation457) {458return false;459}460461return true;462}463464private _isFirst = true;465466private async _addEntry(entry: LoggedInfo): Promise<boolean> {467if (this._isFirst) {468this._isFirst = false;469this._logService.info(`Latest entry: ${ChatRequestScheme.buildUri({ kind: 'latest' })}`);470}471472473this._entries.push(entry);474const maxEntries = this._configService.getConfig(ConfigKey.Advanced.RequestLoggerMaxEntries);475if (this._entries.length > maxEntries) {476const evicted = this._entries.shift();477if (evicted) {478this._entryDisposables.get(evicted.id)?.dispose();479this._entryDisposables.delete(evicted.id);480}481}482this._onDidChangeRequests.fire();483this._onDidChangeDocument.fire(Uri.parse(ChatRequestScheme.buildUri({ kind: 'latest' })));484return true;485}486487private _ensureLinkProvider(): void {488if (this._didRegisterLinkProvider) {489return;490}491this._didRegisterLinkProvider = true;492493const docLinkProvider = new (class implements DocumentLinkProvider {494provideDocumentLinks(495td: TextDocument,496ct: CancellationToken497): DocumentLink[] {498return ChatRequestScheme.findAllUris(td.getText()).map(u => new DocumentLink(499new Range(td.positionAt(u.range.start), td.positionAt(u.range.endExclusive)),500Uri.parse(u.uri)501));502}503})();504505this._register(languages.registerDocumentLinkProvider(506{ scheme: 'output' },507docLinkProvider508));509}510511private _renderMarkdownStyles(): string {512return `513<style>514[id^="system"], [id^="user"], [id^="assistant"] {515margin: 4px 0 4px 0;516}517518.markdown-body > pre {519padding: 4px 16px;520}521</style>522`;523}524525private async _renderToJson(entry: LoggedInfo) {526try {527const jsonObject = await entry.toJSON();528return JSON.stringify(jsonObject, null, 2);529} catch (error) {530return JSON.stringify({531id: entry.id,532kind: 'error',533error: error?.toString() || 'Unknown error',534timestamp: new Date().toISOString()535}, null, 2);536}537}538539private async _renderToolCallToMarkdown(entry: ILoggedToolCall) {540const result: string[] = [];541result.push(`# Tool Call - ${entry.id}`);542result.push(``);543544result.push(`## Request`);545result.push(`~~~`);546547let args: string;548if (typeof entry.args === 'string') {549try {550args = JSON.stringify(JSON.parse(entry.args), undefined, 2)551.replace(/\\n/g, '\n')552.replace(/(?!=\\)\\t/g, '\t');553} catch {554args = entry.args;555}556} else {557args = JSON.stringify(entry.args, undefined, 2);558}559560result.push(`id : ${entry.id}`);561result.push(`tool : ${entry.name}`);562result.push(`args : ${args}`);563result.push(`~~~`);564565result.push(`## Response`);566567for (const content of entry.response.content) {568result.push(`~~~`);569if (content instanceof LanguageModelTextPart) {570result.push(content.value);571} else if (content instanceof LanguageModelDataPart) {572result.push(renderDataPartToString(content));573} else if (content instanceof LanguageModelPromptTsxPart) {574result.push(await renderToolResultToStringNoBudget(content));575}576result.push(`~~~`);577}578579if (entry.thinking?.text) {580result.push(`## Thinking`);581if (entry.thinking.id) {582result.push(`thinkingId: ${entry.thinking.id}`);583}584result.push(`~~~`);585result.push(Array.isArray(entry.thinking.text) ? entry.thinking.text.join('\n') : entry.thinking.text);586result.push(`~~~`);587}588589return result.join('\n');590}591592private _renderRequestToMarkdown(id: string, entry: LoggedRequest): string {593if (entry.type === LoggedRequestKind.MarkdownContentRequest) {594return resolveMarkdownContent(entry);595}596597const result: string[] = [];598result.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.`);599result.push(`# ${entry.debugName} - ${id}`);600result.push(``);601602// Just some other options to track603// TODO Probably we should just extract every item on the body and format it as below, instead of doing this one-by-one604const otherOptions: Record<string, string | number | boolean> = {};605for (const opt of ['temperature', 'stream', 'store'] satisfies (keyof IEndpointBody)[]) {606if (entry.chatParams.body?.[opt] !== undefined) {607otherOptions[opt] = entry.chatParams.body[opt];608}609}610611const durationMs = entry.endTime.getTime() - entry.startTime.getTime();612const tocItems: string[] = [];613tocItems.push(`- [Request Messages](#request-messages)`);614tocItems.push(` - [System](#system)`);615tocItems.push(` - [User](#user)`);616if (!!entry.chatParams.body?.prediction) {617tocItems.push(`- [Prediction](#prediction)`);618}619tocItems.push(`- [Response](#response)`);620621if (tocItems.length) {622for (const item of tocItems) {623result.push(item);624}625result.push(``);626}627628result.push(`## Metadata`);629result.push(`<pre><code>`);630631if (typeof entry.chatEndpoint.urlOrRequestMetadata === 'string') {632result.push(`url : ${entry.chatEndpoint.urlOrRequestMetadata}`);633} else if (entry.chatEndpoint.urlOrRequestMetadata) {634result.push(`requestType : ${entry.chatEndpoint.urlOrRequestMetadata?.type}`);635}636result.push(`model : ${entry.chatParams.model}`);637result.push(`maxPromptTokens : ${entry.chatEndpoint.modelMaxPromptTokens}`);638result.push(`maxResponseTokens: ${entry.chatParams.body?.max_tokens ?? entry.chatParams.body?.max_output_tokens ?? entry.chatParams.body?.max_completion_tokens}`);639result.push(`location : ${entry.chatParams.location}`);640result.push(`otherOptions : ${JSON.stringify(otherOptions)}`);641if (entry.chatParams.body?.reasoning) {642result.push(`reasoning : ${JSON.stringify(entry.chatParams.body.reasoning)}`);643}644result.push(`intent : ${entry.chatParams.intent}`);645result.push(`startTime : ${entry.startTime.toJSON()}`);646result.push(`endTime : ${entry.endTime.toJSON()}`);647result.push(`duration : ${durationMs}ms`);648result.push(`ourRequestId : ${entry.chatParams.ourRequestId}`);649650const ignoreStatefulMarker = entry.chatParams.ignoreStatefulMarker;651if (!ignoreStatefulMarker) {652const statefulMarker = Iterable.first(getAllStatefulMarkersAndIndicies(entry.chatParams.messages));653if (statefulMarker) {654result.push(`lastResponseId : ${statefulMarker.statefulMarker.marker} using ${statefulMarker.statefulMarker.modelId}`);655}656}657658if (entry.type === LoggedRequestKind.ChatMLSuccess) {659result.push(`requestId : ${entry.result.requestId}`);660result.push(`serverRequestId : ${entry.result.serverRequestId}`);661result.push(`timeToFirstToken : ${entry.timeToFirstToken}ms`);662result.push(`resolved model : ${entry.result.resolvedModel}`);663result.push(`usage : ${JSON.stringify(entry.usage)}`);664} else if (entry.type === LoggedRequestKind.ChatMLFailure) {665result.push(`requestId : ${entry.result.requestId}`);666result.push(`serverRequestId : ${entry.result.serverRequestId}`);667}668if (entry.chatParams.body?.tools) {669const toolNames = entry.chatParams.body.tools.map(t => {670if (isOpenAiFunctionTool(t)) {671return t.function.name;672}673if ('name' in t) {674return t.name;675}676return t.type;677});678const numToolsString = `(${toolNames.length})`;679result.push(680`<details>`,681`<summary>tools ${numToolsString}${' '.repeat(9 - numToolsString.length)}: ${toolNames.join(', ')}</summary>${JSON.stringify(entry.chatParams.body.tools, undefined, 4)}`,682`</details>`683);684}685if (entry.customMetadata) {686for (const [key, value] of Object.entries(entry.customMetadata)) {687if (value !== undefined) {688const paddedKey = key.padEnd(16);689result.push(`${paddedKey} : ${value}`);690}691}692}693result.push(`</code></pre>`);694695result.push(`## Request Messages`);696for (const message of entry.chatParams.messages) {697result.push(messageToMarkdown(message, ignoreStatefulMarker));698}699if (typeof entry.chatParams.body?.prediction?.content === 'string') {700result.push(`## Prediction`);701result.push(createFencedCodeBlock('markdown', entry.chatParams.body.prediction.content, false));702}703result.push(``);704705if (entry.type === LoggedRequestKind.ChatMLSuccess) {706result.push(``);707result.push(`## Response`);708if (entry.deltas?.length) {709result.push(this._renderDeltasToMarkdown('assistant', entry.deltas));710} else {711const messages = entry.result.value;712let message: string = '';713if (Array.isArray(messages)) {714if (messages.length === 1) {715message = messages[0];716} else {717message = `${messages.map(v => `<<${v}>>`).join(', ')}`;718}719}720result.push(this._renderStringMessageToMarkdown('assistant', message));721}722} else if (entry.type === LoggedRequestKind.ChatMLFailure) {723result.push(``);724result.push(`<a id="response"></a>`);725if (entry.result.type === ChatFetchResponseType.Length) {726result.push(`## Response (truncated)`);727result.push(this._renderStringMessageToMarkdown('assistant', entry.result.truncatedValue));728} else {729result.push(`## FAILED: ${entry.result.reason}`);730}731} else if (entry.type === LoggedRequestKind.ChatMLCancelation) {732result.push(``);733result.push(`<a id="response"></a>`);734result.push(`## CANCELED`);735}736737result.push(this._renderMarkdownStyles());738739return result.join('\n');740}741742private _renderStringMessageToMarkdown(role: string, message: string): string {743const capitalizedRole = role.charAt(0).toUpperCase() + role.slice(1);744return `### ${capitalizedRole}\n${createFencedCodeBlock('markdown', message)}\n`;745}746747private _renderDeltasToMarkdown(role: string, deltas: IResponseDelta[]): string {748const capitalizedRole = role.charAt(0).toUpperCase() + role.slice(1);749const message = processDeltasToMessage(deltas);750return `### ${capitalizedRole}\n~~~md\n${message}\n~~~\n`;751}752753private _renderModelListToMarkdown(requestId: string, requestMetadata: RequestMetadata, models: IModelAPIResponse[]): string {754const result: string[] = [];755result.push(`# Model List Request`);756result.push(``);757758result.push(`## Metadata`);759result.push(`~~~`);760result.push(`requestId : ${requestId}`);761result.push(`requestType : ${requestMetadata?.type || 'unknown'}`);762result.push(`isModelLab : ${(requestMetadata as { type: string; isModelLab?: boolean }) ? 'yes' : 'no'}`);763if (requestMetadata.type === RequestType.ListModel) {764result.push(`requestedModel : ${(requestMetadata as { type: string; modelId: string })?.modelId || 'unknown'}`);765}766result.push(`modelsCount : ${models.length}`);767result.push(`~~~`);768769if (models.length > 0) {770result.push(`## Available Models (Raw API Response)`);771result.push(``);772result.push(`\`\`\`json`);773result.push(JSON.stringify(models, null, 2));774result.push(`\`\`\``);775result.push(``);776777// Keep a brief summary for quick reference778result.push(`## Summary`);779result.push(`~~~`);780result.push(`Total models : ${models.length}`);781result.push(`Chat models : ${models.filter(m => m.capabilities.type === 'chat').length}`);782result.push(`Completion models: ${models.filter(m => m.capabilities.type === 'completion').length}`);783result.push(`Premium models : ${models.filter(m => m.billing?.is_premium).length}`);784result.push(`Preview models : ${models.filter(m => m.preview).length}`);785result.push(`Default chat : ${models.find(m => m.is_chat_default)?.id || 'none'}`);786result.push(`Fallback chat : ${models.find(m => m.is_chat_fallback)?.id || 'none'}`);787result.push(`~~~`);788}789790result.push(this._renderMarkdownStyles());791792return result.join('\n');793}794795private _renderContentExclusionToMarkdown(repos: string[], rules: { patterns: string[]; ifAnyMatch: string[]; ifNoneMatch: string[] }[], durationMs: number): string {796const result: string[] = [];797result.push(`# Content Exclusion Rules`);798result.push(``);799800const totals = rules.reduce((sum, r) => {801sum.patterns += r.patterns.length;802sum.ifAnyMatch += r.ifAnyMatch.length;803sum.ifNoneMatch += r.ifNoneMatch.length;804return sum;805}, { patterns: 0, ifAnyMatch: 0, ifNoneMatch: 0 });806807result.push(`## Metadata`);808result.push(`~~~`);809result.push(`fetchTime : ${durationMs}ms`);810result.push(`repoCount : ${repos.length}`);811result.push(`totalGlobRules : ${totals.patterns}`);812result.push(`totalIfAnyMatch : ${totals.ifAnyMatch}`);813result.push(`totalIfNoneMatch : ${totals.ifNoneMatch}`);814result.push(`~~~`);815816for (let i = 0; i < repos.length; i++) {817const repo = repos[i];818const repoRules = rules[i];819result.push(``);820result.push(`## ${repo || '(non-git files)'}`);821822if (repoRules.patterns.length === 0 && repoRules.ifAnyMatch.length === 0 && repoRules.ifNoneMatch.length === 0) {823result.push(`_No rules_`);824continue;825}826827if (repoRules.patterns.length > 0) {828result.push(`### Glob Patterns (${repoRules.patterns.length})`);829result.push(`~~~`);830for (const pattern of repoRules.patterns) {831result.push(pattern);832}833result.push(`~~~`);834}835836if (repoRules.ifAnyMatch.length > 0) {837result.push(`### ifAnyMatch Regex (${repoRules.ifAnyMatch.length})`);838result.push(`~~~`);839for (const pattern of repoRules.ifAnyMatch) {840result.push(pattern);841}842result.push(`~~~`);843}844845if (repoRules.ifNoneMatch.length > 0) {846result.push(`### ifNoneMatch Regex (${repoRules.ifNoneMatch.length})`);847result.push(`~~~`);848for (const pattern of repoRules.ifNoneMatch) {849result.push(pattern);850}851result.push(`~~~`);852}853}854855result.push(this._renderMarkdownStyles());856857return result.join('\n');858}859860private _renderRawRequestToJson(entry: LoggedInfo): string {861if (entry.kind !== LoggedInfoKind.Request) {862return 'Not available';863}864865const req = entry.entry;866if (req.type === LoggedRequestKind.MarkdownContentRequest || !req.chatParams.body) {867return 'Not available';868}869870try {871return JSON.stringify(req.chatParams.body, null, 2);872} catch (e) {873return `Failed to render body: ${e}`;874}875}876}877878879