Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts
13401 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 type * as vscode from 'vscode';
7
import { filterMap } from '../../../util/common/arrays';
8
import { TaskQueue } from '../../../util/common/async';
9
import { ErrorUtils } from '../../../util/common/errors';
10
import { pushMany } from '../../../util/vs/base/common/arrays';
11
import { assertNever, softAssert } from '../../../util/vs/base/common/assert';
12
import { Emitter, Event } from '../../../util/vs/base/common/event';
13
import { Disposable } from '../../../util/vs/base/common/lifecycle';
14
import { derived, IObservable, observableFromEvent } from '../../../util/vs/base/common/observable';
15
import { CopilotToken } from '../../authentication/common/copilotToken';
16
import { ICopilotTokenStore } from '../../authentication/common/copilotTokenStore';
17
import { ConfigKey, ExperimentBasedConfig, IConfigurationService } from '../../configuration/common/configurationService';
18
import { IVSCodeExtensionContext } from '../../extContext/common/extensionContext';
19
import { ILogger, ILogService } from '../../log/common/logService';
20
import { IProxyModelsService } from '../../proxyModels/common/proxyModelsService';
21
import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';
22
import { ITelemetryService } from '../../telemetry/common/telemetry';
23
import { WireTypes } from '../common/dataTypes/inlineEditsModelsTypes';
24
import { isPromptingStrategy, MODEL_CONFIGURATION_VALIDATOR, ModelConfiguration, PromptingStrategy } from '../common/dataTypes/xtabPromptOptions';
25
import { IInlineEditsModelService, IUndesiredModelsManager } from '../common/inlineEditsModelService';
26
27
const enum ModelSource {
28
LocalConfig = 'localConfig',
29
ExpConfig = 'expConfig',
30
ExpDefaultConfig = 'expDefaultConfig',
31
Fetched = 'fetched',
32
HardCodedDefault = 'hardCodedDefault',
33
}
34
35
interface ModelConfigurationWithSource extends ModelConfiguration {
36
source: ModelSource;
37
}
38
39
type ModelInfo = {
40
models: ModelConfigurationWithSource[];
41
currentModelId: string;
42
};
43
44
export class InlineEditsModelService extends Disposable implements IInlineEditsModelService {
45
46
_serviceBrand: undefined;
47
48
private static readonly COPILOT_NES_XTAB_MODEL: ModelConfigurationWithSource = {
49
modelName: 'copilot-nes-xtab',
50
promptingStrategy: PromptingStrategy.CopilotNesXtab,
51
includeTagsInCurrentFile: true,
52
source: ModelSource.HardCodedDefault,
53
lintOptions: undefined,
54
};
55
56
private static readonly COPILOT_NES_OCT: ModelConfigurationWithSource = {
57
modelName: 'copilot-nes-oct',
58
promptingStrategy: PromptingStrategy.Xtab275,
59
includeTagsInCurrentFile: false,
60
source: ModelSource.HardCodedDefault,
61
lintOptions: undefined,
62
};
63
64
private static readonly COPILOT_NES_CALLISTO: ModelConfigurationWithSource = {
65
modelName: 'nes-callisto',
66
promptingStrategy: PromptingStrategy.Xtab275,
67
includeTagsInCurrentFile: false,
68
source: ModelSource.HardCodedDefault,
69
lintOptions: undefined,
70
};
71
72
private _copilotTokenObs = observableFromEvent(this, this._tokenStore.onDidStoreUpdate, () => this._tokenStore.copilotToken);
73
74
// TODO@ulugbekna: use a derived observable such that it fires only when nesModels change
75
private _fetchedModelsObs = observableFromEvent(this, this._proxyModelsService.onModelListUpdated, () => this._proxyModelsService.nesModels);
76
77
private _preferredModelNameObs = this._configService.getExperimentBasedConfigObservable(ConfigKey.Advanced.InlineEditsPreferredModel, this._expService);
78
private _localModelConfigObs = this._configService.getConfigObservable(ConfigKey.Advanced.InlineEditsXtabProviderModelConfiguration);
79
private _expBasedModelConfigObs = this._configService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsXtabProviderModelConfigurationString, this._expService);
80
private _defaultModelConfigObs = this._configService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsXtabProviderDefaultModelConfigurationString, this._expService);
81
private _useSlashModelsObs = this._configService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsUseSlashModels, this._expService);
82
private _undesiredModelsObs = observableFromEvent(this, this._undesiredModelsManager.onDidChange, () => this._undesiredModelsManager);
83
84
private _modelsObs: IObservable<ModelConfigurationWithSource[]>;
85
private _currentModelObs: IObservable<ModelConfigurationWithSource>;
86
private _modelInfoObs: IObservable<ModelInfo>;
87
88
public readonly onModelListUpdated: Event<void>;
89
90
private readonly _setModelQueue = new TaskQueue();
91
private _logger: ILogger;
92
93
constructor(
94
@ICopilotTokenStore private readonly _tokenStore: ICopilotTokenStore,
95
@IProxyModelsService private readonly _proxyModelsService: IProxyModelsService,
96
@IUndesiredModelsManager private readonly _undesiredModelsManager: IUndesiredModelsManager,
97
@IConfigurationService private readonly _configService: IConfigurationService,
98
@IExperimentationService private readonly _expService: IExperimentationService,
99
@ITelemetryService private readonly _telemetryService: ITelemetryService,
100
@ILogService private readonly _logService: ILogService,
101
) {
102
super();
103
104
this._logger = _logService.createSubLogger(['NES', 'ModelsService']);
105
106
const logger = this._logger.createSubLogger('constructor');
107
108
this._modelsObs = derived((reader) => {
109
logger.trace('computing models');
110
return this.aggregateModels({
111
copilotToken: this._copilotTokenObs.read(reader),
112
fetchedNesModels: this._fetchedModelsObs.read(reader),
113
localModelConfig: this._localModelConfigObs.read(reader),
114
modelConfigString: this._expBasedModelConfigObs.read(reader),
115
defaultModelConfigString: this._defaultModelConfigObs.read(reader),
116
useSlashModels: this._useSlashModelsObs.read(reader),
117
});
118
}).recomputeInitiallyAndOnChange(this._store);
119
120
this._currentModelObs = derived<ModelConfigurationWithSource, void>((reader) => {
121
logger.trace('computing current model');
122
const undesiredModelsManager = this._undesiredModelsObs.read(reader);
123
return this._pickModel({
124
preferredModelName: this._preferredModelNameObs.read(reader),
125
models: this._modelsObs.read(reader),
126
undesiredModelsManager,
127
});
128
}).recomputeInitiallyAndOnChange(this._store);
129
130
this._modelInfoObs = derived((reader) => {
131
logger.trace('computing model info');
132
return {
133
models: this._modelsObs.read(reader),
134
currentModelId: this._currentModelObs.read(reader).modelName,
135
};
136
}).recomputeInitiallyAndOnChange(this._store);
137
138
this.onModelListUpdated = Event.fromObservableLight(this._modelInfoObs);
139
}
140
141
get modelInfo(): vscode.InlineCompletionModelInfo | undefined {
142
const models: vscode.InlineCompletionModel[] = this._modelsObs.get().map(m => ({
143
id: m.modelName,
144
name: m.modelName,
145
}));
146
147
const currentModel = this._currentModelObs.get();
148
149
return {
150
models,
151
currentModelId: currentModel.modelName,
152
};
153
}
154
155
156
setCurrentModelId(newPreferredModelId: string): Promise<void> {
157
return this._setModelQueue.schedule(() => this._setCurrentModelIdCore(newPreferredModelId));
158
}
159
160
private async _setCurrentModelIdCore(newPreferredModelId: string): Promise<void> {
161
const currentPreferredModelId = this._configService.getExperimentBasedConfig(ConfigKey.Advanced.InlineEditsPreferredModel, this._expService);
162
163
const isSameModel = currentPreferredModelId === newPreferredModelId;
164
if (isSameModel) {
165
return;
166
}
167
168
// snapshot before async calls
169
const currentPreferredModel = this._currentModelObs.get();
170
171
const models = this._modelsObs.get();
172
const newPreferredModel = models.find(m => m.modelName === newPreferredModelId);
173
174
if (newPreferredModel === undefined) {
175
this._logService.error(`New preferred model id ${newPreferredModelId} not found in model list.`);
176
return;
177
}
178
179
// if currently selected model is from exp config, then mark that model as undesired
180
if (currentPreferredModel.source === ModelSource.ExpConfig) {
181
await this._undesiredModelsManager.addUndesiredModelId(currentPreferredModel.modelName);
182
}
183
184
if (this._undesiredModelsManager.isUndesiredModelId(newPreferredModelId)) {
185
await this._undesiredModelsManager.removeUndesiredModelId(newPreferredModelId);
186
}
187
188
// if user picks same as the default model, we should reset the user setting
189
// otherwise, update the model
190
const expectedDefaultModel = this._pickModel({ preferredModelName: 'none', models, undesiredModelsManager: this._undesiredModelsManager });
191
if (newPreferredModel.source === ModelSource.ExpConfig || // because exp-configured model already takes highest priority
192
(newPreferredModelId === expectedDefaultModel.modelName && !models.some(m => m.source === ModelSource.ExpConfig))
193
) {
194
this._logger.trace(`New preferred model id ${newPreferredModelId} is the same as the default model, resetting user setting.`);
195
await this._configService.setConfig(ConfigKey.Advanced.InlineEditsPreferredModel, 'none');
196
} else {
197
this._logger.trace(`New preferred model id ${newPreferredModelId} is different from the default model, updating user setting to ${newPreferredModelId}.`);
198
await this._configService.setConfig(ConfigKey.Advanced.InlineEditsPreferredModel, newPreferredModelId);
199
}
200
}
201
202
private aggregateModels(
203
{
204
copilotToken,
205
fetchedNesModels,
206
localModelConfig,
207
modelConfigString,
208
defaultModelConfigString,
209
useSlashModels,
210
}: {
211
copilotToken: CopilotToken | undefined;
212
fetchedNesModels: WireTypes.Model.t[] | undefined;
213
localModelConfig: ModelConfiguration | null;
214
modelConfigString: string | undefined;
215
defaultModelConfigString: string | undefined;
216
useSlashModels: boolean;
217
},
218
): ModelConfigurationWithSource[] {
219
const logger = this._logger.createSubLogger('aggregateModels');
220
221
const models: ModelConfigurationWithSource[] = [];
222
223
// priority of adding models to the list:
224
// 0. model from user local setting
225
// 1. model from modelConfigurationString setting (set through ExP)
226
// 2. fetched models from /models endpoint (if useSlashModels is true)
227
228
if (localModelConfig) {
229
if (models.some(m => m.modelName === localModelConfig.modelName)) {
230
logger.trace('Local model configuration already exists in the model list, skipping.');
231
} else {
232
logger.trace(`Adding local model configuration: ${localModelConfig.modelName}`);
233
models.push({ ...localModelConfig, source: ModelSource.LocalConfig });
234
}
235
}
236
237
if (modelConfigString) {
238
logger.trace('Parsing modelConfigurationString...');
239
const parsedConfig = this.parseModelConfigString(modelConfigString, ConfigKey.TeamInternal.InlineEditsXtabProviderModelConfigurationString);
240
if (parsedConfig && !models.some(m => m.modelName === parsedConfig.modelName)) {
241
logger.trace(`Adding model from modelConfigurationString: ${parsedConfig.modelName}`);
242
models.push({ ...parsedConfig, source: ModelSource.ExpConfig });
243
} else {
244
logger.trace('No valid model found in modelConfigurationString.');
245
}
246
}
247
248
if (useSlashModels && fetchedNesModels && fetchedNesModels.length > 0) {
249
logger.trace(`Processing ${fetchedNesModels.length} fetched models...`);
250
const filteredFetchedModels = filterMap(fetchedNesModels, (m) => {
251
if (!isPromptingStrategy(m.capabilities.promptStrategy)) {
252
return undefined;
253
}
254
if (models.some(knownModel => knownModel.modelName === m.name)) {
255
logger.trace(`Fetched model ${m.name} already exists in the model list, skipping.`);
256
return undefined;
257
}
258
return {
259
modelName: m.name,
260
promptingStrategy: m.capabilities.promptStrategy,
261
includeTagsInCurrentFile: false, // FIXME@ulugbekna: determine this based on model capabilities and config
262
source: ModelSource.Fetched,
263
lintOptions: undefined,
264
} satisfies ModelConfigurationWithSource;
265
});
266
logger.trace(`Adding ${filteredFetchedModels.length} fetched models after filtering.`);
267
pushMany(models, filteredFetchedModels);
268
} else {
269
// push default model if /models doesn't give us any models
270
logger.trace(`adding built-in default model: useSlashModels ${useSlashModels}, fetchedNesModels ${fetchedNesModels?.length ?? 'undefined'}`);
271
272
const defaultModel = this.determineDefaultModel(copilotToken, defaultModelConfigString);
273
if (defaultModel) {
274
if (models.some(m => m.modelName === defaultModel.modelName)) {
275
logger.trace('Default model configuration already exists in the model list, skipping.');
276
} else {
277
logger.trace(`Adding default model configuration: ${defaultModel.modelName}`);
278
models.push(defaultModel);
279
}
280
}
281
}
282
283
return models;
284
}
285
286
public selectedModelConfiguration(): ModelConfiguration {
287
return toModelConfiguration(this._currentModelObs.get());
288
}
289
290
public defaultModelConfiguration(): ModelConfiguration {
291
const models = this._modelsObs.get();
292
if (models && models.length > 0) {
293
const defaultModels = models.filter(m => !this.isConfiguredModel(m));
294
if (defaultModels.length > 0) {
295
return toModelConfiguration(defaultModels[0]);
296
}
297
}
298
return toModelConfiguration(this.determineDefaultModel(this._copilotTokenObs.get(), this._defaultModelConfigObs.get()));
299
}
300
301
private isConfiguredModel(model: ModelConfigurationWithSource): boolean {
302
switch (model.source) {
303
case ModelSource.LocalConfig:
304
case ModelSource.ExpConfig:
305
case ModelSource.ExpDefaultConfig:
306
return true;
307
case ModelSource.Fetched:
308
case ModelSource.HardCodedDefault:
309
return false;
310
default:
311
assertNever(model.source);
312
}
313
}
314
315
private determineDefaultModel(copilotToken: CopilotToken | undefined, defaultModelConfigString: string | undefined): ModelConfigurationWithSource {
316
// if a default model config string is specified, use that
317
if (defaultModelConfigString) {
318
const parsedConfig = this.parseModelConfigString(defaultModelConfigString, ConfigKey.TeamInternal.InlineEditsXtabProviderDefaultModelConfigurationString);
319
if (parsedConfig) {
320
return { ...parsedConfig, source: ModelSource.ExpDefaultConfig };
321
}
322
}
323
324
// otherwise, use built-in defaults
325
if (copilotToken?.isFcv1()) {
326
return InlineEditsModelService.COPILOT_NES_XTAB_MODEL;
327
} else if (copilotToken?.isFreeUser || copilotToken?.isNoAuthUser) {
328
return InlineEditsModelService.COPILOT_NES_CALLISTO;
329
} else {
330
return InlineEditsModelService.COPILOT_NES_OCT;
331
}
332
}
333
334
private _pickModel({
335
preferredModelName,
336
models,
337
undesiredModelsManager,
338
}: {
339
preferredModelName: string;
340
models: ModelConfigurationWithSource[];
341
undesiredModelsManager: IUndesiredModelsManager;
342
}): ModelConfigurationWithSource {
343
// priority of picking a model:
344
// 0. model from modelConfigurationString setting from ExP, unless marked as undesired
345
// 1. user preferred model
346
// 2. first model in the list
347
348
const expConfiguredModel = models.find(m => m.source === ModelSource.ExpConfig);
349
if (expConfiguredModel) {
350
const isUndesiredModelId = undesiredModelsManager.isUndesiredModelId(expConfiguredModel.modelName);
351
if (isUndesiredModelId) {
352
this._logger.trace(`Exp-configured model ${expConfiguredModel.modelName} is marked as undesired by the user. Skipping.`);
353
} else {
354
return expConfiguredModel;
355
}
356
}
357
358
const userHasPreferredModel = preferredModelName !== 'none';
359
360
if (userHasPreferredModel) {
361
const preferredModel = models.find(m => m.modelName === preferredModelName);
362
if (preferredModel) {
363
return preferredModel;
364
}
365
}
366
367
softAssert(models.length > 0, 'InlineEdits model list should have at least one model');
368
369
const model = models.at(0);
370
if (model) {
371
return model;
372
}
373
374
return this.determineDefaultModel(this._copilotTokenObs.get(), this._defaultModelConfigObs.get());
375
}
376
377
private parseModelConfigString(configString: string, configKey: ExperimentBasedConfig<string | undefined>): ModelConfiguration | undefined {
378
let errorMessage: string;
379
try {
380
const parsed: unknown = JSON.parse(configString);
381
const result = MODEL_CONFIGURATION_VALIDATOR.validate(parsed);
382
if (!result.error) {
383
return result.content;
384
}
385
errorMessage = result.error.message;
386
} catch (e: unknown) {
387
errorMessage = ErrorUtils.toString(ErrorUtils.fromUnknown(e));
388
}
389
390
/* __GDPR__
391
"incorrectNesModelConfig" : {
392
"owner": "ulugbekna",
393
"comment": "Capture if model configuration string is invalid or malformed.",
394
"configName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Name of the configuration that failed to parse." },
395
"errorMessage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error message from parsing or validation." },
396
"configValue": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The invalid config string." }
397
}
398
*/
399
this._telemetryService.sendMSFTTelemetryEvent('incorrectNesModelConfig', { configName: configKey.id, errorMessage, configValue: configString });
400
return undefined;
401
}
402
}
403
404
function toModelConfiguration(model: ModelConfigurationWithSource): ModelConfiguration {
405
const { source: _, ...config } = model;
406
return config;
407
}
408
409
export namespace UndesiredModels {
410
411
const UNDESIRED_MODELS_KEY = 'copilot.chat.nextEdits.undesiredModelIds';
412
type UndesiredModelsValue = string[];
413
414
export class Manager extends Disposable implements IUndesiredModelsManager {
415
declare _serviceBrand: undefined;
416
417
private readonly _onDidChange = this._register(new Emitter<void>());
418
readonly onDidChange = this._onDidChange.event;
419
420
private readonly _queue = new TaskQueue();
421
422
constructor(
423
@IVSCodeExtensionContext private readonly _vscodeExtensionContext: IVSCodeExtensionContext,
424
) {
425
super();
426
}
427
428
isUndesiredModelId(modelId: string) {
429
const models = this._getModels();
430
return models.includes(modelId);
431
}
432
433
addUndesiredModelId(modelId: string): Promise<void> {
434
return this._queue.schedule(async () => {
435
const models = this._getModels();
436
if (!models.includes(modelId)) {
437
models.push(modelId);
438
await this._setModels(models);
439
this._onDidChange.fire();
440
}
441
});
442
}
443
444
removeUndesiredModelId(modelId: string): Promise<void> {
445
return this._queue.schedule(async () => {
446
const models = this._getModels();
447
const index = models.indexOf(modelId);
448
if (index !== -1) {
449
models.splice(index, 1);
450
await this._setModels(models);
451
this._onDidChange.fire();
452
}
453
});
454
}
455
456
private _getModels(): string[] {
457
return this._vscodeExtensionContext.globalState.get<UndesiredModelsValue>(UNDESIRED_MODELS_KEY) ?? [];
458
}
459
460
private _setModels(models: string[]): Promise<void> {
461
return new Promise((resolve, reject) => {
462
this._vscodeExtensionContext.globalState.update(UNDESIRED_MODELS_KEY, models).then(resolve, reject);
463
});
464
}
465
}
466
}
467
468
469