Path: blob/main/extensions/copilot/src/platform/requestLogger/common/requestLogger.ts
13401 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 { HTMLTracer, IChatEndpointInfo, Raw, RenderPromptResult } from '@vscode/prompt-tsx';7import type { Event } from 'vscode';8import { ChatFetchError, ChatFetchResponseType, ChatLocation, ChatResponses, FetchSuccess } from '../../../platform/chat/common/commonTypes';9import { IResponseDelta, OptionalChatRequestParams } from '../../../platform/networking/common/fetch';10import { IChatEndpoint, IEndpointBody } from '../../../platform/networking/common/networking';11import { createServiceIdentifier } from '../../../util/common/services';12import { ThemeIcon } from '../../../util/vs/base/common/themables';13import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';14import type { LanguageModelToolResult2 } from '../../../vscodeTypes';15import type { IModelAPIResponse } from '../../endpoint/common/endpointProvider';16import { APIUsage } from '../../networking/common/openai';17import { ThinkingData } from '../../thinking/common/thinking';18import { CapturingToken } from '../common/capturingToken';1920export type UriData = { kind: 'request'; id: string } | { kind: 'latest' };2122export class ChatRequestScheme {23public static readonly chatRequestScheme = 'ccreq';2425public static buildUri(data: UriData, format: 'markdown' | 'json' | 'rawrequest' = 'markdown'): string {26let extension: string;27if (format === 'markdown') {28extension = 'copilotmd';29} else if (format === 'json') {30extension = 'json';31} else { // rawrequest32extension = 'request.json';33}34if (data.kind === 'latest') {35return `${ChatRequestScheme.chatRequestScheme}:latest.${extension}`;36} else {37return `${ChatRequestScheme.chatRequestScheme}:${data.id}.${extension}`;38}39}4041public static parseUri(uri: string): { data: UriData; format: 'markdown' | 'json' | 'rawrequest' } | undefined {42// Check for latest markdown43if (uri === this.buildUri({ kind: 'latest' }, 'markdown')) {44return { data: { kind: 'latest' }, format: 'markdown' };45}46// Check for latest JSON47if (uri === this.buildUri({ kind: 'latest' }, 'json')) {48return { data: { kind: 'latest' }, format: 'json' };49}50// Check for latest rawrequest51if (uri === this.buildUri({ kind: 'latest' }, 'rawrequest')) {52return { data: { kind: 'latest' }, format: 'rawrequest' };53}5455// Check for specific request markdown56const mdMatch = uri.match(/ccreq:([^\s]+)\.copilotmd/);57if (mdMatch) {58return { data: { kind: 'request', id: mdMatch[1] }, format: 'markdown' };59}6061// specific raw body json62const bodyJsonMatch = uri.match(/ccreq:([^\s]+)\.request\.json/);63if (bodyJsonMatch) {64return { data: { kind: 'request', id: bodyJsonMatch[1] }, format: 'rawrequest' };65}6667// Check for specific request JSON68const jsonMatch = uri.match(/ccreq:([^\s]+)\.json/);69if (jsonMatch) {70return { data: { kind: 'request', id: jsonMatch[1] }, format: 'json' };71}7273return undefined;74}7576public static findAllUris(text: string): { uri: string; range: OffsetRange }[] {77const linkRE = /(ccreq:[^\s]+\.(copilotmd|json|request\.json))/g;78return [...text.matchAll(linkRE)].map(79(m) => {80const identifier = m[1];81return {82uri: identifier,83range: new OffsetRange(m.index!, m.index! + identifier.length)84};85}86);87}88}8990export const enum LoggedInfoKind {91Element,92Request,93ToolCall,94}9596export interface ILoggedElementInfo {97kind: LoggedInfoKind.Element;98id: string;99name: string;100tokens: number;101maxTokens: number;102trace: HTMLTracer;103token: CapturingToken | undefined;104toJSON(): object;105}106107export interface ILoggedRequestInfo {108kind: LoggedInfoKind.Request;109id: string;110entry: LoggedRequest;111token: CapturingToken | undefined;112toJSON(): object;113}114115export interface ILoggedToolCall {116kind: LoggedInfoKind.ToolCall;117id: string;118name: string;119args: unknown;120response: LanguageModelToolResult2;121token: CapturingToken | undefined;122time: number;123thinking?: ThinkingData;124toolMetadata?: unknown;125toJSON(): Promise<object>;126}127128export interface ILoggedPendingRequest {129messages: Raw.ChatMessage[];130ourRequestId: string;131model: string;132location: ChatLocation;133intent?: string;134postOptions?: OptionalChatRequestParams;135body?: IEndpointBody;136ignoreStatefulMarker?: boolean;137isConversationRequest?: boolean;138/** Custom metadata to be displayed in the log document */139customMetadata?: Record<string, string | number | boolean | undefined>;140}141142export type LoggedInfo = ILoggedElementInfo | ILoggedRequestInfo | ILoggedToolCall;143144export const IRequestLogger = createServiceIdentifier<IRequestLogger>('IRequestLogger');145export interface IRequestLogger {146147readonly _serviceBrand: undefined;148149promptRendererTracing: boolean;150151captureInvocation<T>(request: CapturingToken, fn: () => Promise<T>): Promise<T>;152153logToolCall(id: string, name: string, args: unknown, response: LanguageModelToolResult2, thinking?: ThinkingData): void;154155logModelListCall(requestId: string, requestMetadata: RequestMetadata, models: IModelAPIResponse[]): void;156157logContentExclusionRules(repos: string[], rules: { patterns: string[]; ifAnyMatch: string[]; ifNoneMatch: string[] }[], durationMs: number): void;158159logChatRequest(debugName: string, chatEndpoint: IChatEndpointLogInfo, chatParams: ILoggedPendingRequest): PendingLoggedChatRequest;160161addPromptTrace(elementName: string, endpoint: IChatEndpointInfo, result: RenderPromptResult, trace: HTMLTracer): void;162addEntry(entry: LoggedRequest): void;163164onDidChangeRequests: Event<void>;165getRequests(): LoggedInfo[];166getRequestById(id: string): LoggedInfo | undefined;167168enableWorkspaceEditTracing(): void;169disableWorkspaceEditTracing(): void;170}171172export const enum LoggedRequestKind {173ChatMLSuccess = 'ChatMLSuccess',174ChatMLFailure = 'ChatMLFailure',175ChatMLCancelation = 'ChatMLCancelation',176MarkdownContentRequest = 'MarkdownContentRequest',177}178179export type IChatEndpointLogInfo = Partial<Pick<IChatEndpoint, 'model' | 'modelMaxPromptTokens' | 'urlOrRequestMetadata'>>;180181export interface ILoggedChatMLRequest {182debugName: string;183chatEndpoint: IChatEndpointLogInfo;184chatParams: ILoggedPendingRequest;185startTime: Date;186endTime: Date;187isConversationRequest?: boolean;188/** Custom metadata to be displayed in the log document */189customMetadata?: Record<string, string | number | boolean | undefined>;190}191192export interface ILoggedChatMLSuccessRequest extends ILoggedChatMLRequest {193type: LoggedRequestKind.ChatMLSuccess;194timeToFirstToken: number | undefined;195usage: APIUsage | undefined;196result: FetchSuccess<string[]>;197deltas?: IResponseDelta[];198}199200export interface ILoggedChatMLFailureRequest extends ILoggedChatMLRequest {201type: LoggedRequestKind.ChatMLFailure;202timeToFirstToken: number | undefined;203result: ChatFetchError;204}205206export interface ILoggedChatMLCancelationRequest extends ILoggedChatMLRequest {207type: LoggedRequestKind.ChatMLCancelation;208}209210export interface IMarkdownContentRequest {211type: LoggedRequestKind.MarkdownContentRequest;212startTimeMs: number;213icon: ThemeIcon | undefined | (() => ThemeIcon | undefined);214debugName: string;215markdownContent: string | (() => string);216isConversationRequest?: boolean;217/**218* When set, the log tree and virtual document will refresh when this event fires.219* Used for "live" entries that update over time (e.g. in-progress NES requests).220*/221onDidChange?: Event<void>;222/**223* When set, determines whether this entry should be visible in the log tree.224* Used for live entries that may become hidden (e.g. skipped/cancelled NES requests).225*/226isVisible?: () => boolean;227}228229export function resolveMarkdownContent(entry: IMarkdownContentRequest): string {230return typeof entry.markdownContent === 'function' ? entry.markdownContent() : entry.markdownContent;231}232233export function resolveMarkdownIcon(entry: IMarkdownContentRequest): ThemeIcon | undefined {234return typeof entry.icon === 'function' ? entry.icon() : entry.icon;235}236237export type LoggedRequest = (238ILoggedChatMLSuccessRequest239| ILoggedChatMLFailureRequest240| ILoggedChatMLCancelationRequest241| IMarkdownContentRequest242);243244class AbstractPendingLoggedRequest {245protected _time: Date;246protected _timeToFirstToken: number | undefined = undefined;247248constructor(249protected _logbook: IRequestLogger,250protected _debugName: string,251protected _chatEndpoint: IChatEndpointLogInfo,252protected _chatParams: ILoggedPendingRequest253) {254this._time = new Date();255}256257markTimeToFirstToken(timeToFirstToken: number): void {258this._timeToFirstToken = timeToFirstToken;259}260261resolveWithCancelation() {262this._logbook.addEntry({263type: LoggedRequestKind.ChatMLCancelation,264debugName: this._debugName,265chatEndpoint: this._chatEndpoint,266chatParams: this._chatParams,267startTime: this._time,268endTime: new Date(),269isConversationRequest: this._chatParams.isConversationRequest,270customMetadata: this._chatParams.customMetadata271});272}273}274275export class PendingLoggedChatRequest extends AbstractPendingLoggedRequest {276constructor(277logbook: IRequestLogger,278debugName: string,279chatEndpoint: IChatEndpoint,280chatParams: ILoggedPendingRequest281) {282super(logbook, debugName, chatEndpoint, chatParams);283}284285resolve(result: ChatResponses, deltas?: IResponseDelta[]): void {286if (result.type === ChatFetchResponseType.Success) {287this._logbook.addEntry({288type: LoggedRequestKind.ChatMLSuccess,289debugName: this._debugName,290usage: result.usage,291chatEndpoint: this._chatEndpoint,292chatParams: this._chatParams,293startTime: this._time,294endTime: new Date(),295timeToFirstToken: this._timeToFirstToken,296isConversationRequest: this._chatParams.isConversationRequest,297customMetadata: this._chatParams.customMetadata,298result,299deltas300});301} else {302this._logbook.addEntry({303type: result.type === ChatFetchResponseType.Canceled ? LoggedRequestKind.ChatMLCancelation : LoggedRequestKind.ChatMLFailure,304debugName: this._debugName,305chatEndpoint: this._chatEndpoint,306chatParams: this._chatParams,307startTime: this._time,308endTime: new Date(),309timeToFirstToken: this._timeToFirstToken,310isConversationRequest: this._chatParams.isConversationRequest,311customMetadata: this._chatParams.customMetadata,312result,313});314}315}316}317318319