diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 17a8622bc51..4070489c54e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1440,8 +1440,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'languageModels'); return extHostLanguageModels.onDidChangeProviders(listener, thisArgs, disposables); }, - sendChatRequest(languageModel: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: vscode.CancellationToken) { + sendChatRequest(languageModel: string, messages: (vscode.LanguageModelChatMessage | vscode.LanguageModelChatMessage2)[], options?: vscode.LanguageModelChatRequestOptions, token?: vscode.CancellationToken) { checkProposedApiEnabled(extension, 'languageModels'); + token ??= CancellationToken.None; + options ??= {}; return extHostLanguageModels.sendChatRequest(extension, languageModel, messages, options, token); }, computeTokenLength(languageModel: string, text: string | vscode.LanguageModelChatMessage, token?: vscode.CancellationToken) { @@ -1730,12 +1732,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, - LanguageModelChatSystemMessage: extHostTypes.LanguageModelChatSystemMessage, - LanguageModelChatUserMessage: extHostTypes.LanguageModelChatUserMessage, - LanguageModelChatAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage, - LanguageModelSystemMessage: extHostTypes.LanguageModelChatSystemMessage, // TODO@jrieken REMOVE - LanguageModelUserMessage: extHostTypes.LanguageModelChatUserMessage, // TODO@jrieken REMOVE - LanguageModelAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage, // TODO@jrieken REMOVE + LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, + LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage2, + LanguageModelChatSystemMessage: extHostTypes.LanguageModelChatSystemMessage,// TODO@jrieken REMOVE + LanguageModelChatUserMessage: extHostTypes.LanguageModelChatUserMessage,// TODO@jrieken REMOVE + LanguageModelChatAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage,// TODO@jrieken REMOVE LanguageModelError: extHostTypes.LanguageModelError, NewSymbolName: extHostTypes.NewSymbolName, NewSymbolNameTag: extHostTypes.NewSymbolNameTag, diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index d0202a84451..a7823af0e4c 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -7,7 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { LanguageModelError } from 'vs/workbench/api/common/extHostTypes'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import type * as vscode from 'vscode'; import { Progress } from 'vs/platform/progress/common/progress'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; @@ -22,6 +22,7 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ILogService } from 'vs/platform/log/common/log'; import { Iterable } from 'vs/base/common/iterator'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -185,7 +186,11 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part }); }); - return data.provider.provideLanguageModelResponse2(messages.map(typeConvert.LanguageModelMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); + if (data.provider.provideLanguageModelResponse) { + return data.provider.provideLanguageModelResponse(messages.map(typeConvert.LanguageModelChatMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); + } else { + return data.provider.provideLanguageModelResponse2(messages.map(typeConvert.LanguageModelMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); + } } @@ -259,25 +264,27 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - async sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { + async sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: (vscode.LanguageModelChatMessage | vscode.LanguageModelChatMessage2)[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { + + const internalMessages: IChatMessage[] = this._convertMessages(extension, messages); const from = extension.identifier; const metadata = await this._proxy.$prepareChatAccess(from, languageModelId, options.justification); if (!metadata || !this._allLanguageModelData.has(languageModelId)) { - throw LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); + throw extHostTypes.LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); } if (this._isUsingAuth(from, metadata)) { - const success = await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, options.justification, options.silent); + const success = await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, options.justification, false); if (!success || !this._modelAccessList.get(from)?.has(metadata.extension)) { - throw LanguageModelError.NoPermissions(`Language model '${languageModelId}' cannot be used by '${from.value}'.`); + throw extHostTypes.LanguageModelError.NoPermissions(`Language model '${languageModelId}' cannot be used by '${from.value}'.`); } } const requestId = (Math.random() * 1e6) | 0; - const requestPromise = this._proxy.$fetchResponse(from, languageModelId, requestId, messages.map(typeConvert.LanguageModelMessage.from), options.modelOptions ?? {}, token); + const requestPromise = this._proxy.$fetchResponse(from, languageModelId, requestId, internalMessages, options.modelOptions ?? {}, token); const barrier = new Barrier(); @@ -303,11 +310,11 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { await barrier.wait(); if (error) { - if (error.name === LanguageModelError.name) { + if (error.name === extHostTypes.LanguageModelError.name) { throw error; } - throw new LanguageModelError( + throw new extHostTypes.LanguageModelError( `Language model '${languageModelId}' errored, check cause for more details`, 'Unknown', error @@ -317,6 +324,24 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { return res.apiObject; } + private _convertMessages(extension: IExtensionDescription, messages: (vscode.LanguageModelChatMessage2 | vscode.LanguageModelChatMessage)[]) { + const internalMessages: IChatMessage[] = []; + for (const message of messages) { + if (message instanceof extHostTypes.LanguageModelChatMessage2) { + if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { + checkProposedApiEnabled(extension, 'languageModelSystem'); + } + internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); + } else { + if (message instanceof extHostTypes.LanguageModelChatSystemMessage) { + checkProposedApiEnabled(extension, 'languageModelSystem'); + } + internalMessages.push(typeConvert.LanguageModelMessage.from(message)); + } + } + return internalMessages; + } + async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise { const data = this._pendingRequest.get(requestId);//.report(chunk); if (data) { @@ -376,7 +401,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { const data = this._allLanguageModelData.get(languageModelId); if (!data) { - throw LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); + throw extHostTypes.LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); } const local = Iterable.find(this._languageModels.values(), candidate => candidate.languageModelId === languageModelId); @@ -394,12 +419,12 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { return undefined; } - return { + return Object.freeze({ id: data.identifier, name: data.name, version: data.version, - tokens: data.tokens, - }; + contextLength: data.tokens, + }); } private readonly _languageAccessInformationExtensions = new Set>(); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index d163386bf00..f9571a57858 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2239,6 +2239,29 @@ export namespace ChatFollowup { } } + +export namespace LanguageModelChatMessage { + + export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage2 { + switch (message.role) { + case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.System, message.content); + case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.User, message.content); + case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.Assistant, message.content); + } + } + + export function from(message: vscode.LanguageModelChatMessage2): chatProvider.IChatMessage { + switch (message.role as types.LanguageModelChatMessageRole) { + case types.LanguageModelChatMessageRole.System: return { role: chatProvider.ChatMessageRole.System, content: message.content }; + case types.LanguageModelChatMessageRole.User: return { role: chatProvider.ChatMessageRole.User, content: message.content }; + case types.LanguageModelChatMessageRole.Assistant: return { role: chatProvider.ChatMessageRole.Assistant, content: message.content }; + } + } +} + +/** + * @deprecated + */ export namespace LanguageModelMessage { export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 9a85e2f08e8..c52e1c26cc2 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4424,6 +4424,25 @@ export enum ChatLocation { Editor = 4, } +export enum LanguageModelChatMessageRole { + User = 1, + Assistant = 2, + System = 3 +} + +export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessage2 { + + role: vscode.LanguageModelChatMessageRole; + content: string; + name: string | undefined; + + constructor(role: vscode.LanguageModelChatMessageRole, content: string, name?: string) { + this.role = role; + this.content = content; + this.name = name; + } +} + export class LanguageModelChatSystemMessage { content: string; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 6b52bf378b1..0cbeb156a4c 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -75,6 +75,7 @@ export const allApiProposals = Object.freeze({ interactive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts', interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', + languageModelSystem: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', languageModels: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModels.d.ts', languageStatusText: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', mappedEditsProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 504027744c4..6651c4ff859 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -19,6 +19,8 @@ declare module 'vscode' { onDidReceiveLanguageModelResponse2?: Event<{ readonly extensionId: string; readonly participant?: string; readonly tokenCount?: number }>; + provideLanguageModelResponse?(messages: LanguageModelChatMessage2[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; + provideLanguageModelResponse2(messages: LanguageModelChatMessage[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; provideTokenCount(text: string | LanguageModelChatMessage, token: CancellationToken): Thenable; diff --git a/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts b/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts new file mode 100644 index 00000000000..54e1da27a5b --- /dev/null +++ b/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/206265 + + // TODO@API don't have this dedicated type but as property, e.g anthropic doesn't have a system-role, see + // https://github.com/anthropics/anthropic-sdk-typescript/blob/c2da9604646ff103fbdbca016a9a9d49b03b387b/src/resources/messages.ts#L384 + // So, we could have `LanguageModelChatRequestOptions#system` which would be more limiting but also more natural? + + export enum LanguageModelChatMessageRole { + System = 3 + } +} diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 72db29500b9..be546fe75b1 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -5,6 +5,8 @@ declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/206265 + /** * Represents a language model response. * @@ -15,19 +17,84 @@ declare module 'vscode' { /** * An async iterable that is a stream of text chunks forming the overall response. * - * *Note* that this stream will error when during data receiving an error occurrs. + * *Note* that this stream will error when during data receiving an error occurs. Consumers of + * the stream should handle the errors accordingly. + * + * @example + * ```ts + * try { + * // consume stream + * for await (const chunk of response.stream) { + * console.log(chunk); + * } + * + * } catch(e) { + * // stream ended with an error + * console.error(e); + * } + * ``` */ stream: AsyncIterable; } + //TODO@API give this some structure + // https://github.com/openai/openai-openapi/blob/master/openapi.yaml#L7700, https://platform.openai.com/docs/guides/text-generation/chat-completions-api + // https://github.com/ollama/ollama/blob/main/docs/api.md#response-7 + // https://docs.anthropic.com/claude/reference/messages_post + export interface LanguageModelChatResponse2 { + + message: { + role: LanguageModelChatMessageRole.Assistant; + content: AsyncIterable; + }; + + + } + /** - * A language model message that represents a system message. - * - * System messages provide instructions to the language model that define the context in - * which user messages are interpreted. - * - * *Note* that a language model may choose to add additional system messages to the ones - * provided by extensions. + * Represents the role of a chat message. This is either the user or the assistant/model. + */ + export enum LanguageModelChatMessageRole { + /** + * The user role, e.g the human interacting with a language model. + */ + User = 1, + + /** + * The assistant role, e.g. the language model generating responses. + */ + Assistant = 2 + } + + // TODO@API name: LanguageModelChatMessage once the deprecated stuff is removed + export class LanguageModelChatMessage2 { + /** + * The role of this message. + */ + role: LanguageModelChatMessageRole; + + /** + * The content of this message. + */ + content: string; + + /** + * The optional name of a user for this message. + */ + name: string | undefined; + + /** + * Create a new user message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + constructor(role: LanguageModelChatMessageRole, content: string, name?: string); + } + + + /** + * @deprecated */ export class LanguageModelChatSystemMessage { @@ -45,7 +112,7 @@ declare module 'vscode' { } /** - * A language model message that represents a user message. + * @deprecated */ export class LanguageModelChatUserMessage { @@ -69,8 +136,7 @@ declare module 'vscode' { } /** - * A language model message that represents an assistant message, usually in response to a user message - * or as a sample response/reply-pair. + * @deprecated */ export class LanguageModelChatAssistantMessage { @@ -95,6 +161,7 @@ declare module 'vscode' { /** * Different types of language model messages. + * @deprecated */ export type LanguageModelChatMessage = LanguageModelChatSystemMessage | LanguageModelChatUserMessage | LanguageModelChatAssistantMessage; @@ -115,15 +182,18 @@ declare module 'vscode' { /** * The version of the language model. */ + // TODO@API drop this for now? readonly version: string; /** * The number of available tokens that can be used when sending requests * to the language model. * + * _Note_ that input- and output-tokens count towards this limit. + * * @see {@link lm.sendChatRequest} */ - readonly tokens: number; + readonly contextLength: number; } /** @@ -189,12 +259,6 @@ declare module 'vscode' { */ justification?: string; - /** - * Do not show the consent UI if the user has not yet granted access to the language model but fail the request instead. - */ - // TODO@API Revisit this, how do you do the first request? - silent?: boolean; - /** * A set of options that control the behavior of the language model. These options are specific to the language model * and need to be lookup in the respective documentation. @@ -228,18 +292,22 @@ declare module 'vscode' { /** * Make a chat request using a language model. * - * - *Note 1:* language model use may be subject to access restrictions and user consent. + * - *Note 1:* language model use may be subject to access restrictions and user consent. Calling this function + * for the first time (for a extension) will show a consent dialog to the user and because of that this function + * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} + * to check if they have the necessary permissions to make a request. * * - *Note 2:* language models are contributed by other extensions and as they evolve and change, * the set of available language models may change over time. Therefore it is strongly recommend to check - * {@link languageModels} for aviailable values and handle missing language models gracefully. + * {@link languageModels} for available values and handle missing language models gracefully. * * This function will return a rejected promise if making a request to the language model is not * possible. Reasons for this can be: * * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} - * - quota limits exceeded, see {@link LanguageModelError.cause `LanguageModelError.cause`} + * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} + * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} * * @param languageModel A language model identifier. * @param messages An array of message instances. @@ -247,7 +315,7 @@ declare module 'vscode' { * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. */ - export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options: LanguageModelChatRequestOptions, token: CancellationToken): Thenable; + export function sendChatRequest(languageModel: string, messages: (LanguageModelChatMessage | LanguageModelChatMessage2)[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; /** * Uses the language model specific tokenzier and computes the length in token of a given message. @@ -259,7 +327,10 @@ declare module 'vscode' { * @param token Optional cancellation token. * @returns A thenable that resolves to the length of the message in tokens. */ - export function computeTokenLength(languageModel: string, text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + // TODO@API `undefined` when the language model does not support computing token length + // ollama has nothing + // anthropic suggests to count after the fact https://github.com/anthropics/anthropic-tokenizer-typescript?tab=readme-ov-file#anthropic-typescript-tokenizer + export function computeTokenLength(languageModel: string, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token?: CancellationToken): Thenable; } /**