Path: blob/main/src/vs/workbench/contrib/chat/common/languageModels.ts
5240 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 { SequencerByKey } from '../../../../base/common/async.js';6import { VSBuffer } from '../../../../base/common/buffer.js';7import { CancellationToken } from '../../../../base/common/cancellation.js';8import { IStringDictionary } from '../../../../base/common/collections.js';9import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js';10import { Emitter, Event } from '../../../../base/common/event.js';11import { hash } from '../../../../base/common/hash.js';12import { Iterable } from '../../../../base/common/iterator.js';13import { IJSONSchema, TypeFromJsonSchema } from '../../../../base/common/jsonSchema.js';14import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';15import { equals } from '../../../../base/common/objects.js';16import Severity from '../../../../base/common/severity.js';17import { format, isFalsyOrWhitespace } from '../../../../base/common/strings.js';18import { ThemeIcon } from '../../../../base/common/themables.js';19import { isString } from '../../../../base/common/types.js';20import { URI } from '../../../../base/common/uri.js';21import { generateUuid } from '../../../../base/common/uuid.js';22import { localize } from '../../../../nls.js';23import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';24import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';25import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';26import { ILogService } from '../../../../platform/log/common/log.js';27import { IQuickInputService, QuickInputHideReason } from '../../../../platform/quickinput/common/quickInput.js';28import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';29import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';30import { IExtensionService } from '../../../services/extensions/common/extensions.js';31import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';32import { ChatContextKeys } from './actions/chatContextKeys.js';33import { ChatAgentLocation } from './constants.js';34import { ILanguageModelsProviderGroup, ILanguageModelsConfigurationService } from './languageModelsConfiguration.js';3536export const enum ChatMessageRole {37System,38User,39Assistant,40}4142export enum LanguageModelPartAudience {43Assistant = 0,44User = 1,45Extension = 2,46}4748export interface IChatMessageTextPart {49type: 'text';50value: string;51audience?: LanguageModelPartAudience[];52}5354export interface IChatMessageImagePart {55type: 'image_url';56value: IChatImageURLPart;57}5859export interface IChatMessageThinkingPart {60type: 'thinking';61value: string | string[];62id?: string;63// eslint-disable-next-line @typescript-eslint/no-explicit-any64metadata?: { readonly [key: string]: any };65}6667export interface IChatMessageDataPart {68type: 'data';69mimeType: string;70data: VSBuffer;71audience?: LanguageModelPartAudience[];72}7374export interface IChatImageURLPart {75/**76* The image's MIME type (e.g., "image/png", "image/jpeg").77*/78mimeType: ChatImageMimeType;7980/**81* The raw binary data of the image, encoded as a Uint8Array. Note: do not use base64 encoding. Maximum image size is 5MB.82*/83data: VSBuffer;84}8586/**87* Enum for supported image MIME types.88*/89export enum ChatImageMimeType {90PNG = 'image/png',91JPEG = 'image/jpeg',92GIF = 'image/gif',93WEBP = 'image/webp',94BMP = 'image/bmp',95}9697/**98* Specifies the detail level of the image.99*/100export enum ImageDetailLevel {101Low = 'low',102High = 'high'103}104105106export interface IChatMessageToolResultPart {107type: 'tool_result';108toolCallId: string;109value: (IChatResponseTextPart | IChatResponsePromptTsxPart | IChatResponseDataPart)[];110isError?: boolean;111}112113export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart | IChatMessageDataPart | IChatMessageThinkingPart;114115export interface IChatMessage {116readonly name?: string | undefined;117readonly role: ChatMessageRole;118readonly content: IChatMessagePart[];119}120121export interface IChatResponseTextPart {122type: 'text';123value: string;124audience?: LanguageModelPartAudience[];125}126127export interface IChatResponsePromptTsxPart {128type: 'prompt_tsx';129value: unknown;130}131132export interface IChatResponseDataPart {133type: 'data';134mimeType: string;135data: VSBuffer;136audience?: LanguageModelPartAudience[];137}138139export interface IChatResponseToolUsePart {140type: 'tool_use';141name: string;142toolCallId: string;143// eslint-disable-next-line @typescript-eslint/no-explicit-any144parameters: any;145}146147export interface IChatResponseThinkingPart {148type: 'thinking';149value: string | string[];150id?: string;151// eslint-disable-next-line @typescript-eslint/no-explicit-any152metadata?: { readonly [key: string]: any };153}154155export interface IChatResponsePullRequestPart {156type: 'pullRequest';157uri: URI;158title: string;159description: string;160author: string;161linkTag: string;162}163164export type IChatResponsePart = IChatResponseTextPart | IChatResponseToolUsePart | IChatResponseDataPart | IChatResponseThinkingPart;165166export type IExtendedChatResponsePart = IChatResponsePullRequestPart;167168export interface ILanguageModelChatMetadata {169readonly extension: ExtensionIdentifier;170171readonly name: string;172readonly id: string;173readonly vendor: string;174readonly version: string;175readonly tooltip?: string;176readonly detail?: string;177readonly multiplier?: string;178readonly multiplierNumeric?: number;179readonly family: string;180readonly maxInputTokens: number;181readonly maxOutputTokens: number;182183readonly isDefaultForLocation: { [K in ChatAgentLocation]?: boolean };184readonly isUserSelectable?: boolean;185readonly statusIcon?: ThemeIcon;186readonly modelPickerCategory: { label: string; order: number } | undefined;187readonly auth?: {188readonly providerLabel: string;189readonly accountLabel?: string;190};191readonly capabilities?: {192readonly vision?: boolean;193readonly toolCalling?: boolean;194readonly agentMode?: boolean;195readonly editTools?: ReadonlyArray<string>;196};197}198199export namespace ILanguageModelChatMetadata {200export function suitableForAgentMode(metadata: ILanguageModelChatMetadata): boolean {201const supportsToolsAgent = typeof metadata.capabilities?.agentMode === 'undefined' || metadata.capabilities.agentMode;202return supportsToolsAgent && !!metadata.capabilities?.toolCalling;203}204205export function asQualifiedName(metadata: ILanguageModelChatMetadata): string {206return `${metadata.name} (${metadata.vendor})`;207}208209export function matchesQualifiedName(name: string, metadata: ILanguageModelChatMetadata): boolean {210if (metadata.vendor === 'copilot' && name === metadata.name) {211return true;212}213return name === asQualifiedName(metadata);214}215}216217export interface ILanguageModelChatResponse {218stream: AsyncIterable<IChatResponsePart | IChatResponsePart[]>;219// eslint-disable-next-line @typescript-eslint/no-explicit-any220result: Promise<any>;221}222223export interface ILanguageModelChatProvider {224readonly onDidChange: Event<void>;225provideLanguageModelChatInfo(options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise<ILanguageModelChatMetadataAndIdentifier[]>;226sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: unknown }, token: CancellationToken): Promise<ILanguageModelChatResponse>;227provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;228}229230export interface ILanguageModelChat {231metadata: ILanguageModelChatMetadata;232sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: unknown }, token: CancellationToken): Promise<ILanguageModelChatResponse>;233provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise<number>;234}235236export interface ILanguageModelChatSelector {237readonly name?: string;238readonly id?: string;239readonly vendor?: string;240readonly version?: string;241readonly family?: string;242readonly tokens?: number;243readonly extension?: ExtensionIdentifier;244}245246247export function isILanguageModelChatSelector(value: unknown): value is ILanguageModelChatSelector {248if (typeof value !== 'object' || value === null) {249return false;250}251const obj = value as Record<string, unknown>;252return (253(obj.name === undefined || typeof obj.name === 'string') &&254(obj.id === undefined || typeof obj.id === 'string') &&255(obj.vendor === undefined || typeof obj.vendor === 'string') &&256(obj.version === undefined || typeof obj.version === 'string') &&257(obj.family === undefined || typeof obj.family === 'string') &&258(obj.tokens === undefined || typeof obj.tokens === 'number') &&259(obj.extension === undefined || typeof obj.extension === 'object')260);261}262263export const ILanguageModelsService = createDecorator<ILanguageModelsService>('ILanguageModelsService');264265export interface ILanguageModelChatMetadataAndIdentifier {266metadata: ILanguageModelChatMetadata;267identifier: string;268}269270export interface ILanguageModelChatInfoOptions {271readonly group?: string;272readonly silent: boolean;273readonly configuration?: IStringDictionary<unknown>;274}275276export interface ILanguageModelsGroup {277readonly group?: ILanguageModelsProviderGroup;278readonly modelIdentifiers: string[];279readonly status?: {280readonly message: string;281readonly severity: Severity;282};283}284285export interface ILanguageModelsService {286287readonly _serviceBrand: undefined;288289readonly onDidChangeLanguageModelVendors: Event<readonly string[]>;290readonly onDidChangeLanguageModels: Event<string>;291292updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void;293294getLanguageModelIds(): string[];295296getVendors(): ILanguageModelProviderDescriptor[];297298lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined;299300/**301* Find a model by its qualified name. The qualified name is what is used in prompt and agent files and is in the format "Model Name (Vendor)".302*/303lookupLanguageModelByQualifiedName(qualifiedName: string): ILanguageModelChatMetadataAndIdentifier | undefined;304305getLanguageModelGroups(vendor: string): ILanguageModelsGroup[];306307/**308* Given a selector, returns a list of model identifiers309* @param selector The selector to lookup for language models. If the selector is empty, all language models are returned.310*/311selectLanguageModels(selector: ILanguageModelChatSelector): Promise<string[]>;312313registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable;314315deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void;316317// eslint-disable-next-line @typescript-eslint/no-explicit-any318sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse>;319320computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;321322addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void>;323324removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void>;325326configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise<void>;327328migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise<void>;329}330331const languageModelChatProviderType = {332type: 'object',333required: ['vendor', 'displayName'],334properties: {335vendor: {336type: 'string',337description: localize('vscode.extension.contributes.languageModels.vendor', "A globally unique vendor of language model chat provider.")338},339displayName: {340type: 'string',341description: localize('vscode.extension.contributes.languageModels.displayName', "The display name of the language model chat provider.")342},343configuration: {344type: 'object',345description: localize('vscode.extension.contributes.languageModels.configuration', "Configuration options for the language model chat provider."),346anyOf: [347{348$ref: 'http://json-schema.org/draft-07/schema#'349},350{351properties: {352properties: {353type: 'object',354additionalProperties: {355$ref: 'http://json-schema.org/draft-07/schema#',356properties: {357secret: {358type: 'boolean',359description: localize('vscode.extension.contributes.languageModels.configuration.secret', "Whether the property is a secret.")360}361}362}363},364additionalProperties: {365$ref: 'http://json-schema.org/draft-07/schema#',366properties: {367secret: {368type: 'boolean',369description: localize('vscode.extension.contributes.languageModels.configuration.secret', "Whether the property is a secret.")370}371}372}373}374}375]376377},378managementCommand: {379type: 'string',380description: 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."),381deprecated: true,382deprecationMessage: localize('vscode.extension.contributes.languageModels.managementCommand.deprecated', "The managementCommand property is deprecated and will be removed in a future release. Use the new configuration property instead.")383},384when: {385type: 'string',386description: localize('vscode.extension.contributes.languageModels.when', "Condition which must be true to show this language model chat provider in the Manage Models list.")387}388}389} as const satisfies IJSONSchema;390391export type IUserFriendlyLanguageModel = TypeFromJsonSchema<typeof languageModelChatProviderType>;392393export interface ILanguageModelProviderDescriptor extends IUserFriendlyLanguageModel {394readonly isDefault: boolean;395}396397export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint<IUserFriendlyLanguageModel | IUserFriendlyLanguageModel[]>({398extensionPoint: 'languageModelChatProviders',399jsonSchema: {400description: localize('vscode.extension.contributes.languageModelChatProviders', "Contribute language model chat providers of a specific vendor."),401oneOf: [402languageModelChatProviderType,403{404type: 'array',405items: languageModelChatProviderType406}407]408},409activationEventsGenerator: function* (contribs: readonly IUserFriendlyLanguageModel[]) {410for (const contrib of contribs) {411yield `onLanguageModelChatProvider:${contrib.vendor}`;412}413}414});415416const CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY = 'chatModelPickerPreferences';417418export class LanguageModelsService implements ILanguageModelsService {419420private static SECRET_KEY_PREFIX = 'chat.lm.secret.';421private static SECRET_INPUT = '${input:{0}}';422423readonly _serviceBrand: undefined;424425private readonly _store = new DisposableStore();426427private readonly _providers = new Map<string, ILanguageModelChatProvider>();428private readonly _vendors = new Map<string, ILanguageModelProviderDescriptor>();429430private readonly _onDidChangeLanguageModelVendors = this._store.add(new Emitter<string[]>());431readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event;432433private readonly _modelsGroups = new Map<string, ILanguageModelsGroup[]>();434private readonly _modelCache = new Map<string, ILanguageModelChatMetadata>();435private readonly _resolveLMSequencer = new SequencerByKey<string>();436private _modelPickerUserPreferences: IStringDictionary<boolean> = {};437private readonly _hasUserSelectableModels: IContextKey<boolean>;438439private readonly _onLanguageModelChange = this._store.add(new Emitter<string>());440readonly onDidChangeLanguageModels: Event<string> = this._onLanguageModelChange.event;441442constructor(443@IExtensionService private readonly _extensionService: IExtensionService,444@ILogService private readonly _logService: ILogService,445@IStorageService private readonly _storageService: IStorageService,446@IContextKeyService private readonly _contextKeyService: IContextKeyService,447@ILanguageModelsConfigurationService private readonly _languageModelsConfigurationService: ILanguageModelsConfigurationService,448@IQuickInputService private readonly _quickInputService: IQuickInputService,449@ISecretStorageService private readonly _secretStorageService: ISecretStorageService,450) {451this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService);452this._modelPickerUserPreferences = this._readModelPickerPreferences();453this._store.add(this._storageService.onDidChangeValue(StorageScope.PROFILE, CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, this._store)(() => this._onDidChangeModelPickerPreferences()));454455this._store.add(this.onDidChangeLanguageModels(() => this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable))));456this._store.add(this._languageModelsConfigurationService.onDidChangeLanguageModelGroups(changedGroups => this._onDidChangeLanguageModelGroups(changedGroups)));457458this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions, { added, removed }) => {459const addedVendors: IUserFriendlyLanguageModel[] = [];460const removedVendors: IUserFriendlyLanguageModel[] = [];461462for (const extension of added) {463for (const item of Iterable.wrap(extension.value)) {464if (this._vendors.has(item.vendor)) {465extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor));466continue;467}468if (isFalsyOrWhitespace(item.vendor)) {469extension.collector.error(localize('vscode.extension.contributes.languageModels.emptyVendor', "The vendor field cannot be empty."));470continue;471}472if (item.vendor.trim() !== item.vendor) {473extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace."));474continue;475}476addedVendors.push(item);477}478}479480for (const extension of removed) {481for (const item of Iterable.wrap(extension.value)) {482removedVendors.push(item);483}484}485486this.deltaLanguageModelChatProviderDescriptors(addedVendors, removedVendors);487}));488}489490deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void {491const addedVendorIds: string[] = [];492const removedVendorIds: string[] = [];493494for (const item of added) {495if (this._vendors.has(item.vendor)) {496this._logService.error(`The vendor '${item.vendor}' is already registered and cannot be registered twice`);497continue;498}499if (isFalsyOrWhitespace(item.vendor)) {500this._logService.error('The vendor field cannot be empty.');501continue;502}503if (item.vendor.trim() !== item.vendor) {504this._logService.error('The vendor field cannot start or end with whitespace.');505continue;506}507const vendor: ILanguageModelProviderDescriptor = {508vendor: item.vendor,509displayName: item.displayName,510configuration: item.configuration,511managementCommand: item.managementCommand,512when: item.when,513isDefault: item.vendor === 'copilot'514};515this._vendors.set(item.vendor, vendor);516addedVendorIds.push(item.vendor);517// Have some models we want from this vendor, so activate the extension518if (this._hasStoredModelForVendor(item.vendor)) {519this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`);520}521}522523for (const item of removed) {524this._vendors.delete(item.vendor);525this._providers.delete(item.vendor);526this._clearModelCache(item.vendor);527removedVendorIds.push(item.vendor);528}529530for (const [vendor, _] of this._providers) {531if (!this._vendors.has(vendor)) {532this._providers.delete(vendor);533}534}535536if (addedVendorIds.length > 0 || removedVendorIds.length > 0) {537this._onDidChangeLanguageModelVendors.fire([...addedVendorIds, ...removedVendorIds]);538if (removedVendorIds.length > 0) {539for (const vendor of removedVendorIds) {540this._onLanguageModelChange.fire(vendor);541}542}543}544}545546private async _onDidChangeLanguageModelGroups(changedGroups: readonly ILanguageModelsProviderGroup[]): Promise<void> {547const changedVendors = new Set(changedGroups.map(g => g.vendor));548await Promise.all(Array.from(changedVendors).map(vendor => this._resolveAllLanguageModels(vendor, true)));549}550551private _readModelPickerPreferences(): IStringDictionary<boolean> {552return this._storageService.getObject<IStringDictionary<boolean>>(CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, StorageScope.PROFILE, {});553}554555private _onDidChangeModelPickerPreferences(): void {556const newPreferences = this._readModelPickerPreferences();557const oldPreferences = this._modelPickerUserPreferences;558559// Check if there are any changes by computing diff560const affectedVendors = new Set<string>();561let hasChanges = false;562563// Check for added or updated keys564for (const modelId in newPreferences) {565if (oldPreferences[modelId] !== newPreferences[modelId]) {566hasChanges = true;567const model = this._modelCache.get(modelId);568if (model) {569affectedVendors.add(model.vendor);570}571}572}573574// Check for removed keys575for (const modelId in oldPreferences) {576if (!newPreferences.hasOwnProperty(modelId)) {577hasChanges = true;578const model = this._modelCache.get(modelId);579if (model) {580affectedVendors.add(model.vendor);581}582}583}584585if (hasChanges) {586this._logService.trace('[LM] Updated model picker preferences from storage');587this._modelPickerUserPreferences = newPreferences;588for (const vendor of affectedVendors) {589this._onLanguageModelChange.fire(vendor);590}591}592}593594private _hasStoredModelForVendor(vendor: string): boolean {595return Object.keys(this._modelPickerUserPreferences).some(modelId => {596return modelId.startsWith(vendor);597});598}599600private _saveModelPickerPreferences(): void {601this._storageService.store(CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER);602}603604updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void {605const model = this._modelCache.get(modelIdentifier);606if (!model) {607this._logService.warn(`[LM] Cannot update model picker preference for unknown model ${modelIdentifier}`);608return;609}610611this._modelPickerUserPreferences[modelIdentifier] = showInModelPicker;612if (showInModelPicker === model.isUserSelectable) {613delete this._modelPickerUserPreferences[modelIdentifier];614this._saveModelPickerPreferences();615} else if (model.isUserSelectable !== showInModelPicker) {616this._saveModelPickerPreferences();617}618this._onLanguageModelChange.fire(model.vendor);619this._logService.trace(`[LM] Updated model picker preference for ${modelIdentifier} to ${showInModelPicker}`);620}621622getVendors(): ILanguageModelProviderDescriptor[] {623return Array.from(this._vendors.values())624.filter(vendor => {625if (!vendor.when) {626return true; // No when clause means always visible627}628const whenClause = ContextKeyExpr.deserialize(vendor.when);629return whenClause ? this._contextKeyService.contextMatchesRules(whenClause) : false;630});631}632633getLanguageModelIds(): string[] {634return Array.from(this._modelCache.keys());635}636637lookupLanguageModel(modelIdentifier: string): ILanguageModelChatMetadata | undefined {638const model = this._modelCache.get(modelIdentifier);639if (model && this._modelPickerUserPreferences[modelIdentifier] !== undefined) {640return { ...model, isUserSelectable: this._modelPickerUserPreferences[modelIdentifier] };641}642return model;643}644645lookupLanguageModelByQualifiedName(referenceName: string): ILanguageModelChatMetadataAndIdentifier | undefined {646for (const [identifier, model] of this._modelCache.entries()) {647if (ILanguageModelChatMetadata.matchesQualifiedName(referenceName, model)) {648return { metadata: model, identifier };649}650}651return undefined;652}653654private async _resolveAllLanguageModels(vendorId: string, silent: boolean): Promise<void> {655656const vendor = this._vendors.get(vendorId);657658if (!vendor) {659return;660}661662// Activate extensions before requesting to resolve the models663await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendorId}`);664665const provider = this._providers.get(vendorId);666if (!provider) {667this._logService.warn(`[LM] No provider registered for vendor ${vendorId}`);668return;669}670671return this._resolveLMSequencer.queue(vendorId, async () => {672673const allModels: ILanguageModelChatMetadataAndIdentifier[] = [];674const languageModelsGroups: ILanguageModelsGroup[] = [];675676try {677const models = await provider.provideLanguageModelChatInfo({ silent }, CancellationToken.None);678if (models.length) {679allModels.push(...models);680const modelIdentifiers = [];681for (const m of models) {682if (vendor.isDefault) {683// Special case for copilot models - they are all user selectable unless marked otherwise684if (m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true) {685modelIdentifiers.push(m.identifier);686} else {687this._logService.trace(`[LM] Skipping model ${m.identifier} from model picker as it is not user selectable.`);688}689} else {690modelIdentifiers.push(m.identifier);691}692}693languageModelsGroups.push({ modelIdentifiers });694}695} catch (error) {696languageModelsGroups.push({697modelIdentifiers: [],698status: {699message: getErrorMessage(error),700severity: Severity.Error701}702});703}704705const groups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();706for (const group of groups) {707if (group.vendor !== vendorId) {708continue;709}710711const configuration = await this._resolveConfiguration(group, vendor.configuration);712713try {714const models = await provider.provideLanguageModelChatInfo({ group: group.name, silent, configuration }, CancellationToken.None);715if (models.length) {716allModels.push(...models);717languageModelsGroups.push({ group, modelIdentifiers: models.map(m => m.identifier) });718}719} catch (error) {720languageModelsGroups.push({721group,722modelIdentifiers: [],723status: {724message: getErrorMessage(error),725severity: Severity.Error726}727});728}729}730731this._modelsGroups.set(vendorId, languageModelsGroups);732const oldModels = this._clearModelCache(vendorId);733let hasChanges = false;734for (const model of allModels) {735if (this._modelCache.has(model.identifier)) {736this._logService.warn(`[LM] Model ${model.identifier} is already registered. Skipping.`);737continue;738}739this._modelCache.set(model.identifier, model.metadata);740hasChanges = hasChanges || !equals(oldModels.get(model.identifier), model.metadata);741oldModels.delete(model.identifier);742}743this._logService.trace(`[LM] Resolved language models for vendor ${vendorId}`, allModels);744hasChanges = hasChanges || oldModels.size > 0;745746if (hasChanges) {747this._onLanguageModelChange.fire(vendorId);748} else {749this._logService.trace(`[LM] No changes in language models for vendor ${vendorId}`);750}751});752}753754getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] {755return this._modelsGroups.get(vendor) ?? [];756}757758async selectLanguageModels(selector: ILanguageModelChatSelector): Promise<string[]> {759760if (selector.vendor) {761await this._resolveAllLanguageModels(selector.vendor, true);762} else {763const allVendors = Array.from(this._vendors.keys());764await Promise.all(allVendors.map(vendor => this._resolveAllLanguageModels(vendor, true)));765}766767const result: string[] = [];768769for (const [internalModelIdentifier, model] of this._modelCache) {770if ((selector.vendor === undefined || model.vendor === selector.vendor)771&& (selector.family === undefined || model.family === selector.family)772&& (selector.version === undefined || model.version === selector.version)773&& (selector.id === undefined || model.id === selector.id)) {774result.push(internalModelIdentifier);775}776}777778this._logService.trace('[LM] selected language models', selector, result);779780return result;781}782783registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable {784this._logService.trace('[LM] registering language model provider', vendor, provider);785786if (!this._vendors.has(vendor)) {787throw new Error(`Chat model provider uses UNKNOWN vendor ${vendor}.`);788}789if (this._providers.has(vendor)) {790throw new Error(`Chat model provider for vendor ${vendor} is already registered.`);791}792793this._providers.set(vendor, provider);794795if (this._hasStoredModelForVendor(vendor)) {796this._resolveAllLanguageModels(vendor, true);797}798799const modelChangeListener = provider.onDidChange(() => {800this._resolveAllLanguageModels(vendor, true);801});802803return toDisposable(() => {804this._logService.trace('[LM] UNregistered language model provider', vendor);805this._clearModelCache(vendor);806this._providers.delete(vendor);807modelChangeListener.dispose();808});809}810811// eslint-disable-next-line @typescript-eslint/no-explicit-any812async sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse> {813const provider = this._providers.get(this._modelCache.get(modelId)?.vendor || '');814if (!provider) {815throw new Error(`Chat provider for model ${modelId} is not registered.`);816}817return provider.sendChatRequest(modelId, messages, from, options, token);818}819820computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number> {821const model = this._modelCache.get(modelId);822if (!model) {823throw new Error(`Chat model ${modelId} could not be found.`);824}825const provider = this._providers.get(model.vendor);826if (!provider) {827throw new Error(`Chat provider for model ${modelId} is not registered.`);828}829return provider.provideTokenCount(modelId, message, token);830}831832async configureLanguageModelsProviderGroup(vendorId: string, providerGroupName?: string): Promise<void> {833834const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);835if (!vendor) {836throw new Error(`Vendor ${vendorId} not found.`);837}838839if (vendor.managementCommand) {840await this._resolveAllLanguageModels(vendor.vendor, false);841return;842}843844const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();845const existing = languageModelProviderGroups.find(g => g.vendor === vendorId && g.name === providerGroupName);846847const name = await this.promptForName(languageModelProviderGroups, vendor, existing);848if (!name) {849return;850}851852const existingConfiguration = existing ? await this._resolveConfiguration(existing, vendor.configuration) : undefined;853854try {855const configuration = vendor.configuration ? await this.promptForConfiguration(name, vendor.configuration, existingConfiguration) : undefined;856if (vendor.configuration && !configuration) {857return;858}859860const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration);861const saved = existing862? await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, languageModelProviderGroup)863: await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup);864865if (vendor.configuration && this.requireConfiguring(vendor.configuration)) {866const snippet = this.getSnippetForFirstUnconfiguredProperty(configuration ?? {}, vendor.configuration);867await this._languageModelsConfigurationService.configureLanguageModels({ group: saved, snippet });868}869} catch (error) {870if (isCancellationError(error)) {871return;872}873throw error;874}875}876877async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void> {878const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);879if (!vendor) {880throw new Error(`Vendor ${vendorId} not found.`);881}882883const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration);884await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup);885}886887async removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void> {888const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);889if (!vendor) {890throw new Error(`Vendor ${vendorId} not found.`);891}892893const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();894const existing = languageModelProviderGroups.find(g => g.vendor === vendorId && g.name === providerGroupName);895896if (!existing) {897throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);898}899900await this._deleteSecretsInConfiguration(existing, vendor.configuration);901await this._languageModelsConfigurationService.removeLanguageModelsProviderGroup(existing);902}903904private requireConfiguring(schema: IJSONSchema): boolean {905if (schema.additionalProperties) {906return true;907}908if (!schema.properties) {909return false;910}911for (const property of Object.keys(schema.properties)) {912if (!this.canPromptForProperty(schema.properties[property])) {913return true;914}915}916return false;917}918919private getSnippetForFirstUnconfiguredProperty(configuration: IStringDictionary<unknown>, schema: IJSONSchema): string | undefined {920if (!schema.properties) {921return undefined;922}923for (const property of Object.keys(schema.properties)) {924if (configuration[property] === undefined) {925const propertySchema = schema.properties[property];926if (propertySchema && typeof propertySchema !== 'boolean' && propertySchema.defaultSnippets?.[0]) {927const snippet = propertySchema.defaultSnippets[0];928let bodyText = snippet.bodyText ?? JSON.stringify(snippet.body, null, '\t');929// Handle ^ prefix for raw values (numbers/booleans) - remove quotes around ^-prefixed values930bodyText = bodyText.replace(/"(\^[^"]*)"/g, (_, value) => value.substring(1));931return `"${property}": ${bodyText}`;932}933}934}935return undefined;936}937938private async promptForName(languageModelProviderGroups: readonly ILanguageModelsProviderGroup[], vendor: IUserFriendlyLanguageModel, existing: ILanguageModelsProviderGroup | undefined): Promise<string | undefined> {939let providerGroupName = existing?.name;940if (!providerGroupName) {941providerGroupName = vendor.displayName;942let count = 1;943while (languageModelProviderGroups.some(g => g.vendor === vendor.vendor && g.name === providerGroupName)) {944count++;945providerGroupName = `${vendor.displayName} ${count}`;946}947}948949let result: string | undefined;950const disposables = new DisposableStore();951try {952await new Promise<void>(resolve => {953const inputBox = disposables.add(this._quickInputService.createInputBox());954inputBox.title = localize('configureLanguageModelGroup', "Group Name");955inputBox.placeholder = localize('languageModelGroupName', "Enter a name for the group");956inputBox.value = providerGroupName;957inputBox.ignoreFocusOut = true;958959disposables.add(inputBox.onDidChangeValue(value => {960if (!value) {961inputBox.validationMessage = localize('enterName', "Please enter a name");962inputBox.severity = Severity.Error;963return;964}965if (!existing && languageModelProviderGroups.some(g => g.name === value)) {966inputBox.validationMessage = localize('nameExists', "A language models group with this name already exists");967inputBox.severity = Severity.Error;968return;969}970inputBox.validationMessage = undefined;971inputBox.severity = Severity.Ignore;972}));973disposables.add(inputBox.onDidAccept(async () => {974result = inputBox.value;975inputBox.hide();976}));977disposables.add(inputBox.onDidHide(() => resolve()));978inputBox.show();979});980} finally {981disposables.dispose();982}983return result;984}985986private async promptForConfiguration(groupName: string, configuration: IJSONSchema, existing: IStringDictionary<unknown> | undefined): Promise<IStringDictionary<unknown> | undefined> {987if (!configuration.properties) {988return;989}990991const result: IStringDictionary<unknown> = existing ? { ...existing } : {};992993for (const property of Object.keys(configuration.properties)) {994const propertySchema = configuration.properties[property];995const required = !!configuration.required?.includes(property);996const value = await this.promptForValue(groupName, property, propertySchema, required, existing);997if (value !== undefined) {998result[property] = value;999}1000}10011002return result;1003}10041005private async promptForValue(groupName: string, property: string, propertySchema: IJSONSchema | undefined, required: boolean, existing: IStringDictionary<unknown> | undefined): Promise<unknown | undefined> {1006if (!propertySchema) {1007return undefined;1008}10091010if (!this.canPromptForProperty(propertySchema)) {1011return undefined;1012}10131014if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) {1015const selectedItems = await this.promptForArray(groupName, property, propertySchema);1016if (selectedItems === undefined) {1017return undefined;1018}1019return selectedItems;1020}10211022const value = await this.promptForInput(groupName, property, propertySchema, required, existing);1023if (value === undefined) {1024return undefined;1025}10261027return value;1028}10291030private canPromptForProperty(propertySchema: IJSONSchema | undefined): boolean {1031if (!propertySchema || typeof propertySchema === 'boolean') {1032return false;1033}10341035if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) {1036return true;1037}10381039if (propertySchema.type === 'string' || propertySchema.type === 'number' || propertySchema.type === 'integer' || propertySchema.type === 'boolean') {1040return true;1041}10421043return false;1044}10451046private async promptForArray(groupName: string, property: string, propertySchema: IJSONSchema): Promise<string[] | undefined> {1047if (!propertySchema.items || Array.isArray(propertySchema.items) || !propertySchema.items.enum) {1048return undefined;1049}1050const items = propertySchema.items.enum;1051const disposables = new DisposableStore();1052try {1053return await new Promise<string[] | undefined>(resolve => {1054const quickPick = disposables.add(this._quickInputService.createQuickPick());1055quickPick.title = `${groupName}: ${propertySchema.title ?? property}`;1056quickPick.items = items.map(item => ({ label: item }));1057quickPick.placeholder = propertySchema.description ?? localize('selectValue', "Select value for {0}", property);1058quickPick.canSelectMany = true;1059quickPick.ignoreFocusOut = true;10601061disposables.add(quickPick.onDidAccept(() => {1062resolve(quickPick.selectedItems.map(item => item.label));1063quickPick.hide();1064}));1065disposables.add(quickPick.onDidHide(() => {1066resolve(undefined);1067}));1068quickPick.show();1069});1070} finally {1071disposables.dispose();1072}1073}10741075private async promptForInput(groupName: string, property: string, propertySchema: IJSONSchema, required: boolean, existing: IStringDictionary<unknown> | undefined): Promise<string | number | boolean | undefined> {1076const disposables = new DisposableStore();1077try {1078const value = await new Promise<string | undefined>((resolve, reject) => {1079const inputBox = disposables.add(this._quickInputService.createInputBox());1080inputBox.title = `${groupName}: ${propertySchema.title ?? property}`;1081inputBox.placeholder = localize('enterValue', "Enter value for {0}", property);1082inputBox.password = !!propertySchema.secret;1083inputBox.ignoreFocusOut = true;1084if (existing?.[property]) {1085inputBox.value = String(existing?.[property]);1086} else if (propertySchema.default) {1087inputBox.value = String(propertySchema.default);1088}1089if (propertySchema.description) {1090inputBox.prompt = propertySchema.description;1091}10921093disposables.add(inputBox.onDidChangeValue(value => {1094if (!value && required) {1095inputBox.validationMessage = localize('valueRequired', "Value is required");1096inputBox.severity = Severity.Error;1097return;1098}1099if (propertySchema.type === 'number' || propertySchema.type === 'integer') {1100if (isNaN(Number(value))) {1101inputBox.validationMessage = localize('numberRequired', "Please enter a number");1102inputBox.severity = Severity.Error;1103return;1104}1105}1106if (propertySchema.type === 'boolean') {1107if (value !== 'true' && value !== 'false') {1108inputBox.validationMessage = localize('booleanRequired', "Please enter true or false");1109inputBox.severity = Severity.Error;1110return;1111}1112}1113inputBox.validationMessage = undefined;1114inputBox.severity = Severity.Ignore;1115}));11161117disposables.add(inputBox.onDidAccept(() => {1118if (!inputBox.value && required) {1119inputBox.validationMessage = localize('valueRequired', "Value is required");1120inputBox.severity = Severity.Error;1121return;1122}1123resolve(inputBox.value);1124inputBox.hide();1125}));11261127disposables.add(inputBox.onDidHide((e) => {1128if (e.reason === QuickInputHideReason.Gesture) {1129reject(new CancellationError());1130} else {1131resolve(undefined);1132}1133}));11341135inputBox.show();1136});11371138if (!value) {1139return undefined; // User cancelled1140}11411142if (propertySchema.type === 'number' || propertySchema.type === 'integer') {1143return Number(value);1144} else if (propertySchema.type === 'boolean') {1145return value === 'true';1146} else {1147return value;1148}11491150} finally {1151disposables.dispose();1152}1153}11541155private encodeSecretKey(property: string): string {1156return format(LanguageModelsService.SECRET_INPUT, property);1157}11581159private decodeSecretKey(secretInput: unknown): string | undefined {1160if (!isString(secretInput)) {1161return undefined;1162}1163return secretInput.substring(secretInput.indexOf(':') + 1, secretInput.length - 1);1164}11651166private _clearModelCache(vendor: string): Map<string, ILanguageModelChatMetadata> {1167const removed = new Map<string, ILanguageModelChatMetadata>();1168for (const [id, model] of this._modelCache.entries()) {1169if (model.vendor === vendor) {1170removed.set(id, model);1171this._modelCache.delete(id);1172}1173}1174return removed;1175}11761177private async _resolveConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise<IStringDictionary<unknown>> {1178if (!schema) {1179return {};1180}11811182const result: IStringDictionary<unknown> = {};1183for (const key in group) {1184if (key === 'vendor' || key === 'name' || key === 'range') {1185continue;1186}1187let value = group[key];1188if (schema.properties?.[key]?.secret) {1189const secretKey = this.decodeSecretKey(value);1190value = secretKey ? await this._secretStorageService.get(secretKey) : undefined;1191}1192result[key] = value;1193}11941195return result;1196}11971198private async _resolveLanguageModelProviderGroup(name: string, vendor: string, configuration: IStringDictionary<unknown> | undefined, schema: IJSONSchema | undefined): Promise<ILanguageModelsProviderGroup> {1199if (!schema) {1200return { name, vendor };1201}12021203const result: IStringDictionary<unknown> = {};1204for (const key in configuration) {1205let value = configuration[key];1206if (schema.properties?.[key]?.secret && isString(value)) {1207const secretKey = `${LanguageModelsService.SECRET_KEY_PREFIX}${hash(generateUuid()).toString(16)}`;1208await this._secretStorageService.set(secretKey, value);1209value = this.encodeSecretKey(secretKey);1210}1211result[key] = value;1212}12131214return { name, vendor, ...result };1215}12161217private async _deleteSecretsInConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise<void> {1218if (!schema) {1219return;1220}12211222const { vendor, name, range, ...configuration } = group;1223for (const key in configuration) {1224const value = group[key];1225if (schema.properties?.[key]?.secret) {1226const secretKey = this.decodeSecretKey(value);1227if (secretKey) {1228await this._secretStorageService.delete(secretKey);1229}1230}1231}1232}12331234async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise<void> {1235const { vendor, name, ...configuration } = languageModelsProviderGroup;1236if (!this._vendors.get(vendor)) {1237throw new Error(`Vendor ${vendor} not found.`);1238}12391240await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendor}`);1241const provider = this._providers.get(vendor);1242if (!provider) {1243throw new Error(`Chat model provider for vendor ${vendor} is not registered.`);1244}12451246const models = await provider.provideLanguageModelChatInfo({ group: name, silent: false, configuration }, CancellationToken.None);1247for (const model of models) {1248const oldIdentifier = `${vendor}/${model.metadata.id}`;1249if (this._modelPickerUserPreferences[oldIdentifier] === true) {1250this._modelPickerUserPreferences[model.identifier] = true;1251}1252delete this._modelPickerUserPreferences[oldIdentifier];1253}1254this._saveModelPickerPreferences();12551256await this.addLanguageModelsProviderGroup(name, vendor, configuration);1257}12581259dispose() {1260this._store.dispose();1261this._providers.clear();1262}12631264}126512661267