Path: blob/main/extensions/copilot/src/platform/chat/common/chatMLFetcher.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 { CancellationToken } from 'vscode';6import { createServiceIdentifier } from '../../../util/common/services';7import { AsyncIterableObject, AsyncIterableSource } from '../../../util/vs/base/common/async';8import { Event } from '../../../util/vs/base/common/event';9import { FinishedCallback, IResponseDelta, OptionalChatRequestParams } from '../../networking/common/fetch';10import { IChatEndpoint, IMakeChatRequestOptions } from '../../networking/common/networking';11import { ChatResponse, ChatResponses } from './commonTypes';1213export interface Source {14readonly extensionId?: string;15}1617export interface IResponsePart {18readonly delta: IResponseDelta;19}2021export interface IFetchMLOptions extends IMakeChatRequestOptions {22endpoint: IChatEndpoint;23requestOptions: OptionalChatRequestParams;24}252627export const IChatMLFetcher = createServiceIdentifier<IChatMLFetcher>('IChatMLFetcher');2829export interface IChatMLFetcher {3031readonly _serviceBrand: undefined;3233readonly onDidMakeChatMLRequest: Event<{ readonly model: string; readonly source?: Source; readonly tokenCount?: number }>;3435fetchOne(options: IFetchMLOptions, token: CancellationToken): Promise<ChatResponse>;3637/**38* Note: the returned array of strings may be less than `n` (e.g., in case there were errors during streaming)39*/40fetchMany(options: IFetchMLOptions, token: CancellationToken): Promise<ChatResponses>;41}4243interface IResponsePartWithText extends IResponsePart {44readonly text: string;45}4647export class FetchStreamSource {4849private _stream = new AsyncIterableSource<IResponsePart>();50private _paused?: (IResponsePartWithText | undefined)[];5152// This means that we will only show one instance of each annotation type, but the IDs are not correct and there is no other way53private _seenAnnotationTypes = new Set<string>();5455public get stream(): AsyncIterableObject<IResponsePart> {56return this._stream.asyncIterable;57}5859constructor() { }6061pause() {62this._paused ??= [];63}6465unpause() {66const toEmit = this._paused;67if (!toEmit) {68return;69}7071this._paused = undefined;72for (const part of toEmit) {73if (part) {74this.update(part.text, part.delta);75} else {76this.resolve();77}78}79}8081update(text: string, delta: IResponseDelta): void {82if (this._paused) {83this._paused.push({ text, delta });84return;85}8687if (delta.codeVulnAnnotations) {88// We can only display vulnerabilities inside codeblocks, and it's ok to discard annotations that fell outside of them89const numTripleBackticks = text.match(/(^|\n)```/g)?.length ?? 0;90const insideCodeblock = numTripleBackticks % 2 === 1;91if (!insideCodeblock || text.match(/(^|\n)```\w*\s*$/)) { // Not inside a codeblock, or right on the start triple-backtick of a codeblock92delta.codeVulnAnnotations = undefined;93}94}9596if (delta.codeVulnAnnotations) {97delta.codeVulnAnnotations = delta.codeVulnAnnotations.filter(annotation => !this._seenAnnotationTypes.has(annotation.details.type));98delta.codeVulnAnnotations.forEach(annotation => this._seenAnnotationTypes.add(annotation.details.type));99}100this._stream.emitOne({ delta });101}102103resolve(): void {104if (this._paused) {105this._paused.push(undefined);106return;107}108109this._stream.resolve();110}111112reject(error: Error): void {113this._paused = undefined;114this._stream.reject(error);115}116}117118export class FetchStreamRecorder {119public readonly callback: FinishedCallback;120public readonly deltas: IResponseDelta[] = [];121122// TTFTe123private _firstTokenEmittedTime: number | undefined;124public get firstTokenEmittedTime(): number | undefined {125return this._firstTokenEmittedTime;126}127128constructor(129callback: FinishedCallback | undefined130) {131this.callback = async (text: string, index: number, delta: IResponseDelta): Promise<number | undefined> => {132if (this._firstTokenEmittedTime === undefined && (delta.text || delta.beginToolCalls || (typeof delta.thinking?.text === 'string' && delta.thinking?.text || delta.thinking?.text?.length) || delta.copilotToolCalls || delta.copilotToolCallStreamUpdates)) {133this._firstTokenEmittedTime = Date.now();134}135136const result = callback ? await callback(text, index, delta) : undefined;137this.deltas.push(delta);138return result;139};140}141}142143144