Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/languageModels.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { VSBuffer } from '../../../../base/common/buffer.js';
7
import { CancellationToken } from '../../../../base/common/cancellation.js';
8
import { Emitter, Event } from '../../../../base/common/event.js';
9
import { Iterable } from '../../../../base/common/iterator.js';
10
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
11
import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
12
import { isFalsyOrWhitespace } from '../../../../base/common/strings.js';
13
import { ThemeIcon } from '../../../../base/common/themables.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import { localize } from '../../../../nls.js';
16
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
17
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
18
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
19
import { ILogService } from '../../../../platform/log/common/log.js';
20
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
21
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
22
import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';
23
import { ChatContextKeys } from './chatContextKeys.js';
24
25
export const enum ChatMessageRole {
26
System,
27
User,
28
Assistant,
29
}
30
31
export enum LanguageModelPartAudience {
32
Assistant = 0,
33
User = 1,
34
Extension = 2,
35
}
36
37
export interface IChatMessageTextPart {
38
type: 'text';
39
value: string;
40
audience?: LanguageModelPartAudience[];
41
}
42
43
export interface IChatMessageImagePart {
44
type: 'image_url';
45
value: IChatImageURLPart;
46
}
47
48
export interface IChatMessageThinkingPart {
49
type: 'thinking';
50
value: string | string[];
51
id?: string;
52
metadata?: { readonly [key: string]: any };
53
}
54
55
export interface IChatMessageDataPart {
56
type: 'data';
57
mimeType: string;
58
data: VSBuffer;
59
audience?: LanguageModelPartAudience[];
60
}
61
62
export interface IChatImageURLPart {
63
/**
64
* The image's MIME type (e.g., "image/png", "image/jpeg").
65
*/
66
mimeType: ChatImageMimeType;
67
68
/**
69
* The raw binary data of the image, encoded as a Uint8Array. Note: do not use base64 encoding. Maximum image size is 5MB.
70
*/
71
data: VSBuffer;
72
}
73
74
/**
75
* Enum for supported image MIME types.
76
*/
77
export enum ChatImageMimeType {
78
PNG = 'image/png',
79
JPEG = 'image/jpeg',
80
GIF = 'image/gif',
81
WEBP = 'image/webp',
82
BMP = 'image/bmp',
83
}
84
85
/**
86
* Specifies the detail level of the image.
87
*/
88
export enum ImageDetailLevel {
89
Low = 'low',
90
High = 'high'
91
}
92
93
94
export interface IChatMessageToolResultPart {
95
type: 'tool_result';
96
toolCallId: string;
97
value: (IChatResponseTextPart | IChatResponsePromptTsxPart | IChatResponseDataPart)[];
98
isError?: boolean;
99
}
100
101
export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart | IChatMessageDataPart | IChatMessageThinkingPart;
102
103
export interface IChatMessage {
104
readonly name?: string | undefined;
105
readonly role: ChatMessageRole;
106
readonly content: IChatMessagePart[];
107
}
108
109
export interface IChatResponseTextPart {
110
type: 'text';
111
value: string;
112
audience?: LanguageModelPartAudience[];
113
}
114
115
export interface IChatResponsePromptTsxPart {
116
type: 'prompt_tsx';
117
value: unknown;
118
}
119
120
export interface IChatResponseDataPart {
121
type: 'data';
122
mimeType: string;
123
data: VSBuffer;
124
audience?: LanguageModelPartAudience[];
125
}
126
127
export interface IChatResponseToolUsePart {
128
type: 'tool_use';
129
name: string;
130
toolCallId: string;
131
parameters: any;
132
}
133
134
export interface IChatResponseThinkingPart {
135
type: 'thinking';
136
value: string | string[];
137
id?: string;
138
metadata?: { readonly [key: string]: any };
139
}
140
141
export interface IChatResponsePullRequestPart {
142
type: 'pullRequest';
143
uri: URI;
144
title: string;
145
description: string;
146
author: string;
147
linkTag: string;
148
}
149
150
export type IChatResponsePart = IChatResponseTextPart | IChatResponseToolUsePart | IChatResponseDataPart | IChatResponseThinkingPart;
151
152
export type IExtendedChatResponsePart = IChatResponsePullRequestPart;
153
154
export interface ILanguageModelChatMetadata {
155
readonly extension: ExtensionIdentifier;
156
157
readonly name: string;
158
readonly id: string;
159
readonly vendor: string;
160
readonly version: string;
161
readonly tooltip?: string;
162
readonly detail?: string;
163
readonly family: string;
164
readonly maxInputTokens: number;
165
readonly maxOutputTokens: number;
166
167
readonly isDefault?: boolean;
168
readonly isUserSelectable?: boolean;
169
readonly statusIcon?: ThemeIcon;
170
readonly modelPickerCategory: { label: string; order: number } | undefined;
171
readonly auth?: {
172
readonly providerLabel: string;
173
readonly accountLabel?: string;
174
};
175
readonly capabilities?: {
176
readonly vision?: boolean;
177
readonly toolCalling?: boolean;
178
readonly agentMode?: boolean;
179
};
180
}
181
182
export namespace ILanguageModelChatMetadata {
183
export function suitableForAgentMode(metadata: ILanguageModelChatMetadata): boolean {
184
const supportsToolsAgent = typeof metadata.capabilities?.agentMode === 'undefined' || metadata.capabilities.agentMode;
185
return supportsToolsAgent && !!metadata.capabilities?.toolCalling;
186
}
187
188
export function asQualifiedName(metadata: ILanguageModelChatMetadata): string {
189
return `${metadata.name} (${metadata.vendor})`;
190
}
191
192
export function matchesQualifiedName(name: string, metadata: ILanguageModelChatMetadata): boolean {
193
if (metadata.vendor === 'copilot' && name === metadata.name) {
194
return true;
195
}
196
return name === asQualifiedName(metadata);
197
}
198
}
199
200
export interface ILanguageModelChatResponse {
201
stream: AsyncIterable<IChatResponsePart | IChatResponsePart[]>;
202
result: Promise<any>;
203
}
204
205
export interface ILanguageModelChatProvider {
206
onDidChange: Event<void>;
207
provideLanguageModelChatInfo(options: { silent: boolean }, token: CancellationToken): Promise<ILanguageModelChatMetadataAndIdentifier[]>;
208
sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse>;
209
provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;
210
}
211
212
export interface ILanguageModelChat {
213
metadata: ILanguageModelChatMetadata;
214
sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse>;
215
provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise<number>;
216
}
217
218
export interface ILanguageModelChatSelector {
219
readonly name?: string;
220
readonly id?: string;
221
readonly vendor?: string;
222
readonly version?: string;
223
readonly family?: string;
224
readonly tokens?: number;
225
readonly extension?: ExtensionIdentifier;
226
}
227
228
export const ILanguageModelsService = createDecorator<ILanguageModelsService>('ILanguageModelsService');
229
230
export interface ILanguageModelChatMetadataAndIdentifier {
231
metadata: ILanguageModelChatMetadata;
232
identifier: string;
233
}
234
235
export interface ILanguageModelsService {
236
237
readonly _serviceBrand: undefined;
238
239
// TODO @lramos15 - Make this a richer event in the future. Right now it just indicates some change happened, but not what
240
onDidChangeLanguageModels: Event<void>;
241
242
updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void;
243
244
getLanguageModelIds(): string[];
245
246
getVendors(): IUserFriendlyLanguageModel[];
247
248
lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined;
249
250
/**
251
* Given a selector, returns a list of model identifiers
252
* @param selector The selector to lookup for language models. If the selector is empty, all language models are returned.
253
* @param allowPromptingUser If true the user may be prompted for things like API keys for us to select the model.
254
*/
255
selectLanguageModels(selector: ILanguageModelChatSelector, allowPromptingUser?: boolean): Promise<string[]>;
256
257
registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable;
258
259
sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse>;
260
261
computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;
262
}
263
264
const languageModelChatProviderType: IJSONSchema = {
265
type: 'object',
266
properties: {
267
vendor: {
268
type: 'string',
269
description: localize('vscode.extension.contributes.languageModels.vendor', "A globally unique vendor of language model chat provider.")
270
},
271
displayName: {
272
type: 'string',
273
description: localize('vscode.extension.contributes.languageModels.displayName', "The display name of the language model chat provider.")
274
},
275
managementCommand: {
276
type: 'string',
277
description: 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.")
278
}
279
}
280
};
281
282
export interface IUserFriendlyLanguageModel {
283
vendor: string;
284
displayName: string;
285
managementCommand?: string;
286
}
287
288
export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint<IUserFriendlyLanguageModel | IUserFriendlyLanguageModel[]>({
289
extensionPoint: 'languageModelChatProviders',
290
jsonSchema: {
291
description: localize('vscode.extension.contributes.languageModelChatProviders', "Contribute language model chat providers of a specific vendor."),
292
oneOf: [
293
languageModelChatProviderType,
294
{
295
type: 'array',
296
items: languageModelChatProviderType
297
}
298
]
299
},
300
activationEventsGenerator: (contribs: IUserFriendlyLanguageModel[], result: { push(item: string): void }) => {
301
for (const contrib of contribs) {
302
result.push(`onLanguageModelChatProvider:${contrib.vendor}`);
303
}
304
}
305
});
306
307
export class LanguageModelsService implements ILanguageModelsService {
308
309
readonly _serviceBrand: undefined;
310
311
private readonly _store = new DisposableStore();
312
313
private readonly _providers = new Map<string, ILanguageModelChatProvider>();
314
private readonly _modelCache = new Map<string, ILanguageModelChatMetadata>();
315
private readonly _vendors = new Map<string, IUserFriendlyLanguageModel>();
316
private readonly _modelPickerUserPreferences: Record<string, boolean> = {}; // We use a record instead of a map for better serialization when storing
317
318
private readonly _hasUserSelectableModels: IContextKey<boolean>;
319
private readonly _onLanguageModelChange = this._store.add(new Emitter<void>());
320
readonly onDidChangeLanguageModels: Event<void> = this._onLanguageModelChange.event;
321
322
constructor(
323
@IExtensionService private readonly _extensionService: IExtensionService,
324
@ILogService private readonly _logService: ILogService,
325
@IStorageService private readonly _storageService: IStorageService,
326
@IContextKeyService _contextKeyService: IContextKeyService
327
) {
328
this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService);
329
this._modelPickerUserPreferences = this._storageService.getObject<Record<string, boolean>>('chatModelPickerPreferences', StorageScope.PROFILE, this._modelPickerUserPreferences);
330
331
332
333
this._store.add(this.onDidChangeLanguageModels(() => {
334
this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable));
335
}));
336
337
this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions) => {
338
339
this._vendors.clear();
340
341
for (const extension of extensions) {
342
for (const item of Iterable.wrap(extension.value)) {
343
if (this._vendors.has(item.vendor)) {
344
extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor));
345
continue;
346
}
347
if (isFalsyOrWhitespace(item.vendor)) {
348
extension.collector.error(localize('vscode.extension.contributes.languageModels.emptyVendor', "The vendor field cannot be empty."));
349
continue;
350
}
351
if (item.vendor.trim() !== item.vendor) {
352
extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace."));
353
continue;
354
}
355
this._vendors.set(item.vendor, item);
356
// Have some models we want from this vendor, so activate the extension
357
if (this._hasStoredModelForvendor(item.vendor)) {
358
this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`);
359
}
360
}
361
}
362
for (const [vendor, _] of this._providers) {
363
if (!this._vendors.has(vendor)) {
364
this._providers.delete(vendor);
365
}
366
}
367
}));
368
}
369
370
private _hasStoredModelForvendor(vendor: string): boolean {
371
return Object.keys(this._modelPickerUserPreferences).some(modelId => {
372
return modelId.startsWith(vendor);
373
});
374
}
375
376
dispose() {
377
this._store.dispose();
378
this._providers.clear();
379
}
380
381
updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void {
382
const model = this._modelCache.get(modelIdentifier);
383
if (!model) {
384
this._logService.warn(`[LM] Cannot update model picker preference for unknown model ${modelIdentifier}`);
385
return;
386
}
387
388
this._modelPickerUserPreferences[modelIdentifier] = showInModelPicker;
389
if (showInModelPicker === model.isUserSelectable) {
390
delete this._modelPickerUserPreferences[modelIdentifier];
391
this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER);
392
} else if (model.isUserSelectable !== showInModelPicker) {
393
this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER);
394
}
395
this._onLanguageModelChange.fire();
396
this._logService.trace(`[LM] Updated model picker preference for ${modelIdentifier} to ${showInModelPicker}`);
397
}
398
399
getVendors(): IUserFriendlyLanguageModel[] {
400
return Array.from(this._vendors.values());
401
}
402
403
getLanguageModelIds(): string[] {
404
return Array.from(this._modelCache.keys());
405
}
406
407
lookupLanguageModel(modelIdentifier: string): ILanguageModelChatMetadata | undefined {
408
const model = this._modelCache.get(modelIdentifier);
409
if (model && this._modelPickerUserPreferences[modelIdentifier] !== undefined) {
410
return { ...model, isUserSelectable: this._modelPickerUserPreferences[modelIdentifier] };
411
}
412
return model;
413
}
414
415
private _clearModelCache(vendors: string | string[]): void {
416
if (typeof vendors === 'string') {
417
vendors = [vendors];
418
}
419
for (const vendor of vendors) {
420
for (const [id, model] of this._modelCache.entries()) {
421
if (model.vendor === vendor) {
422
this._modelCache.delete(id);
423
}
424
}
425
}
426
}
427
428
async resolveLanguageModels(vendors: string | string[], silent: boolean): Promise<void> {
429
if (typeof vendors === 'string') {
430
vendors = [vendors];
431
}
432
// Activate extensions before requesting to resolve the models
433
const all = vendors.map(vendor => this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendor}`));
434
await Promise.all(all);
435
this._clearModelCache(vendors);
436
for (const vendor of vendors) {
437
const provider = this._providers.get(vendor);
438
if (!provider) {
439
this._logService.warn(`[LM] No provider registered for vendor ${vendor}`);
440
continue;
441
}
442
try {
443
let modelsAndIdentifiers = await provider.provideLanguageModelChatInfo({ silent }, CancellationToken.None);
444
// 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 list
445
if (!silent && modelsAndIdentifiers.some(m => m.metadata.isUserSelectable)) {
446
modelsAndIdentifiers = modelsAndIdentifiers.filter(m => m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true);
447
}
448
for (const modelAndIdentifier of modelsAndIdentifiers) {
449
if (this._modelCache.has(modelAndIdentifier.identifier)) {
450
this._logService.warn(`[LM] Model ${modelAndIdentifier.identifier} is already registered. Skipping.`);
451
continue;
452
}
453
this._modelCache.set(modelAndIdentifier.identifier, modelAndIdentifier.metadata);
454
}
455
this._logService.trace(`[LM] Resolved language models for vendor ${vendor}`, modelsAndIdentifiers);
456
} catch (error) {
457
this._logService.error(`[LM] Error resolving language models for vendor ${vendor}:`, error);
458
}
459
}
460
this._onLanguageModelChange.fire();
461
}
462
463
async selectLanguageModels(selector: ILanguageModelChatSelector, allowPromptingUser?: boolean): Promise<string[]> {
464
465
if (selector.vendor) {
466
await this.resolveLanguageModels([selector.vendor], !allowPromptingUser);
467
} else {
468
const allVendors = Array.from(this._vendors.keys());
469
await this.resolveLanguageModels(allVendors, !allowPromptingUser);
470
}
471
472
const result: string[] = [];
473
474
for (const [internalModelIdentifier, model] of this._modelCache) {
475
if ((selector.vendor === undefined || model.vendor === selector.vendor)
476
&& (selector.family === undefined || model.family === selector.family)
477
&& (selector.version === undefined || model.version === selector.version)
478
&& (selector.id === undefined || model.id === selector.id)) {
479
result.push(internalModelIdentifier);
480
}
481
}
482
483
this._logService.trace('[LM] selected language models', selector, result);
484
485
return result;
486
}
487
488
registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable {
489
this._logService.trace('[LM] registering language model provider', vendor, provider);
490
491
if (!this._vendors.has(vendor)) {
492
throw new Error(`Chat model provider uses UNKNOWN vendor ${vendor}.`);
493
}
494
if (this._providers.has(vendor)) {
495
throw new Error(`Chat model provider for vendor ${vendor} is already registered.`);
496
}
497
498
this._providers.set(vendor, provider);
499
500
// TODO @lramos15 - Smarter restore logic. Don't resolve models for all providers, but only those which were known to need restoring
501
this.resolveLanguageModels(vendor, true).then(() => {
502
this._onLanguageModelChange.fire();
503
});
504
505
return toDisposable(() => {
506
this._logService.trace('[LM] UNregistered language model provider', vendor);
507
this._clearModelCache(vendor);
508
this._providers.delete(vendor);
509
});
510
}
511
512
async sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse> {
513
const provider = this._providers.get(this._modelCache.get(modelId)?.vendor || '');
514
if (!provider) {
515
throw new Error(`Chat provider for model ${modelId} is not registered.`);
516
}
517
return provider.sendChatRequest(modelId, messages, from, options, token);
518
}
519
520
computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number> {
521
const model = this._modelCache.get(modelId);
522
if (!model) {
523
throw new Error(`Chat model ${modelId} could not be found.`);
524
}
525
const provider = this._providers.get(model.vendor);
526
if (!provider) {
527
throw new Error(`Chat provider for model ${modelId} is not registered.`);
528
}
529
return provider.provideTokenCount(modelId, message, token);
530
}
531
}
532
533