Skip to content

Commit 92c6899

Browse files
Copilotlramos15
andauthored
Add when clause support to languageModelChatProviders contribution (microsoft#272299)
* Initial plan * Add when clause support to languageModelChatProviders contribution Co-authored-by: lramos15 <4544166+lramos15@users.noreply.github.com> * Use contextMatchesRules instead of evaluate for when clause evaluation Co-authored-by: lramos15 <4544166+lramos15@users.noreply.github.com> * Fix test failure by using separate test suite with proper lifecycle Co-authored-by: lramos15 <4544166+lramos15@users.noreply.github.com> * Fix when clause evaluation in tests by implementing contextMatchesRules Co-authored-by: lramos15 <4544166+lramos15@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lramos15 <4544166+lramos15@users.noreply.github.com> Co-authored-by: Logan Ramos <lramos15@gmail.com>
1 parent 7ac9316 commit 92c6899

File tree

2 files changed

+106
-2
lines changed

2 files changed

+106
-2
lines changed

src/vs/workbench/contrib/chat/common/languageModels.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
1515
import { URI } from '../../../../base/common/uri.js';
1616
import { localize } from '../../../../nls.js';
1717
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
18-
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
18+
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
1919
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
2020
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
2121
import { ILogService } from '../../../../platform/log/common/log.js';
@@ -279,6 +279,10 @@ const languageModelChatProviderType: IJSONSchema = {
279279
managementCommand: {
280280
type: 'string',
281281
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.")
282+
},
283+
when: {
284+
type: 'string',
285+
description: localize('vscode.extension.contributes.languageModels.when', "Condition which must be true to show this language model chat provider in the Manage Models list.")
282286
}
283287
}
284288
};
@@ -287,6 +291,7 @@ export interface IUserFriendlyLanguageModel {
287291
vendor: string;
288292
displayName: string;
289293
managementCommand?: string;
294+
when?: string;
290295
}
291296

