Path: blob/main/src/vs/workbench/contrib/chat/common/languageModels.ts
3296 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 { VSBuffer } from '../../../../base/common/buffer.js';6import { CancellationToken } from '../../../../base/common/cancellation.js';7import { Emitter, Event } from '../../../../base/common/event.js';8import { Iterable } from '../../../../base/common/iterator.js';9import { IJSONSchema } from '../../../../base/common/jsonSchema.js';10import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';11import { isFalsyOrWhitespace } from '../../../../base/common/strings.js';12import { ThemeIcon } from '../../../../base/common/themables.js';13import { URI } from '../../../../base/common/uri.js';14import { localize } from '../../../../nls.js';15import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';16import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';17import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';18import { ILogService } from '../../../../platform/log/common/log.js';19import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';20import { IExtensionService } from '../../../services/extensions/common/extensions.js';21import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';22import { ChatContextKeys } from './chatContextKeys.js';2324export const enum ChatMessageRole {25System,26User,27Assistant,28}2930export enum LanguageModelPartAudience {31Assistant = 0,32User = 1,33Extension = 2,34}3536export interface IChatMessageTextPart {37type: 'text';38value: string;39audience?: LanguageModelPartAudience[];40}4142export interface IChatMessageImagePart {43type: 'image_url';44value: IChatImageURLPart;45}4647export interface IChatMessageThinkingPart {48type: 'thinking';49value: string | string[];50id?: string;51metadata?: { readonly [key: string]: any };52}5354export interface IChatMessageDataPart {55type: 'data';56mimeType: string;57data: VSBuffer;58audience?: LanguageModelPartAudience[];59}6061export interface IChatImageURLPart {62/**63* The image's MIME type (e.g., "image/png", "image/jpeg").64*/65mimeType: ChatImageMimeType;6667/**68* The raw binary data of the image, encoded as a Uint8Array. Note: do not use base64 encoding. Maximum image size is 5MB.69*/70data: VSBuffer;71}7273/**74* Enum for supported image MIME types.75*/76export enum ChatImageMimeType {77PNG = 'image/png',78JPEG = 'image/jpeg',79GIF = 'image/gif',80WEBP = 'image/webp',81BMP = 'image/bmp',82}8384/**85* Specifies the detail level of the image.86*/87export enum ImageDetailLevel {88Low = 'low',89High = 'high'90}919293export interface IChatMessageToolResultPart {94type: 'tool_result';95toolCallId: string;96value: (IChatResponseTextPart | IChatResponsePromptTsxPart | IChatResponseDataPart)[];97isError?: boolean;98}99100export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart | IChatMessageDataPart | IChatMessageThinkingPart;101102export interface IChatMessage {103readonly name?: string | undefined;104readonly role: ChatMessageRole;105readonly content: IChatMessagePart[];106}107108export interface IChatResponseTextPart {109type: 'text';110value: string;111audience?: LanguageModelPartAudience[];112}113114export interface IChatResponsePromptTsxPart {115type: 'prompt_tsx';116value: unknown;117}118119export interface IChatResponseDataPart {120type: 'data';121mimeType: string;122data: VSBuffer;123audience?: LanguageModelPartAudience[];124}125126export interface IChatResponseToolUsePart {127type: 'tool_use';128name: string;129toolCallId: string;130parameters: any;131}132133export interface IChatResponseThinkingPart {134type: 'thinking';135value: string | string[];136id?: string;137metadata?: { readonly [key: string]: any };138}139140export interface IChatResponsePullRequestPart {141type: 'pullRequest';142uri: URI;143title: string;144description: string;145author: string;146linkTag: string;147}148149export type IChatResponsePart = IChatResponseTextPart | IChatResponseToolUsePart | IChatResponseDataPart | IChatResponseThinkingPart;150151export type IExtendedChatResponsePart = IChatResponsePullRequestPart;152153export interface ILanguageModelChatMetadata {154readonly extension: ExtensionIdentifier;155156readonly name: string;157readonly id: string;158readonly vendor: string;159readonly version: string;160readonly tooltip?: string;161readonly detail?: string;162readonly family: string;163readonly maxInputTokens: number;164readonly maxOutputTokens: number;165166readonly isDefault?: boolean;167readonly isUserSelectable?: boolean;168readonly statusIcon?: ThemeIcon;169readonly modelPickerCategory: { label: string; order: number } | undefined;170readonly auth?: {171readonly providerLabel: string;172readonly accountLabel?: string;173};174readonly capabilities?: {175readonly vision?: boolean;176readonly toolCalling?: boolean;177readonly agentMode?: boolean;178};179}180181export namespace ILanguageModelChatMetadata {182export function suitableForAgentMode(metadata: ILanguageModelChatMetadata): boolean {183const supportsToolsAgent = typeof metadata.capabilities?.agentMode === 'undefined' || metadata.capabilities.agentMode;184return supportsToolsAgent && !!metadata.capabilities?.toolCalling;185}186187export function asQualifiedName(metadata: ILanguageModelChatMetadata): string {188return `${metadata.name} (${metadata.vendor})`;189}190191export function matchesQualifiedName(name: string, metadata: ILanguageModelChatMetadata): boolean {192if (metadata.vendor === 'copilot' && name === metadata.name) {193return true;194}195return name === asQualifiedName(metadata);196}197}198199export interface ILanguageModelChatResponse {200stream: AsyncIterable<IChatResponsePart | IChatResponsePart[]>;201result: Promise<any>;202}203204export interface ILanguageModelChatProvider {205onDidChange: Event<void>;206provideLanguageModelChatInfo(options: { silent: boolean }, token: CancellationToken): Promise<ILanguageModelChatMetadataAndIdentifier[]>;207sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse>;208provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;209}210211export interface ILanguageModelChat {212metadata: ILanguageModelChatMetadata;213sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse>;214provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise<number>;215}216217export interface ILanguageModelChatSelector {218readonly name?: string;219readonly id?: string;220readonly vendor?: string;221readonly version?: string;222readonly family?: string;223readonly tokens?: number;224readonly extension?: ExtensionIdentifier;225}226227export const ILanguageModelsService = createDecorator<ILanguageModelsService>('ILanguageModelsService');228229export interface ILanguageModelChatMetadataAndIdentifier {230metadata: ILanguageModelChatMetadata;231identifier: string;232}233234export interface ILanguageModelsService {235236readonly _serviceBrand: undefined;237238// TODO @lramos15 - Make this a richer event in the future. Right now it just indicates some change happened, but not what239onDidChangeLanguageModels: Event<void>;240241updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void;242243getLanguageModelIds(): string[];244245getVendors(): IUserFriendlyLanguageModel[];246247lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined;248249/**250* Given a selector, returns a list of model identifiers251* @param selector The selector to lookup for language models. If the selector is empty, all language models are returned.252* @param allowPromptingUser If true the user may be prompted for things like API keys for us to select the model.253*/254selectLanguageModels(selector: ILanguageModelChatSelector, allowPromptingUser?: boolean): Promise<string[]>;255256registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable;257258sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse>;259260computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;261}262263const languageModelChatProviderType: IJSONSchema = {264type: 'object',265properties: {266vendor: {267type: 'string',268description: localize('vscode.extension.contributes.languageModels.vendor', "A globally unique vendor of language model chat provider.")269},270displayName: {271type: 'string',272description: localize('vscode.extension.contributes.languageModels.displayName', "The display name of the language model chat provider.")273},274managementCommand: {275type: 'string',276description: localize('vscode.extension.contributes.languageModels.managementCommand', "A command to manage the language model chat provider, e.g. 'Manage Copilot models'. This is used in the chat model picker. If not provided, a gear icon is not rendered during vendor selection.")277}278}279};280281export interface IUserFriendlyLanguageModel {282vendor: string;283displayName: string;284managementCommand?: string;285}286287export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint<IUserFriendlyLanguageModel | IUserFriendlyLanguageModel[]>({288extensionPoint: 'languageModelChatProviders',289jsonSchema: {290description: localize('vscode.extension.contributes.languageModelChatProviders', "Contribute language model chat providers of a specific vendor."),291oneOf: [292languageModelChatProviderType,293{294type: 'array',295items: languageModelChatProviderType296}297]298},299activationEventsGenerator: (contribs: IUserFriendlyLanguageModel[], result: { push(item: string): void }) => {300for (const contrib of contribs) {301result.push(`onLanguageModelChatProvider:${contrib.vendor}`);302}303}304});305306export class LanguageModelsService implements ILanguageModelsService {307308readonly _serviceBrand: undefined;309310private readonly _store = new DisposableStore();311312private readonly _providers = new Map<string, ILanguageModelChatProvider>();313private readonly _modelCache = new Map<string, ILanguageModelChatMetadata>();314private readonly _vendors = new Map<string, IUserFriendlyLanguageModel>();315private readonly _modelPickerUserPreferences: Record<string, boolean> = {}; // We use a record instead of a map for better serialization when storing316317private readonly _hasUserSelectableModels: IContextKey<boolean>;318private readonly _onLanguageModelChange = this._store.add(new Emitter<void>());319readonly onDidChangeLanguageModels: Event<void> = this._onLanguageModelChange.event;320321constructor(322@IExtensionService private readonly _extensionService: IExtensionService,323@ILogService private readonly _logService: ILogService,324@IStorageService private readonly _storageService: IStorageService,325@IContextKeyService _contextKeyService: IContextKeyService326) {327this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService);328this._modelPickerUserPreferences = this._storageService.getObject<Record<string, boolean>>('chatModelPickerPreferences', StorageScope.PROFILE, this._modelPickerUserPreferences);329330331332this._store.add(this.onDidChangeLanguageModels(() => {333this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable));334}));335336this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions) => {337338this._vendors.clear();339340for (const extension of extensions) {341for (const item of Iterable.wrap(extension.value)) {342if (this._vendors.has(item.vendor)) {343extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor));344continue;345}346if (isFalsyOrWhitespace(item.vendor)) {347extension.collector.error(localize('vscode.extension.contributes.languageModels.emptyVendor', "The vendor field cannot be empty."));348continue;349}350if (item.vendor.trim() !== item.vendor) {351extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace."));352continue;353}354this._vendors.set(item.vendor, item);355// Have some models we want from this vendor, so activate the extension356if (this._hasStoredModelForvendor(item.vendor)) {357this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`);358}359}360}361for (const [vendor, _] of this._providers) {362if (!this._vendors.has(vendor)) {363this._providers.delete(vendor);364}365}366}));367}368369private _hasStoredModelForvendor(vendor: string): boolean {370return Object.keys(this._modelPickerUserPreferences).some(modelId => {371return modelId.startsWith(vendor);372});373}374375dispose() {376this._store.dispose();377this._providers.clear();378}379380updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void {381const model = this._modelCache.get(modelIdentifier);382if (!model) {383this._logService.warn(`[LM] Cannot update model picker preference for unknown model ${modelIdentifier}`);384return;385}386387this._modelPickerUserPreferences[modelIdentifier] = showInModelPicker;388if (showInModelPicker === model.isUserSelectable) {389delete this._modelPickerUserPreferences[modelIdentifier];390this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER);391} else if (model.isUserSelectable !== showInModelPicker) {392this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER);393}394this._onLanguageModelChange.fire();395this._logService.trace(`[LM] Updated model picker preference for ${modelIdentifier} to ${showInModelPicker}`);396}397398getVendors(): IUserFriendlyLanguageModel[] {399return Array.from(this._vendors.values());400}401402getLanguageModelIds(): string[] {403return Array.from(this._modelCache.keys());404}405406lookupLanguageModel(modelIdentifier: string): ILanguageModelChatMetadata | undefined {407const model = this._modelCache.get(modelIdentifier);408if (model && this._modelPickerUserPreferences[modelIdentifier] !== undefined) {409return { ...model, isUserSelectable: this._modelPickerUserPreferences[modelIdentifier] };410}411return model;412}413414private _clearModelCache(vendors: string | string[]): void {415if (typeof vendors === 'string') {416vendors = [vendors];417}418for (const vendor of vendors) {419for (const [id, model] of this._modelCache.entries()) {420if (model.vendor === vendor) {421this._modelCache.delete(id);422}423}424}425}426427async resolveLanguageModels(vendors: string | string[], silent: boolean): Promise<void> {428if (typeof vendors === 'string') {429vendors = [vendors];430}431// Activate extensions before requesting to resolve the models432const all = vendors.map(vendor => this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendor}`));433await Promise.all(all);434this._clearModelCache(vendors);435for (const vendor of vendors) {436const provider = this._providers.get(vendor);437if (!provider) {438this._logService.warn(`[LM] No provider registered for vendor ${vendor}`);439continue;440}441try {442let modelsAndIdentifiers = await provider.provideLanguageModelChatInfo({ silent }, CancellationToken.None);443// This is a bit of a hack, when prompting user if the provider returns any models that are user selectable then we only want to show those and not the entire model list444if (!silent && modelsAndIdentifiers.some(m => m.metadata.isUserSelectable)) {445modelsAndIdentifiers = modelsAndIdentifiers.filter(m => m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true);446}447for (const modelAndIdentifier of modelsAndIdentifiers) {448if (this._modelCache.has(modelAndIdentifier.identifier)) {449this._logService.warn(`[LM] Model ${modelAndIdentifier.identifier} is already registered. Skipping.`);450continue;451}452this._modelCache.set(modelAndIdentifier.identifier, modelAndIdentifier.metadata);453}454this._logService.trace(`[LM] Resolved language models for vendor ${vendor}`, modelsAndIdentifiers);455} catch (error) {456this._logService.error(`[LM] Error resolving language models for vendor ${vendor}:`, error);457}458}459this._onLanguageModelChange.fire();460}461462async selectLanguageModels(selector: ILanguageModelChatSelector, allowPromptingUser?: boolean): Promise<string[]> {463464if (selector.vendor) {465await this.resolveLanguageModels([selector.vendor], !allowPromptingUser);466} else {467const allVendors = Array.from(this._vendors.keys());468await this.resolveLanguageModels(allVendors, !allowPromptingUser);469}470471const result: string[] = [];472473for (const [internalModelIdentifier, model] of this._modelCache) {474if ((selector.vendor === undefined || model.vendor === selector.vendor)475&& (selector.family === undefined || model.family === selector.family)476&& (selector.version === undefined || model.version === selector.version)477&& (selector.id === undefined || model.id === selector.id)) {478result.push(internalModelIdentifier);479}480}481482this._logService.trace('[LM] selected language models', selector, result);483484return result;485}486487registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable {488this._logService.trace('[LM] registering language model provider', vendor, provider);489490if (!this._vendors.has(vendor)) {491throw new Error(`Chat model provider uses UNKNOWN vendor ${vendor}.`);492}493if (this._providers.has(vendor)) {494throw new Error(`Chat model provider for vendor ${vendor} is already registered.`);495}496497this._providers.set(vendor, provider);498499// TODO @lramos15 - Smarter restore logic. Don't resolve models for all providers, but only those which were known to need restoring500this.resolveLanguageModels(vendor, true).then(() => {501this._onLanguageModelChange.fire();502});503504return toDisposable(() => {505this._logService.trace('[LM] UNregistered language model provider', vendor);506this._clearModelCache(vendor);507this._providers.delete(vendor);508});509}510511async sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse> {512const provider = this._providers.get(this._modelCache.get(modelId)?.vendor || '');513if (!provider) {514throw new Error(`Chat provider for model ${modelId} is not registered.`);515}516return provider.sendChatRequest(modelId, messages, from, options, token);517}518519computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number> {520const model = this._modelCache.get(modelId);521if (!model) {522throw new Error(`Chat model ${modelId} could not be found.`);523}524const provider = this._providers.get(model.vendor);525if (!provider) {526throw new Error(`Chat provider for model ${modelId} is not registered.`);527}528return provider.provideTokenCount(modelId, message, token);529}530}531532533