Path: blob/main/extensions/copilot/src/platform/requestLogger/test/node/testRequestLogger.ts
13405 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 type { RequestMetadata } from '@vscode/copilot-api';6import type { HTMLTracer, IChatEndpointInfo, RenderPromptResult } from '@vscode/prompt-tsx';7import type { LanguageModelToolResult2 } from 'vscode';8import { Emitter, Event } from '../../../../util/vs/base/common/event';9import { generateUuid } from '../../../../util/vs/base/common/uuid';10import { IModelAPIResponse } from '../../../endpoint/common/endpointProvider';11import { ThinkingData } from '../../../thinking/common/thinking';12import { CapturingToken } from '../../common/capturingToken';13import { ILoggedRequestInfo, LoggedInfo, LoggedInfoKind, LoggedRequest, LoggedRequestKind, resolveMarkdownContent } from '../../common/requestLogger';14import { AbstractRequestLogger } from '../../node/requestLogger';1516/**17* A test implementation of IRequestLogger that stores logged requests for verification in tests.18* Unlike NullRequestLogger, this actually stores entries so they can be validated.19*/20export class TestRequestLogger extends AbstractRequestLogger {21private readonly _entries: LoggedInfo[] = [];22private readonly _onDidChangeRequests = new Emitter<void>();23public readonly onDidChangeRequests: Event<void> = this._onDidChangeRequests.event;2425public override addPromptTrace(elementName: string, endpoint: IChatEndpointInfo, result: RenderPromptResult, trace: HTMLTracer): void {26const id = generateUuid().substring(0, 8);27this._entries.push(new TestLoggedElementInfo(id, elementName, result.tokenCount, endpoint.modelMaxPromptTokens, trace, this.currentRequest));28this._onDidChangeRequests.fire();29}3031public addEntry(entry: LoggedRequest): void {32const id = generateUuid().substring(0, 8);33this._entries.push(new TestLoggedRequestInfo(id, entry, this.currentRequest));34this._onDidChangeRequests.fire();35}3637public override getRequests(): LoggedInfo[] {38return [...this._entries];39}4041public override getRequestById(id: string): LoggedInfo | undefined {42return this._entries.find(e => e.id === id);43}4445public override logModelListCall(id: string, requestMetadata: RequestMetadata, models: IModelAPIResponse[]): void {46this.addEntry({47type: LoggedRequestKind.MarkdownContentRequest,48debugName: 'modelList',49startTimeMs: Date.now(),50icon: undefined,51markdownContent: `Model list call: ${models.length} models`,52isConversationRequest: false53});54}5556public override logToolCall(id: string, name: string, args: unknown, response: LanguageModelToolResult2, thinking?: ThinkingData): void {57this._entries.push(new TestLoggedToolCall(id, name, args, response, this.currentRequest, Date.now(), thinking));58this._onDidChangeRequests.fire();59}6061/**62* Clear all logged entries (useful between tests).63*/64public clear(): void {65this._entries.length = 0;66this._onDidChangeRequests.fire();67}68}6970class TestLoggedElementInfo {71public readonly kind = LoggedInfoKind.Element;7273constructor(74public readonly id: string,75public readonly name: string,76public readonly tokens: number,77public readonly maxTokens: number,78public readonly trace: HTMLTracer,79public readonly token: CapturingToken | undefined80) { }8182toJSON(): object {83return {84id: this.id,85kind: 'element',86name: this.name,87tokens: this.tokens,88maxTokens: this.maxTokens89};90}91}9293class TestLoggedRequestInfo implements ILoggedRequestInfo {94public readonly kind = LoggedInfoKind.Request;9596constructor(97public readonly id: string,98public readonly entry: LoggedRequest,99public readonly token: CapturingToken | undefined100) { }101102toJSON(): object {103const baseInfo = {104id: this.id,105kind: 'request',106type: this.entry.type,107name: this.entry.debugName108};109110if (this.entry.type === LoggedRequestKind.MarkdownContentRequest) {111return {112...baseInfo,113startTime: new Date(this.entry.startTimeMs).toISOString(),114content: resolveMarkdownContent(this.entry)115};116}117118// Handle ChatML request types (Success, Failure, Cancellation)119// These all have startTime/endTime as Date objects120if (this.entry.type === LoggedRequestKind.ChatMLSuccess ||121this.entry.type === LoggedRequestKind.ChatMLFailure ||122this.entry.type === LoggedRequestKind.ChatMLCancelation) {123124const metadata = {125model: this.entry.chatParams?.model,126location: this.entry.chatParams?.location,127startTime: this.entry.startTime.toISOString(),128endTime: this.entry.endTime.toISOString(),129duration: this.entry.endTime.getTime() - this.entry.startTime.getTime(),130maxResponseTokens: this.entry.chatParams?.body?.max_tokens ?? this.entry.chatParams?.body?.max_output_tokens,131};132133// Build response data matching the real LoggedRequestInfo.toJSON() format134let responseData;135let errorInfo;136137if (this.entry.type === LoggedRequestKind.ChatMLSuccess) {138responseData = {139type: 'success',140message: this.entry.result.value141};142} else if (this.entry.type === LoggedRequestKind.ChatMLFailure) {143errorInfo = {144type: 'failure',145reason: this.entry.result.reason146};147} else if (this.entry.type === LoggedRequestKind.ChatMLCancelation) {148errorInfo = {149type: 'canceled'150};151}152153const response = responseData || errorInfo ? {154...responseData,155...errorInfo156} : undefined;157158return {159...baseInfo,160metadata,161response,162isConversationRequest: this.entry.isConversationRequest163};164}165166// Fallback for any unknown types167return baseInfo;168}169}170171class TestLoggedToolCall {172public readonly kind = LoggedInfoKind.ToolCall;173public readonly toolMetadata: unknown;174175constructor(176public readonly id: string,177public readonly name: string,178public readonly args: unknown,179public readonly response: LanguageModelToolResult2,180public readonly token: CapturingToken | undefined,181public readonly time: number,182public readonly thinking?: ThinkingData,183) {184// Extract toolMetadata from response if it exists185this.toolMetadata = 'toolMetadata' in response ? (response as { toolMetadata?: unknown }).toolMetadata : undefined;186}187188async toJSON(): Promise<object> {189return {190id: this.id,191kind: 'toolCall',192tool: this.name,193args: this.args,194time: new Date(this.time).toISOString(),195};196}197}198199200