Path: blob/main/extensions/copilot/src/platform/networking/common/openai.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 { OpenAI, OutputMode, Raw, toMode } from '@vscode/prompt-tsx';6import { ChatCompletionContentPartImage } from '@vscode/prompt-tsx/dist/base/output/openaiTypes';7import { ChatCompletionContentPartKind } from '@vscode/prompt-tsx/dist/base/output/rawTypes';8import { rawPartAsThinkingData } from '../../endpoint/common/thinkingDataContainer';9import { TelemetryData } from '../../telemetry/common/telemetryData';10import { ThinkingData, ThinkingDataInMessage } from '../../thinking/common/thinking';11import { ICopilotReference, RequestId } from './fetch';1213/**14* How the logprobs field looks in the OpenAI API chunks.15*/16export interface APILogprobs {17text_offset: number[];18token_logprobs: number[];19top_logprobs?: { [key: string]: number }[];20tokens: string[];21}2223/**24* Usage statistics for the completion request.25*/26export interface APIUsage {27/**28* Number of tokens in the prompt.29*/30prompt_tokens: number;31/**32* Number of tokens in the generated completion.33*/34completion_tokens: number;35/**36* Total number of tokens used in the request (prompt + completion).37*/38total_tokens: number;39/**40* Breakdown of tokens used in the prompt.41*/42prompt_tokens_details?: {43cached_tokens: number;44cache_creation_input_tokens?: number;45};46/**47* Breakdown of tokens used in a completion.48*49* @remark it's an optional field because Copilot Proxy returns this information but not CAPI as of 18 Jun 202550*/51completion_tokens_details?: {52/**53* Tokens generated by the model for reasoning.54*/55reasoning_tokens: number;56/**57* When using Predicted Outputs, the number of tokens in the prediction that appeared in the completion.58*/59accepted_prediction_tokens: number;60/**61* When using Predicted Outputs, the number of tokens in the prediction that did not appear in the completion.62* However, like reasoning tokens, these tokens are still counted in the total completion tokens for purposes of billing,63* output, and context window limits.64*/65rejected_prediction_tokens: number;66};67}6869export function isApiUsage(obj: unknown): obj is APIUsage {70return typeof (obj as APIUsage).prompt_tokens === 'number' &&71typeof (obj as APIUsage).completion_tokens === 'number' &&72typeof (obj as APIUsage).total_tokens === 'number';73}747576export interface APIJsonData {77text: string;78/* Joining this together produces `text`, due to the way the proxy works. */79tokens: readonly string[];80/* These are only generated in certain situations. */81logprobs?: APILogprobs;82}8384export interface APIErrorResponse {85code: number;86message: string;87metadata?: Record<string, any>;88}8990export const openAIContextManagementCompactionType = 'compaction';9192export const modelsWithoutResponsesContextManagement = new Set(['gpt-5', 'gpt-5.1', 'gpt-5.2']);93949596export interface OpenAIContextManagement {97type: typeof openAIContextManagementCompactionType;98compact_threshold: number;99}100101102export interface OpenAIContextManagementResponse {103encrypted_content: string;104type: typeof openAIContextManagementCompactionType;105id: string;106}107108109export enum ChatRole {110System = 'system',111User = 'user',112Assistant = 'assistant',113Function = 'function',114Tool = 'tool'115}116117118export type CAPIChatMessage = OpenAI.ChatMessage & {119/**120* CAPI references used in this message.121*/122copilot_references?: ICopilotReference[];123/**124* CAPI confirmations used in this message.125*/126copilot_confirmations?: { state: string; confirmation: any }[];127128copilot_cache_control?: {129'type': 'ephemeral';130};131} & ThinkingDataInMessage;132133export function getCAPITextPart(content: string | OpenAI.ChatCompletionContentPart[] | OpenAI.ChatCompletionContentPart): string {134if (Array.isArray(content)) {135return content.map((part) => getCAPITextPart(part)).join('');136} else if (typeof content === 'string') {137return content;138} else if (typeof content === 'object' && 'text' in content) {139return content.text;140} else {141return '';142}143}144145export type RawMessageConversionCallback = (message: CAPIChatMessage, thinkingData?: ThinkingData) => void;146/**147* Converts a raw TSX chat message to CAPI's format.148*149* **Extra:** the raw message can have `copilot_references` and150* `copilot_confirmations` properties, which are copied to the CAPI message.151*/152export function rawMessageToCAPI(message: Raw.ChatMessage, callback?: RawMessageConversionCallback): CAPIChatMessage;153export function rawMessageToCAPI(message: Raw.ChatMessage[], callback?: RawMessageConversionCallback): CAPIChatMessage[];154export function rawMessageToCAPI(message: Raw.ChatMessage[] | Raw.ChatMessage, callback?: RawMessageConversionCallback): CAPIChatMessage | CAPIChatMessage[] {155if (Array.isArray(message)) {156return message.map(m => rawMessageToCAPI(m, callback));157}158159const out: CAPIChatMessage = toMode(OutputMode.OpenAI, message);160if ('copilot_references' in message) {161out.copilot_references = (message as any).copilot_references;162}163if ('copilot_confirmations' in message) {164out.copilot_confirmations = (message as any).copilot_confirmations;165}166if (typeof out.content === 'string') {167out.content = out.content.trimEnd();168} else {169for (let i = 0; i < out.content.length; i++) {170const part = out.content[i];171if (part.type === 'text') {172part.text = part.text.trimEnd();173} else if (part.type === 'image_url' && Array.isArray(message.content) && i < message.content.length) {174const rawPart = message.content[i] as Raw.ChatCompletionContentPart;175if (rawPart?.type === Raw.ChatCompletionContentPartKind.Image && rawPart.imageUrl?.mediaType) {176// CAPI expects `media_type` instead of `mediaType`. This is only used for CAPI and not OpenAI.177const { mediaType, ...rawImageUrl } = rawPart.imageUrl;178(part.image_url as ChatCompletionContentPartImage.ImageURL & { media_type: string }) = {179...rawImageUrl,180media_type: mediaType181};182}183}184}185}186187if (message.content.find(part => part.type === ChatCompletionContentPartKind.CacheBreakpoint)) {188out.copilot_cache_control = { type: 'ephemeral' };189}190191for (const content of message.content) {192if (content.type === Raw.ChatCompletionContentPartKind.Opaque) {193const data = rawPartAsThinkingData(content);194if (callback && data) {195callback(out, data);196}197}198}199200return out;201}202203export enum FinishedCompletionReason {204/**205* Reason generated by the server. See https://platform.openai.com/docs/guides/gpt/chat-completions-api206*/207Stop = 'stop',208/**209* Reason generated by the server. See https://platform.openai.com/docs/guides/gpt/chat-completions-api210*/211Length = 'length',212/**213* Reason generated by the server. See https://platform.openai.com/docs/guides/gpt/chat-completions-api214*/215FunctionCall = 'function_call',216/**217* Reason generated by the server. See https://platform.openai.com/docs/guides/gpt/chat-completions-api218*/219ToolCalls = 'tool_calls',220/**221* Reason generated by the server. See https://platform.openai.com/docs/guides/gpt/chat-completions-api222*/223ContentFilter = 'content_filter',224/**225* Reason generated by the server (CAPI). Happens when the stream cannot be completed and the server must terminate the response.226*/227ServerError = 'error',228/**229* Reason generated by the client when the finish callback asked for processing to stop.230*/231ClientTrimmed = 'client-trimmed',232/**233* Reason generated by the client when we never received a finish_reason for this particular completion (indicates a server-side bug)234*/235ClientIterationDone = 'Iteration Done',236/**237* Reason generated by the client when we never received a finish_reason for this particular completion (indicates a server-side bug)238*/239ClientDone = 'DONE',240}241242export interface IToolCall {243index: number;244id?: string;245function?: { name: string; arguments: string };246}247248/**249* Contains the possible reasons a response can be filtered250*/251export enum FilterReason {252/**253* Content deemed to be hateful254*/255Hate = 'hate',256/**257* Content deemed to cause self harm258*/259SelfHarm = 'self_harm',260/**261* Content deemed to be sexual in nature262*/263Sexual = 'sexual',264/**265* Content deemed to be violent in nature266*/267Violence = 'violence',268/**269* Content contains copyrighted material270*/271Copyright = 'snippy',272/**273* The prompt was filtered, the reason was not provided274*/275Prompt = 'prompt'276}277278export interface ChatCompletion {279message: Raw.ChatMessage;280choiceIndex: number;281requestId: RequestId;282tokens: readonly string[];283usage: APIUsage | undefined;284model: string;285blockFinished: boolean; // Whether the block completion was determined to be finished286finishReason: FinishedCompletionReason;287filterReason?: FilterReason; // optional filter reason if the completion was filtered288telemetryData: TelemetryData; // optional telemetry data providing background289error?: APIErrorResponse; // optional, error was encountered during the response290}291292export interface ChoiceLogProbs {293content: ChoiceLogProbsContent[];294}295296export interface TokenLogProb {297bytes: number[];298token: string;299logprob: number;300}301302export interface ChoiceLogProbsContent extends TokenLogProb {303top_logprobs: TokenLogProb[];304}305306307