Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts
4780 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 { Emitter, Event } from '../../../../base/common/event.js';
8
import { Disposable } from '../../../../base/common/lifecycle.js';
9
import { Mutable } from '../../../../base/common/types.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { IFileService } from '../../../../platform/files/common/files.js';
12
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
13
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
14
import { ITextEditorService } from '../../../services/textfile/common/textEditorService.js';
15
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
16
import { equals } from '../../../../base/common/objects.js';
17
import { IRange } from '../../../../editor/common/core/range.js';
18
import { JSONVisitor, visit } from '../../../../base/common/json.js';
19
import { ITextModel } from '../../../../editor/common/model.js';
20
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
21
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
22
import { getCodeEditor } from '../../../../editor/browser/editorBrowser.js';
23
import { ILanguageModelsConfigurationService, ILanguageModelsProviderGroup } from '../common/languageModelsConfiguration.js';
24
import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';
25
import { Registry } from '../../../../platform/registry/common/platform.js';
26
import { IWorkbenchContribution } from '../../../common/contributions.js';
27
import { ILanguageModelsService } from '../common/languageModels.js';
28
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
29
30
type LanguageModelsProviderGroups = Mutable<ILanguageModelsProviderGroup>[];
31
32
export class LanguageModelsConfigurationService extends Disposable implements ILanguageModelsConfigurationService {
33
34
declare _serviceBrand: undefined;
35
36
private readonly modelsConfigurationFile: URI;
37
38
private readonly _onDidChangeLanguageModelGroups = new Emitter<void>();
39
readonly onDidChangeLanguageModelGroups: Event<void> = this._onDidChangeLanguageModelGroups.event;
40
41
private languageModelsProviderGroups: LanguageModelsProviderGroups = [];
42
43
constructor(
44
@IFileService private readonly fileService: IFileService,
45
@ITextFileService private readonly textFileService: ITextFileService,
46
@ITextModelService private readonly textModelService: ITextModelService,
47
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
48
@ITextEditorService private readonly textEditorService: ITextEditorService,
49
@IUserDataProfileService userDataProfileService: IUserDataProfileService,
50
@IUriIdentityService uriIdentityService: IUriIdentityService,
51
) {
52
super();
53
this.modelsConfigurationFile = uriIdentityService.extUri.joinPath(userDataProfileService.currentProfile.location, 'models.json');
54
this.updateLanguageModelsConfiguration();
55
this._register(fileService.watch(this.modelsConfigurationFile));
56
this._register(fileService.onDidFilesChange(e => {
57
if (e.contains(this.modelsConfigurationFile)) {
58
this.updateLanguageModelsConfiguration();
59
}
60
}));
61
}
62
63
private setLanguageModelsConfiguration(languageModelsConfiguration: LanguageModelsProviderGroups): void {
64
if (equals(this.languageModelsProviderGroups, languageModelsConfiguration)) {
65
return;
66
}
67
this.languageModelsProviderGroups = languageModelsConfiguration;
68
this._onDidChangeLanguageModelGroups.fire();
69
}
70
71
private async updateLanguageModelsConfiguration(): Promise<void> {
72
const languageModelsProviderGroups = await this.withLanguageModelsProviderGroups();
73
this.setLanguageModelsConfiguration(languageModelsProviderGroups);
74
}
75
76
getLanguageModelsProviderGroups(): readonly ILanguageModelsProviderGroup[] {
77
return this.languageModelsProviderGroups;
78
}
79
80
async addLanguageModelsProviderGroup(toAdd: ILanguageModelsProviderGroup): Promise<ILanguageModelsProviderGroup> {
81
await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {
82
if (languageModelsProviderGroups.some(({ name, vendor }) => name === toAdd.name && vendor === toAdd.vendor)) {
83
throw new Error(`Language model group with name ${toAdd.name} already exists for vendor ${toAdd.vendor}`);
84
}
85
languageModelsProviderGroups.push(toAdd);
86
return languageModelsProviderGroups;
87
});
88
89
await this.updateLanguageModelsConfiguration();
90
const result = this.getLanguageModelsProviderGroups().find(group => group.name === toAdd.name && group.vendor === toAdd.vendor);
91
if (!result) {
92
throw new Error(`Language model group with name ${toAdd.name} not found for vendor ${toAdd.vendor}`);
93
}
94
return result;
95
}
96
97
async updateLanguageModelsProviderGroup(toUpdate: ILanguageModelsProviderGroup): Promise<ILanguageModelsProviderGroup> {
98
await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {
99
const result: LanguageModelsProviderGroups = [];
100
for (const group of languageModelsProviderGroups) {
101
if (group.name === toUpdate.name && group.vendor === toUpdate.vendor) {
102
result.push(toUpdate);
103
} else {
104
result.push(group);
105
}
106
}
107
return result;
108
});
109
110
await this.updateLanguageModelsConfiguration();
111
const result = this.getLanguageModelsProviderGroups().find(group => group.name === toUpdate.name && group.vendor === toUpdate.vendor);
112
if (!result) {
113
throw new Error(`Language model group with name ${toUpdate.name} not found for vendor ${toUpdate.vendor}`);
114
}
115
return result;
116
}
117
118
async removeLanguageModelsProviderGroup(toRemove: ILanguageModelsProviderGroup): Promise<void> {
119
await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {
120
const result: LanguageModelsProviderGroups = [];
121
for (const group of languageModelsProviderGroups) {
122
if (group.name === toRemove.name && group.vendor === toRemove.vendor) {
123
continue;
124
}
125
result.push(group);
126
}
127
return result;
128
});
129
await this.updateLanguageModelsConfiguration();
130
}
131
132
async configureLanguageModels(range?: IRange): Promise<void> {
133
const editor = await this.editorGroupsService.activeGroup.openEditor(this.textEditorService.createTextEditor({ resource: this.modelsConfigurationFile }));
134
if (!editor || !range) {
135
return;
136
}
137
138
const codeEditor = getCodeEditor(editor.getControl());
139
if (!codeEditor) {
140
return;
141
}
142
143
const position = { lineNumber: range.startLineNumber, column: range.startColumn };
144
codeEditor.setPosition(position);
145
codeEditor.revealPositionNearTop(position);
146
codeEditor.focus();
147
}
148
149
private async withLanguageModelsProviderGroups(update?: (languageModelsProviderGroups: LanguageModelsProviderGroups) => Promise<LanguageModelsProviderGroups>): Promise<LanguageModelsProviderGroups> {
150
const exists = await this.fileService.exists(this.modelsConfigurationFile);
151
if (!exists) {
152
await this.fileService.writeFile(this.modelsConfigurationFile, VSBuffer.fromString(JSON.stringify([], undefined, '\t')));
153
}
154
const ref = await this.textModelService.createModelReference(this.modelsConfigurationFile);
155
const model = ref.object.textEditorModel;
156
try {
157
const languageModelsProviderGroups = parseLanguageModelsProviderGroups(model);
158
if (!update) {
159
return languageModelsProviderGroups;
160
}
161
const updatedLanguageModelsProviderGroups = await update(languageModelsProviderGroups);
162
for (const group of updatedLanguageModelsProviderGroups) {
163
delete group.range;
164
}
165
model.setValue(JSON.stringify(updatedLanguageModelsProviderGroups, undefined, '\t'));
166
await this.textFileService.save(this.modelsConfigurationFile);
167
return updatedLanguageModelsProviderGroups;
168
} finally {
169
ref.dispose();
170
}
171
}
172
}
173
174
export function parseLanguageModelsProviderGroups(model: ITextModel): LanguageModelsProviderGroups {
175
const configuration: LanguageModelsProviderGroups = [];
176
let currentProperty: string | null = null;
177
let currentParent: unknown = configuration;
178
const previousParents: unknown[] = [];
179
180
function onValue(value: unknown, offset: number, length: number) {
181
if (Array.isArray(currentParent)) {
182
(currentParent as unknown[]).push(value);
183
} else if (currentProperty !== null) {
184
(currentParent as Record<string, unknown>)[currentProperty] = value;
185
if (currentProperty === 'configuration') {
186
const start = model.getPositionAt(offset);
187
const range: Mutable<IRange> = {
188
startLineNumber: start.lineNumber,
189
startColumn: start.column,
190
endLineNumber: start.lineNumber,
191
endColumn: start.column
192
};
193
if (value && typeof value === 'object') {
194
(value as { _parentConfigurationRange?: Mutable<IRange> })._parentConfigurationRange = range;
195
} else {
196
const end = model.getPositionAt(offset + length);
197
range.endLineNumber = end.lineNumber;
198
range.endColumn = end.column;
199
}
200
(currentParent as { configurationRange?: IRange }).configurationRange = range;
201
}
202
}
203
}
204
205
const visitor: JSONVisitor = {
206
onObjectBegin: (offset: number, length: number) => {
207
const object: Record<string, unknown> & { range?: IRange } = {};
208
if (Array.isArray(currentParent)) {
209
const start = model.getPositionAt(offset);
210
const end = model.getPositionAt(offset + length);
211
object.range = {
212
startLineNumber: start.lineNumber,
213
startColumn: start.column,
214
endLineNumber: end.lineNumber,
215
endColumn: end.column
216
};
217
}
218
onValue(object, offset, length);
219
previousParents.push(currentParent);
220
currentParent = object;
221
currentProperty = null;
222
},
223
onObjectProperty: (name: string, offset: number, length: number) => {
224
currentProperty = name;
225
},
226
onObjectEnd: (offset: number, length: number) => {
227
const parent = currentParent as Record<string, unknown> & { range?: IRange; _parentConfigurationRange?: Mutable<IRange> };
228
if (parent.range) {
229
const end = model.getPositionAt(offset + length);
230
parent.range = {
231
startLineNumber: parent.range.startLineNumber,
232
startColumn: parent.range.startColumn,
233
endLineNumber: end.lineNumber,
234
endColumn: end.column
235
};
236
}
237
if (parent._parentConfigurationRange) {
238
const end = model.getPositionAt(offset + length);
239
parent._parentConfigurationRange.endLineNumber = end.lineNumber;
240
parent._parentConfigurationRange.endColumn = end.column;
241
delete parent._parentConfigurationRange;
242
}
243
currentParent = previousParents.pop();
244
},
245
onArrayBegin: (offset: number, length: number) => {
246
if (currentParent === configuration && previousParents.length === 0) {
247
previousParents.push(currentParent);
248
currentProperty = null;
249
return;
250
}
251
const array: unknown[] = [];
252
onValue(array, offset, length);
253
previousParents.push(currentParent);
254
currentParent = array;
255
currentProperty = null;
256
},
257
onArrayEnd: (offset: number, length: number) => {
258
const parent = currentParent as { _parentConfigurationRange?: Mutable<IRange> };
259
if (parent._parentConfigurationRange) {
260
const end = model.getPositionAt(offset + length);
261
parent._parentConfigurationRange.endLineNumber = end.lineNumber;
262
parent._parentConfigurationRange.endColumn = end.column;
263
delete parent._parentConfigurationRange;
264
}
265
currentParent = previousParents.pop();
266
},
267
onLiteralValue: (value: unknown, offset: number, length: number) => {
268
onValue(value, offset, length);
269
},
270
};
271
visit(model.getValue(), visitor);
272
return configuration;
273
}
274
275
const languageModelsSchemaId = 'vscode://schemas/language-models';
276
277
export class ChatLanguageModelsDataContribution extends Disposable implements IWorkbenchContribution {
278
279
static readonly ID = 'workbench.contrib.chatLanguageModelsData';
280
281
constructor(
282
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
283
@IUserDataProfileService userDataProfileService: IUserDataProfileService,
284
@IUriIdentityService uriIdentityService: IUriIdentityService,
285
) {
286
super();
287
const modelsConfigurationFile = uriIdentityService.extUri.joinPath(userDataProfileService.currentProfile.location, 'models.json');
288
const registry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
289
this._register(registry.registerSchemaAssociation(languageModelsSchemaId, modelsConfigurationFile.toString()));
290
291
this.updateSchema(registry);
292
this._register(this.languageModelsService.onDidChangeLanguageModels(() => this.updateSchema(registry)));
293
}
294
295
private updateSchema(registry: IJSONContributionRegistry): void {
296
const vendors = this.languageModelsService.getVendors();
297
298
const schema: IJSONSchema = {
299
type: 'array',
300
items: {
301
properties: {
302
vendor: {
303
type: 'string',
304
enum: vendors.map(v => v.vendor)
305
},
306
name: { type: 'string' }
307
},
308
allOf: vendors.map(vendor => ({
309
if: {
310
properties: {
311
vendor: { const: vendor.vendor }
312
}
313
},
314
then: vendor.configuration
315
})),
316
required: ['vendor', 'name']
317
}
318
};
319
320
registry.registerSchema(languageModelsSchemaId, schema);
321
}
322
}
323
324