Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts
5252 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 assert from 'assert';
7
import { AsyncIterableSource, DeferredPromise, timeout } from '../../../../../base/common/async.js';
8
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
9
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
10
import { mock } from '../../../../../base/test/common/mock.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
12
import { NullLogService } from '../../../../../platform/log/common/log.js';
13
import { ChatMessageRole, LanguageModelsService, IChatMessage, IChatResponsePart, ILanguageModelChatMetadata } from '../../common/languageModels.js';
14
import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';
15
import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/widget/input/modelPickerWidget.js';
16
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
17
import { TestStorageService } from '../../../../test/common/workbenchTestServices.js';
18
import { StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
19
import { Event } from '../../../../../base/common/event.js';
20
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
21
import { ContextKeyExpression } from '../../../../../platform/contextkey/common/contextkey.js';
22
import { ILanguageModelsConfigurationService } from '../../common/languageModelsConfiguration.js';
23
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
24
import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js';
25
26
suite('LanguageModels', function () {
27
28
let languageModels: LanguageModelsService;
29
30
const store = new DisposableStore();
31
const activationEvents = new Set<string>();
32
33
setup(function () {
34
35
languageModels = new LanguageModelsService(
36
new class extends mock<IExtensionService>() {
37
override activateByEvent(name: string) {
38
activationEvents.add(name);
39
return Promise.resolve();
40
}
41
},
42
new NullLogService(),
43
new TestStorageService(),
44
new MockContextKeyService(),
45
new class extends mock<ILanguageModelsConfigurationService>() {
46
override onDidChangeLanguageModelGroups = Event.None;
47
override getLanguageModelsProviderGroups() {
48
return [];
49
}
50
},
51
new class extends mock<IQuickInputService>() { },
52
new TestSecretStorageService(),
53
);
54
55
languageModels.deltaLanguageModelChatProviderDescriptors([
56
{ vendor: 'test-vendor', displayName: 'Test Vendor', configuration: undefined, managementCommand: undefined, when: undefined },
57
{ vendor: 'actual-vendor', displayName: 'Actual Vendor', configuration: undefined, managementCommand: undefined, when: undefined }
58
], []);
59
60
store.add(languageModels.registerLanguageModelProvider('test-vendor', {
61
onDidChange: Event.None,
62
provideLanguageModelChatInfo: async () => {
63
const modelMetadata = [
64
{
65
extension: nullExtensionDescription.identifier,
66
name: 'Pretty Name',
67
vendor: 'test-vendor',
68
family: 'test-family',
69
version: 'test-version',
70
modelPickerCategory: undefined,
71
id: 'test-id-1',
72
maxInputTokens: 100,
73
maxOutputTokens: 100,
74
isDefaultForLocation: {}
75
} satisfies ILanguageModelChatMetadata,
76
{
77
extension: nullExtensionDescription.identifier,
78
name: 'Pretty Name',
79
vendor: 'test-vendor',
80
family: 'test2-family',
81
version: 'test2-version',
82
modelPickerCategory: undefined,
83
id: 'test-id-12',
84
maxInputTokens: 100,
85
maxOutputTokens: 100,
86
isDefaultForLocation: {}
87
} satisfies ILanguageModelChatMetadata
88
];
89
const modelMetadataAndIdentifier = modelMetadata.map(m => ({
90
metadata: m,
91
identifier: m.id,
92
}));
93
return modelMetadataAndIdentifier;
94
},
95
sendChatRequest: async () => {
96
throw new Error();
97
},
98
provideTokenCount: async () => {
99
throw new Error();
100
}
101
}));
102
});
103
104
teardown(function () {
105
languageModels.dispose();
106
activationEvents.clear();
107
store.clear();
108
});
109
110
ensureNoDisposablesAreLeakedInTestSuite();
111
112
test('empty selector returns all', async function () {
113
114
const result1 = await languageModels.selectLanguageModels({});
115
assert.deepStrictEqual(result1.length, 2);
116
assert.deepStrictEqual(result1[0], 'test-id-1');
117
assert.deepStrictEqual(result1[1], 'test-id-12');
118
});
119
120
test('selector with id works properly', async function () {
121
const result1 = await languageModels.selectLanguageModels({ id: 'test-id-1' });
122
assert.deepStrictEqual(result1.length, 1);
123
assert.deepStrictEqual(result1[0], 'test-id-1');
124
});
125
126
test('no warning that a matching model was not found #213716', async function () {
127
const result1 = await languageModels.selectLanguageModels({ vendor: 'test-vendor' });
128
assert.deepStrictEqual(result1.length, 2);
129
130
const result2 = await languageModels.selectLanguageModels({ vendor: 'test-vendor', family: 'FAKE' });
131
assert.deepStrictEqual(result2.length, 0);
132
});
133
134
test('sendChatRequest returns a response-stream', async function () {
135
136
store.add(languageModels.registerLanguageModelProvider('actual-vendor', {
137
onDidChange: Event.None,
138
provideLanguageModelChatInfo: async () => {
139
const modelMetadata = [
140
{
141
extension: nullExtensionDescription.identifier,
142
name: 'Pretty Name',
143
vendor: 'actual-vendor',
144
family: 'actual-family',
145
version: 'actual-version',
146
id: 'actual-lm',
147
maxInputTokens: 100,
148
maxOutputTokens: 100,
149
modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY,
150
isDefaultForLocation: {}
151
} satisfies ILanguageModelChatMetadata
152
];
153
const modelMetadataAndIdentifier = modelMetadata.map(m => ({
154
metadata: m,
155
identifier: m.id,
156
}));
157
return modelMetadataAndIdentifier;
158
},
159
sendChatRequest: async (modelId: string, messages: IChatMessage[], _from: ExtensionIdentifier, _options: { [name: string]: any }, token: CancellationToken) => {
160
// const message = messages.at(-1);
161
162
const defer = new DeferredPromise();
163
const stream = new AsyncIterableSource<IChatResponsePart>();
164
165
(async () => {
166
while (!token.isCancellationRequested) {
167
stream.emitOne({ type: 'text', value: Date.now().toString() });
168
await timeout(10);
169
}
170
defer.complete(undefined);
171
})();
172
173
return {
174
stream: stream.asyncIterable,
175
result: defer.p
176
};
177
},
178
provideTokenCount: async () => {
179
throw new Error();
180
}
181
}));
182
183
// Register the extension point for the actual vendor
184
languageModels.deltaLanguageModelChatProviderDescriptors([
185
{ vendor: 'actual-vendor', displayName: 'Actual Vendor', configuration: undefined, managementCommand: undefined, when: undefined }
186
], []);
187
188
const models = await languageModels.selectLanguageModels({ id: 'actual-lm' });
189
assert.ok(models.length === 1);
190
191
const first = models[0];
192
193
const cts = new CancellationTokenSource();
194
195
const request = await languageModels.sendChatRequest(first, nullExtensionDescription.identifier, [{ role: ChatMessageRole.User, content: [{ type: 'text', value: 'hello' }] }], {}, cts.token);
196
197
assert.ok(request);
198
199
cts.dispose(true);
200
201
await request.result;
202
});
203
204
test('when clause defaults to true when omitted', async function () {
205
const vendors = languageModels.getVendors();
206
// Both test-vendor and actual-vendor have no when clause, so they should be visible
207
assert.ok(vendors.length >= 2);
208
assert.ok(vendors.some(v => v.vendor === 'test-vendor'));
209
assert.ok(vendors.some(v => v.vendor === 'actual-vendor'));
210
});
211
});
212
213
suite('LanguageModels - When Clause', function () {
214
215
class TestContextKeyService extends MockContextKeyService {
216
override contextMatchesRules(rules: ContextKeyExpression): boolean {
217
if (!rules) {
218
return true;
219
}
220
// Simple evaluation based on stored keys
221
const keys = rules.keys();
222
for (const key of keys) {
223
const contextKey = this.getContextKeyValue(key);
224
// If the key exists and is truthy, the rule matches
225
if (contextKey) {
226
return true;
227
}
228
}
229
return false;
230
}
231
}
232
233
let languageModelsWithWhen: LanguageModelsService;
234
let contextKeyService: TestContextKeyService;
235
236
setup(function () {
237
contextKeyService = new TestContextKeyService();
238
contextKeyService.createKey('testKey', true);
239
240
languageModelsWithWhen = new LanguageModelsService(
241
new class extends mock<IExtensionService>() {
242
override activateByEvent(name: string) {
243
return Promise.resolve();
244
}
245
},
246
new NullLogService(),
247
new TestStorageService(),
248
contextKeyService,
249
new class extends mock<ILanguageModelsConfigurationService>() {
250
override onDidChangeLanguageModelGroups = Event.None;
251
},
252
new class extends mock<IQuickInputService>() { },
253
new TestSecretStorageService(),
254
);
255
256
languageModelsWithWhen.deltaLanguageModelChatProviderDescriptors([
257
{ vendor: 'visible-vendor', displayName: 'Visible Vendor', configuration: undefined, managementCommand: undefined, when: undefined },
258
{ vendor: 'conditional-vendor', displayName: 'Conditional Vendor', configuration: undefined, managementCommand: undefined, when: 'testKey' },
259
{ vendor: 'hidden-vendor', displayName: 'Hidden Vendor', configuration: undefined, managementCommand: undefined, when: 'falseKey' }
260
], []);
261
});
262
263
teardown(function () {
264
languageModelsWithWhen.dispose();
265
});
266
267
ensureNoDisposablesAreLeakedInTestSuite();
268
269
test('when clause filters vendors correctly', async function () {
270
const vendors = languageModelsWithWhen.getVendors();
271
assert.strictEqual(vendors.length, 2);
272
assert.ok(vendors.some(v => v.vendor === 'visible-vendor'));
273
assert.ok(vendors.some(v => v.vendor === 'conditional-vendor'));
274
assert.ok(!vendors.some(v => v.vendor === 'hidden-vendor'));
275
});
276
277
test('when clause evaluates to true when context key is true', async function () {
278
const vendors = languageModelsWithWhen.getVendors();
279
assert.ok(vendors.some(v => v.vendor === 'conditional-vendor'), 'conditional-vendor should be visible when testKey is true');
280
});
281
282
test('when clause evaluates to false when context key is false', async function () {
283
const vendors = languageModelsWithWhen.getVendors();
284
assert.ok(!vendors.some(v => v.vendor === 'hidden-vendor'), 'hidden-vendor should be hidden when falseKey is false');
285
});
286
287
});
288
289
suite('LanguageModels - Model Picker Preferences Storage', function () {
290
291
let languageModelsService: LanguageModelsService;
292
let storageService: TestStorageService;
293
const disposables = new DisposableStore();
294
295
setup(async function () {
296
storageService = new TestStorageService();
297
298
languageModelsService = new LanguageModelsService(
299
new class extends mock<IExtensionService>() {
300
override activateByEvent(name: string) {
301
return Promise.resolve();
302
}
303
},
304
new NullLogService(),
305
storageService,
306
new MockContextKeyService(),
307
new class extends mock<ILanguageModelsConfigurationService>() {
308
override onDidChangeLanguageModelGroups = Event.None;
309
override getLanguageModelsProviderGroups() {
310
return [];
311
}
312
},
313
new class extends mock<IQuickInputService>() { },
314
new TestSecretStorageService(),
315
);
316
317
// Register vendor1 used in most tests
318
languageModelsService.deltaLanguageModelChatProviderDescriptors([
319
{ vendor: 'vendor1', displayName: 'Vendor 1', configuration: undefined, managementCommand: undefined, when: undefined }
320
], []);
321
322
disposables.add(languageModelsService.registerLanguageModelProvider('vendor1', {
323
onDidChange: Event.None,
324
provideLanguageModelChatInfo: async () => {
325
return [{
326
metadata: {
327
extension: nullExtensionDescription.identifier,
328
name: 'Model 1',
329
vendor: 'vendor1',
330
family: 'family1',
331
version: '1.0',
332
id: 'vendor1/model1',
333
maxInputTokens: 100,
334
maxOutputTokens: 100,
335
modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY,
336
isDefaultForLocation: {}
337
} satisfies ILanguageModelChatMetadata,
338
identifier: 'vendor1/model1'
339
}];
340
},
341
sendChatRequest: async () => { throw new Error(); },
342
provideTokenCount: async () => { throw new Error(); }
343
}));
344
345
// Populate the model cache
346
await languageModelsService.selectLanguageModels({});
347
});
348
349
teardown(function () {
350
languageModelsService.dispose();
351
disposables.clear();
352
});
353
354
ensureNoDisposablesAreLeakedInTestSuite();
355
356
test('fires onChange event when new model preferences are added', async function () {
357
// Listen for change event
358
let firedVendorId: string | undefined;
359
disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => firedVendorId = vendorId));
360
361
// Add new preferences to storage - store() automatically triggers change event synchronously
362
const preferences = {
363
'vendor1/model1': true
364
};
365
storageService.store('chatModelPickerPreferences', JSON.stringify(preferences), StorageScope.PROFILE, StorageTarget.USER);
366
367
// Verify change event was fired
368
assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1');
369
370
// Verify preference was updated
371
const model = languageModelsService.lookupLanguageModel('vendor1/model1');
372
assert.ok(model);
373
assert.strictEqual(model.isUserSelectable, true);
374
});
375
376
test('fires onChange event when model preferences are removed', async function () {
377
// Set initial preference using the API
378
languageModelsService.updateModelPickerPreference('vendor1/model1', true);
379
380
// Listen for change event
381
let firedVendorId: string | undefined;
382
disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => {
383
firedVendorId = vendorId;
384
}));
385
386
// Remove preferences via storage API
387
const updatedPreferences = {};
388
storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER);
389
390
// Verify change event was fired
391
assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1 when preference removed');
392
393
// Verify preference was removed
394
const model = languageModelsService.lookupLanguageModel('vendor1/model1');
395
assert.ok(model);
396
assert.strictEqual(model.isUserSelectable, undefined);
397
});
398
399
test('fires onChange event when model preferences are updated', async function () {
400
// Set initial preference using the API
401
languageModelsService.updateModelPickerPreference('vendor1/model1', true);
402
403
// Listen for change event
404
let firedVendorId: string | undefined;
405
disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => {
406
firedVendorId = vendorId;
407
}));
408
409
// Update the preference value
410
const updatedPreferences = {
411
'vendor1/model1': false
412
};
413
storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER);
414
415
// Verify change event was fired
416
assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1 when preference updated');
417
418
// Verify preference was updated
419
const model = languageModelsService.lookupLanguageModel('vendor1/model1');
420
assert.ok(model);
421
assert.strictEqual(model.isUserSelectable, false);
422
});
423
424
test('only fires onChange event for affected vendors', async function () {
425
// Register vendor2
426
languageModelsService.deltaLanguageModelChatProviderDescriptors([
427
{ vendor: 'vendor2', displayName: 'Vendor 2', configuration: undefined, managementCommand: undefined, when: undefined }
428
], []);
429
430
disposables.add(languageModelsService.registerLanguageModelProvider('vendor2', {
431
onDidChange: Event.None,
432
provideLanguageModelChatInfo: async () => {
433
return [{
434
metadata: {
435
extension: nullExtensionDescription.identifier,
436
name: 'Model 2',
437
vendor: 'vendor2',
438
family: 'family2',
439
version: '1.0',
440
id: 'vendor2/model2',
441
maxInputTokens: 100,
442
maxOutputTokens: 100,
443
modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY,
444
isDefaultForLocation: {}
445
} satisfies ILanguageModelChatMetadata,
446
identifier: 'vendor2/model2'
447
}];
448
},
449
sendChatRequest: async () => { throw new Error(); },
450
provideTokenCount: async () => { throw new Error(); }
451
}));
452
453
await languageModelsService.selectLanguageModels({});
454
455
// Set initial preferences using the API
456
languageModelsService.updateModelPickerPreference('vendor1/model1', true);
457
languageModelsService.updateModelPickerPreference('vendor2/model2', false);
458
459
// Listen for change event
460
let firedVendorId: string | undefined;
461
disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => {
462
firedVendorId = vendorId;
463
}));
464
465
// Update only vendor1 preference
466
const updatedPreferences = {
467
'vendor1/model1': false,
468
'vendor2/model2': false // unchanged
469
};
470
storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER);
471
472
// Verify only vendor1 was affected
473
assert.strictEqual(firedVendorId, 'vendor1', 'Should only affect vendor1');
474
475
// Verify preferences were updated correctly
476
const model1 = languageModelsService.lookupLanguageModel('vendor1/model1');
477
assert.ok(model1);
478
assert.strictEqual(model1.isUserSelectable, false, 'vendor1/model1 should be updated to false');
479
480
const model2 = languageModelsService.lookupLanguageModel('vendor2/model2');
481
assert.ok(model2);
482
assert.strictEqual(model2.isUserSelectable, false, 'vendor2/model2 should remain false');
483
});
484
485
test('does not fire onChange event when preferences are unchanged', async function () {
486
// Set initial preference using the API
487
languageModelsService.updateModelPickerPreference('vendor1/model1', true);
488
489
// Listen for change event
490
let eventFired = false;
491
disposables.add(languageModelsService.onDidChangeLanguageModels(() => {
492
eventFired = true;
493
}));
494
495
// Store the same preferences again
496
const initialPreferences = {
497
'vendor1/model1': true
498
};
499
storageService.store('chatModelPickerPreferences', JSON.stringify(initialPreferences), StorageScope.PROFILE, StorageTarget.USER);
500
501
// Verify no event was fired
502
assert.strictEqual(eventFired, false, 'Should not fire event when preferences are unchanged');
503
504
// Verify preference remains the same
505
const model = languageModelsService.lookupLanguageModel('vendor1/model1');
506
assert.ok(model);
507
assert.strictEqual(model.isUserSelectable, true);
508
});
509
510
test('handles malformed JSON in storage gracefully', function () {
511
// Listen for change event
512
let eventFired = false;
513
disposables.add(languageModelsService.onDidChangeLanguageModels(() => {
514
eventFired = true;
515
}));
516
517
// Store empty preferences - store() automatically triggers change event
518
storageService.store('chatModelPickerPreferences', '{}', StorageScope.PROFILE, StorageTarget.USER);
519
520
// Verify no event was fired - empty preferences is valid and causes no changes
521
assert.strictEqual(eventFired, false, 'Should not fire event for empty preferences');
522
});
523
});
524
525
suite('LanguageModels - Model Change Events', function () {
526
527
let languageModelsService: LanguageModelsService;
528
let storageService: TestStorageService;
529
const disposables = new DisposableStore();
530
531
setup(async function () {
532
storageService = new TestStorageService();
533
534
languageModelsService = new LanguageModelsService(
535
new class extends mock<IExtensionService>() {
536
override activateByEvent(name: string) {
537
return Promise.resolve();
538
}
539
},
540
new NullLogService(),
541
storageService,
542
new MockContextKeyService(),
543
new class extends mock<ILanguageModelsConfigurationService>() {
544
override onDidChangeLanguageModelGroups = Event.None;
545
override getLanguageModelsProviderGroups() {
546
return [];
547
}
548
},
549
new class extends mock<IQuickInputService>() { },
550
new TestSecretStorageService(),
551
);
552
553
// Register the vendor first
554
languageModelsService.deltaLanguageModelChatProviderDescriptors([
555
{ vendor: 'test-vendor', displayName: 'Test Vendor', configuration: undefined, managementCommand: undefined, when: undefined }
556
], []);
557
});
558
559
teardown(function () {
560
languageModelsService.dispose();
561
disposables.clear();
562
});
563
564
ensureNoDisposablesAreLeakedInTestSuite();
565
566
test('fires onChange event when new models are added', async function () {
567
// Create a promise that resolves when the event fires
568
const eventPromise = new Promise<string>((resolve) => {
569
disposables.add(languageModelsService.onDidChangeLanguageModels((vendorId) => {
570
resolve(vendorId);
571
}));
572
});
573
574
// Store a preference to trigger auto-resolution when provider is registered
575
storageService.store('chatModelPickerPreferences', JSON.stringify({ 'test-vendor/model1': true }), StorageScope.PROFILE, StorageTarget.USER);
576
577
disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', {
578
onDidChange: Event.None,
579
provideLanguageModelChatInfo: async () => {
580
return [{
581
metadata: {
582
extension: nullExtensionDescription.identifier,
583
name: 'Model 1',
584
vendor: 'test-vendor',
585
family: 'family1',
586
version: '1.0',
587
id: 'model1',
588
maxInputTokens: 100,
589
maxOutputTokens: 100,
590
modelPickerCategory: undefined,
591
isDefaultForLocation: {}
592
} satisfies ILanguageModelChatMetadata,
593
identifier: 'test-vendor/model1'
594
}];
595
},
596
sendChatRequest: async () => { throw new Error(); },
597
provideTokenCount: async () => { throw new Error(); }
598
}));
599
600
const firedVendorId = await eventPromise;
601
assert.strictEqual(firedVendorId, 'test-vendor', 'Should fire event when new models are added');
602
});
603
604
test('does not fire onChange event when models are unchanged', async function () {
605
const models = [{
606
metadata: {
607
extension: nullExtensionDescription.identifier,
608
name: 'Model 1',
609
vendor: 'test-vendor',
610
family: 'family1',
611
version: '1.0',
612
id: 'model1',
613
maxInputTokens: 100,
614
maxOutputTokens: 100,
615
modelPickerCategory: undefined,
616
isDefaultForLocation: {}
617
} satisfies ILanguageModelChatMetadata,
618
identifier: 'test-vendor/model1'
619
}];
620
621
let onDidChangeEmitter: any;
622
disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', {
623
onDidChange: (listener) => {
624
onDidChangeEmitter = { fire: () => listener() };
625
return { dispose: () => { } };
626
},
627
provideLanguageModelChatInfo: async () => models,
628
sendChatRequest: async () => { throw new Error(); },
629
provideTokenCount: async () => { throw new Error(); }
630
}));
631
632
// Initial resolution
633
await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' });
634
635
// Listen for change event
636
let eventFired = false;
637
disposables.add(languageModelsService.onDidChangeLanguageModels(() => {
638
eventFired = true;
639
}));
640
// Trigger provider change with same models
641
onDidChangeEmitter.fire();
642
643
// Call selectLanguageModels again - provider will return different models
644
await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' });
645
assert.strictEqual(eventFired, false, 'Should not fire event when models are unchanged');
646
});
647
648
test('fires onChange event when model metadata changes', async function () {
649
const initialModels = [{
650
metadata: {
651
extension: nullExtensionDescription.identifier,
652
name: 'Model 1',
653
vendor: 'test-vendor',
654
family: 'family1',
655
version: '1.0',
656
id: 'model1',
657
maxInputTokens: 100,
658
maxOutputTokens: 100,
659
modelPickerCategory: undefined,
660
isDefaultForLocation: {}
661
} satisfies ILanguageModelChatMetadata,
662
identifier: 'test-vendor/model1'
663
}];
664
665
let currentModels = initialModels;
666
let onDidChangeEmitter: any;
667
disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', {
668
onDidChange: (listener) => {
669
onDidChangeEmitter = { fire: () => listener() };
670
return { dispose: () => { } };
671
},
672
provideLanguageModelChatInfo: async () => currentModels,
673
sendChatRequest: async () => { throw new Error(); },
674
provideTokenCount: async () => { throw new Error(); }
675
}));
676
677
// Initial resolution
678
await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' });
679
680
// Create a promise that resolves when the event fires
681
const eventPromise = new Promise<void>((resolve) => {
682
disposables.add(languageModelsService.onDidChangeLanguageModels(() => {
683
resolve();
684
}));
685
});
686
687
// Change model metadata (e.g., maxInputTokens)
688
currentModels = [{
689
metadata: {
690
...initialModels[0].metadata,
691
maxInputTokens: 200 // Changed from 100
692
},
693
identifier: 'test-vendor/model1'
694
}];
695
696
onDidChangeEmitter.fire();
697
698
await eventPromise;
699
assert.ok(true, 'Event fired when model metadata changed');
700
});
701
702
test('fires onChange event when models are removed', async function () {
703
let currentModels = [{
704
metadata: {
705
extension: nullExtensionDescription.identifier,
706
name: 'Model 1',
707
vendor: 'test-vendor',
708
family: 'family1',
709
version: '1.0',
710
id: 'model1',
711
maxInputTokens: 100,
712
maxOutputTokens: 100,
713
modelPickerCategory: undefined,
714
isDefaultForLocation: {}
715
} satisfies ILanguageModelChatMetadata,
716
identifier: 'test-vendor/model1'
717
}];
718
719
let onDidChangeEmitter: any;
720
disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', {
721
onDidChange: (listener) => {
722
onDidChangeEmitter = { fire: () => listener() };
723
return { dispose: () => { } };
724
},
725
provideLanguageModelChatInfo: async () => currentModels,
726
sendChatRequest: async () => { throw new Error(); },
727
provideTokenCount: async () => { throw new Error(); }
728
}));
729
730
// Initial resolution
731
await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' });
732
733
// Create a promise that resolves when the event fires
734
const eventPromise = new Promise<void>((resolve) => {
735
disposables.add(languageModelsService.onDidChangeLanguageModels(() => {
736
resolve();
737
}));
738
});
739
740
// Remove all models
741
currentModels = [];
742
743
onDidChangeEmitter.fire();
744
745
await eventPromise;
746
assert.ok(true, 'Event fired when models were removed');
747
});
748
749
test('fires onChange event when new model is added to existing set', async function () {
750
let currentModels = [{
751
metadata: {
752
extension: nullExtensionDescription.identifier,
753
name: 'Model 1',
754
vendor: 'test-vendor',
755
family: 'family1',
756
version: '1.0',
757
id: 'model1',
758
maxInputTokens: 100,
759
maxOutputTokens: 100,
760
modelPickerCategory: undefined,
761
isDefaultForLocation: {}
762
} satisfies ILanguageModelChatMetadata,
763
identifier: 'test-vendor/model1'
764
}];
765
766
let onDidChangeEmitter: any;
767
disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', {
768
onDidChange: (listener) => {
769
onDidChangeEmitter = { fire: () => listener() };
770
return { dispose: () => { } };
771
},
772
provideLanguageModelChatInfo: async () => currentModels,
773
sendChatRequest: async () => { throw new Error(); },
774
provideTokenCount: async () => { throw new Error(); }
775
}));
776
777
// Initial resolution
778
await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' });
779
780
// Create a promise that resolves when the event fires
781
const eventPromise = new Promise<void>((resolve) => {
782
disposables.add(languageModelsService.onDidChangeLanguageModels(() => {
783
resolve();
784
}));
785
});
786
787
// Add a new model
788
currentModels = [
789
...currentModels,
790
{
791
metadata: {
792
extension: nullExtensionDescription.identifier,
793
name: 'Model 2',
794
vendor: 'test-vendor',
795
family: 'family2',
796
version: '1.0',
797
id: 'model2',
798
maxInputTokens: 100,
799
maxOutputTokens: 100,
800
modelPickerCategory: undefined,
801
isDefaultForLocation: {}
802
} satisfies ILanguageModelChatMetadata,
803
identifier: 'test-vendor/model2'
804
}
805
];
806
807
onDidChangeEmitter.fire();
808
809
await eventPromise;
810
assert.ok(true, 'Event fired when new model was added');
811
});
812
813
test('fires onChange event when models change without provider emitting change event', async function () {
814
let callCount = 0;
815
disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', {
816
onDidChange: Event.None, // Provider doesn't emit change events
817
provideLanguageModelChatInfo: async () => {
818
callCount++;
819
if (callCount === 1) {
820
// First call returns initial model
821
return [{
822
metadata: {
823
extension: nullExtensionDescription.identifier,
824
name: 'Model 1',
825
vendor: 'test-vendor',
826
family: 'family1',
827
version: '1.0',
828
id: 'model1',
829
maxInputTokens: 100,
830
maxOutputTokens: 100,
831
modelPickerCategory: undefined,
832
isDefaultForLocation: {}
833
} satisfies ILanguageModelChatMetadata,
834
identifier: 'test-vendor/model1'
835
}];
836
} else {
837
// Subsequent calls return different model
838
return [{
839
metadata: {
840
extension: nullExtensionDescription.identifier,
841
name: 'Model 2',
842
vendor: 'test-vendor',
843
family: 'family2',
844
version: '2.0',
845
id: 'model2',
846
maxInputTokens: 200,
847
maxOutputTokens: 200,
848
modelPickerCategory: undefined,
849
isDefaultForLocation: {}
850
} satisfies ILanguageModelChatMetadata,
851
identifier: 'test-vendor/model2'
852
}];
853
}
854
},
855
sendChatRequest: async () => { throw new Error(); },
856
provideTokenCount: async () => { throw new Error(); }
857
}));
858
859
// Initial resolution
860
await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' });
861
862
// Listen for change event
863
let eventFired = false;
864
disposables.add(languageModelsService.onDidChangeLanguageModels(() => {
865
eventFired = true;
866
}));
867
868
// Call selectLanguageModels again - provider will return different models
869
await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' });
870
871
assert.strictEqual(eventFired, true, 'Should fire event when models change even without provider change event');
872
});
873
});
874
875
suite('LanguageModels - Vendor Change Events', function () {
876
877
let languageModelsService: LanguageModelsService;
878
const disposables = new DisposableStore();
879
880
setup(function () {
881
languageModelsService = new LanguageModelsService(
882
new class extends mock<IExtensionService>() {
883
override activateByEvent(name: string) {
884
return Promise.resolve();
885
}
886
},
887
new NullLogService(),
888
new TestStorageService(),
889
new MockContextKeyService(),
890
new class extends mock<ILanguageModelsConfigurationService>() {
891
override onDidChangeLanguageModelGroups = Event.None;
892
override getLanguageModelsProviderGroups() {
893
return [];
894
}
895
},
896
new class extends mock<IQuickInputService>() { },
897
new TestSecretStorageService(),
898
);
899
});
900
901
teardown(function () {
902
languageModelsService.dispose();
903
disposables.clear();
904
});
905
906
ensureNoDisposablesAreLeakedInTestSuite();
907
908
test('fires onDidChangeLanguageModelVendors when a vendor is added', async function () {
909
const eventPromise = new Promise<readonly string[]>((resolve) => {
910
disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors)));
911
});
912
913
languageModelsService.deltaLanguageModelChatProviderDescriptors([
914
{ vendor: 'added-vendor', displayName: 'Added Vendor', configuration: undefined, managementCommand: undefined, when: undefined }
915
], []);
916
917
const vendors = await eventPromise;
918
assert.ok(vendors.includes('added-vendor'));
919
});
920
921
test('fires onDidChangeLanguageModelVendors when a vendor is removed', async function () {
922
languageModelsService.deltaLanguageModelChatProviderDescriptors([
923
{ vendor: 'removed-vendor', displayName: 'Removed Vendor', configuration: undefined, managementCommand: undefined, when: undefined }
924
], []);
925
926
const eventPromise = new Promise<readonly string[]>((resolve) => {
927
disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors)));
928
});
929
930
languageModelsService.deltaLanguageModelChatProviderDescriptors([], [
931
{ vendor: 'removed-vendor', displayName: 'Removed Vendor', configuration: undefined, managementCommand: undefined, when: undefined }
932
]);
933
934
const vendors = await eventPromise;
935
assert.ok(vendors.includes('removed-vendor'));
936
});
937
938
test('fires onDidChangeLanguageModelVendors when multiple vendors are added and removed', async function () {
939
// Add multiple vendors
940
const addEventPromise = new Promise<readonly string[]>((resolve) => {
941
disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors)));
942
});
943
944
languageModelsService.deltaLanguageModelChatProviderDescriptors([
945
{ vendor: 'vendor-a', displayName: 'Vendor A', configuration: undefined, managementCommand: undefined, when: undefined },
946
{ vendor: 'vendor-b', displayName: 'Vendor B', configuration: undefined, managementCommand: undefined, when: undefined }
947
], []);
948
949
const addedVendors = await addEventPromise;
950
assert.ok(addedVendors.includes('vendor-a'));
951
assert.ok(addedVendors.includes('vendor-b'));
952
953
// Remove one vendor
954
const removeEventPromise = new Promise<readonly string[]>((resolve) => {
955
disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors)));
956
});
957
958
languageModelsService.deltaLanguageModelChatProviderDescriptors([], [
959
{ vendor: 'vendor-a', displayName: 'Vendor A', configuration: undefined, managementCommand: undefined, when: undefined }
960
]);
961
962
const removedVendors = await removeEventPromise;
963
assert.ok(removedVendors.includes('vendor-a'));
964
});
965
966
test('does not fire onDidChangeLanguageModelVendors when no vendors are added or removed', async function () {
967
// Add initial vendor
968
languageModelsService.deltaLanguageModelChatProviderDescriptors([
969
{ vendor: 'stable-vendor', displayName: 'Stable Vendor', configuration: undefined, managementCommand: undefined, when: undefined }
970
], []);
971
972
// Listen for change event
973
let eventFired = false;
974
disposables.add(languageModelsService.onDidChangeLanguageModelVendors(() => {
975
eventFired = true;
976
}));
977
978
// Call with empty arrays - should not fire event
979
languageModelsService.deltaLanguageModelChatProviderDescriptors([], []);
980
981
assert.strictEqual(eventFired, false, 'Should not fire event when vendor list is unchanged');
982
});
983
});
984
985