292297
export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint<IUserFriendlyLanguageModel | IUserFriendlyLanguageModel[]>({
@@ -320,6 +325,7 @@ export class LanguageModelsService implements ILanguageModelsService {
320325
private readonly _resolveLMSequencer = new SequencerByKey<string>();
321326
private _modelPickerUserPreferences: Record<string, boolean> = {};
322327
private readonly _hasUserSelectableModels: IContextKey<boolean>;
328+
private readonly _contextKeyService: IContextKeyService;
323329
private readonly _onLanguageModelChange = this._store.add(new Emitter<void>());
324330
readonly onDidChangeLanguageModels: Event<void> = this._onLanguageModelChange.event;
325331

@@ -332,6 +338,7 @@ export class LanguageModelsService implements ILanguageModelsService {
332338
@IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService,
333339
) {
334340
this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService);
341+
this._contextKeyService = _contextKeyService;
335342
this._modelPickerUserPreferences = this._storageService.getObject<Record<string, boolean>>('chatModelPickerPreferences', StorageScope.PROFILE, this._modelPickerUserPreferences);
336343
// TODO @lramos15 - Remove after a few releases, as this is just cleaning a bad storage state
337344
const entitlementChangeHandler = () => {
@@ -411,7 +418,13 @@ export class LanguageModelsService implements ILanguageModelsService {
411418
}
412419

413420
getVendors(): IUserFriendlyLanguageModel[] {
414-
return Array.from(this._vendors.values());
421+
return Array.from(this._vendors.values()).filter(vendor => {
422+
if (!vendor.when) {
423+
return true; // No when clause means always visible
424+
}
425+
const whenClause = ContextKeyExpr.deserialize(vendor.when);
426+
return whenClause ? this._contextKeyService.contextMatchesRules(whenClause) : false;
427+
});
415428
}
416429

417430
getLanguageModelIds(): string[] {

src/vs/workbench/contrib/chat/test/common/languageModels.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { TestChatEntitlementService, TestStorageService } from '../../../../test
1919
import { Event } from '../../../../../base/common/event.js';
2020
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
2121
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
22+
import { ContextKeyExpression } from '../../../../../platform/contextkey/common/contextkey.js';
2223

2324
suite('LanguageModels', function () {
2425

@@ -198,4 +199,94 @@ suite('LanguageModels', function () {
198199

199200
await request.result;
200201
});
202+
203+
test('when clause defaults to true when omitted', async function () {
204+
const vendors = languageModels.getVendors();
205+
// Both test-vendor and actual-vendor have no when clause, so they should be visible
206+
assert.ok(vendors.length >= 2);
207+
assert.ok(vendors.some(v => v.vendor === 'test-vendor'));
208+
assert.ok(vendors.some(v => v.vendor === 'actual-vendor'));
209+
});
210+
});
211+
212+
suite('LanguageModels - When Clause', function () {
213+
214+
class TestContextKeyService extends MockContextKeyService {
215+
override contextMatchesRules(rules: ContextKeyExpression): boolean {
216+
if (!rules) {
217+
return true;
218+
}
219+
// Simple evaluation based on stored keys
220+
const keys = rules.keys();
221+
for (const key of keys) {
222+
const contextKey = this.getContextKeyValue(key);
223+
// If the key exists and is truthy, the rule matches
224+
if (contextKey) {
225+
return true;
226+
}
227+
}
228+
return false;
229+
}
230+
}
231+
232+
let languageModelsWithWhen: LanguageModelsService;
233+
let contextKeyService: TestContextKeyService;
234+
235+
setup(function () {
236+
contextKeyService = new TestContextKeyService();
237+
contextKeyService.createKey('testKey', true);
238+
239+
languageModelsWithWhen = new LanguageModelsService(
240+
new class extends mock<IExtensionService>() {
241+
override activateByEvent(name: string) {
242+
return Promise.resolve();
243+
}
244+
},
245+
new NullLogService(),
246+
new TestStorageService(),
247+
contextKeyService,
248+
new TestConfigurationService(),
249+
new TestChatEntitlementService()
250+
);
251+
252+
const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!;
253+
254+
ext.acceptUsers([{
255+
description: { ...nullExtensionDescription },
256+
value: { vendor: 'visible-vendor', displayName: 'Visible Vendor' },
257+
collector: null!
258+
}, {
259+
description: { ...nullExtensionDescription },
260+
value: { vendor: 'conditional-vendor', displayName: 'Conditional Vendor', when: 'testKey' },
261+
collector: null!
262+
}, {
263+
description: { ...nullExtensionDescription },
264+
value: { vendor: 'hidden-vendor', displayName: 'Hidden Vendor', when: 'falseKey' },
265+
collector: null!
266+
}]);
267+
});
268+
269+
teardown(function () {
270+
languageModelsWithWhen.dispose();
271+
});
272+
273+
ensureNoDisposablesAreLeakedInTestSuite();
274+
275+
test('when clause filters vendors correctly', async function () {
276+
const vendors = languageModelsWithWhen.getVendors();
277+
assert.strictEqual(vendors.length, 2);
278+
assert.ok(vendors.some(v => v.vendor === 'visible-vendor'));
279+
assert.ok(vendors.some(v => v.vendor === 'conditional-vendor'));
280+
assert.ok(!vendors.some(v => v.vendor === 'hidden-vendor'));
281+
});
282+
283+
test('when clause evaluates to true when context key is true', async function () {
284+
const vendors = languageModelsWithWhen.getVendors();
285+
assert.ok(vendors.some(v => v.vendor === 'conditional-vendor'), 'conditional-vendor should be visible when testKey is true');
286+
});
287+
288+
test('when clause evaluates to false when context key is false', async function () {
289+
const vendors = languageModelsWithWhen.getVendors();
290+
assert.ok(!vendors.some(v => v.vendor === 'hidden-vendor'), 'hidden-vendor should be hidden when falseKey is false');
291+
});
201292
});

0 commit comments

Comments
 (0)