-
Notifications
You must be signed in to change notification settings - Fork 37.3k
implement configuring language models from the editor #286616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request implements configuration support for language models directly from the editor, allowing users to add and manage multiple language model provider groups with custom configurations.
Key Changes:
- Adds configuration schema support to language model providers via extension point
- Implements a new configuration service for persisting provider groups in a JSON file
- Extends the UI to support configuring, adding, and deleting provider groups
- Integrates secret storage for sensitive configuration values
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| vscode.proposed.chatProvider.d.ts | Adds PrepareLanguageModelChatModelOptions interface and provideLanguageModelChatInformation method to support configuration |
| languageModels.ts | Implements configuration handling, secret storage integration, and methods for managing provider groups |
| languageModelsConfiguration.ts | Defines service interface for managing language model provider groups |
| languageModelsConfigurationService.ts | Implements file-based configuration persistence with JSON parsing and range tracking |
| chatModelsWidget.ts | Updates UI to support configure/delete actions and status display for provider groups |
| chatModelsViewModel.ts | Refactors view model to support grouped models with status entries |
| languageModelsConfiguration.test.ts | Adds tests for JSON configuration parsing |
| chatModelsViewModel.test.ts | Updates tests for refactored view model structure |
| languageModels.test.ts | Updates mock service with new methods |
| chat.contribution.ts | Registers new configuration service and contribution |
| chatLanguageModelActions.ts | Adds action for configuring language model groups |
| jsonSchema.ts | Adds secret property to JSON schema interface |
Comments suppressed due to low confidence (3)
src/vs/workbench/contrib/chat/test/browser/languageModelsConfiguration.test.ts:53
- Test assertion is incorrect. On line 52-53, the result is being compared as a single object with
assert.deepStrictEqual(config, { foo: 'bar' }), butresultis an array of parsed provider groups, not a configuration object. This should likely access the first element's configuration property, or the test setup is incorrect.
const config = result;
assert.deepStrictEqual(config, { foo: 'bar' });
src/vs/workbench/contrib/chat/common/languageModels.ts:404
- The SECRET_KEY format string at line 404 uses
$as a literal character, but this could conflict with variable substitution in some contexts. The format'${input:{0}}'might be interpreted as a template literal pattern. Consider using a more distinctive format or escaping the dollar sign to avoid potential parsing issues.
private static SECRET_KEY = '${input:{0}}';
src/vs/workbench/contrib/chat/common/languageModels.ts:962
- The
decodeSecretKeymethod at lines 957-962 uses a simplistic substring extraction that assumes the format${input:KEY}. However, it doesn't validate the expected prefix before extracting, which could lead to incorrect parsing if the string doesn't match the expected format. Add validation to ensure the string starts with the expected prefix before extracting.
private decodeSecretKey(secretInput: unknown): string | undefined {
if (!isString(secretInput)) {
return undefined;
}
return secretInput.substring(secretInput.indexOf(':') + 1, secretInput.length - 1);
}
| export interface ILanguageModelsProviderGroup extends IStringDictionary<unknown> { | ||
| readonly name: string; | ||
| readonly vendor: string; | ||
| readonly range?: IRange; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The interface ILanguageModelsProviderGroup at line 29 extends IStringDictionary<unknown> which allows any string keys, making it difficult to enforce type safety for the required name, vendor, and optional range properties. This design could lead to naming conflicts if configuration properties happen to be named 'name', 'vendor', or 'range'. Consider using a more explicit structure with a separate configuration property to hold the dynamic configuration values.
| export interface ILanguageModelsProviderGroup extends IStringDictionary<unknown> { | |
| readonly name: string; | |
| readonly vendor: string; | |
| readonly range?: IRange; | |
| export interface ILanguageModelsProviderGroup { | |
| readonly name: string; | |
| readonly vendor: string; | |
| readonly range?: IRange; | |
| readonly configuration: IStringDictionary<unknown>; |
| .models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.error-status { | ||
| color: var(--vscode-errorForeground); | ||
| } | ||
|
|
||
| .models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.warning-status { |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CSS classes at lines 124-130 use compound selectors that are very specific (.models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.error-status). These deeply nested selectors make the CSS fragile and hard to maintain. Consider using more semantic class names or reducing specificity to improve maintainability.
| .models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.error-status { | |
| color: var(--vscode-errorForeground); | |
| } | |
| .models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-table-td .model-name-container .model-name .monaco-highlighted-label.warning-status { | |
| .models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-highlighted-label.error-status { | |
| color: var(--vscode-errorForeground); | |
| } | |
| .models-widget .models-table-container .monaco-table-tr.models-status-row .monaco-highlighted-label.warning-status { |
| const config = result[0]; | ||
| assert.strictEqual(config.str, 'value'); | ||
| assert.strictEqual(config.num, 123); | ||
| assert.strictEqual(config.bool, true); | ||
| assert.strictEqual(config.null, null); | ||
| assert.deepStrictEqual(config.arr, [1, 2]); | ||
| assert.deepStrictEqual(config.obj, { nested: 'val' }); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test assertions are accessing properties directly on config object (lines 96-102), but config appears to be the first element of the result array based on line 96. The property access should be from the correct nested structure (e.g., accessing configuration properties from the first group's configuration object).
This issue also appears in the following locations of the same file:
- line 52
| assert.strictEqual(visibleModels[0].model.metadata.name, 'GPT-4'); | ||
| assert.strictEqual(visibleModels[0].model.provider, 'copilot'); | ||
|
|
||
| assert.strictEqual(visibleModels[1].modelEntry.metadata.name, 'GPT-4o'); | ||
| assert.strictEqual(visibleModels[1].modelEntry.vendor, 'copilot'); | ||
| assert.strictEqual(visibleModels[1].model.metadata.name, 'GPT-4o'); | ||
| assert.strictEqual(visibleModels[1].model.provider, 'copilot'); | ||
|
|
||
| assert.strictEqual(visibleModels[2].modelEntry.metadata.name, 'Claude 3'); | ||
| assert.strictEqual(visibleModels[2].modelEntry.vendor, 'anthropic'); | ||
| assert.strictEqual(visibleModels[2].model.metadata.name, 'Claude 3'); | ||
| assert.strictEqual(visibleModels[2].model.provider, 'anthropic'); | ||
|
|
||
| assert.strictEqual(visibleModels[3].modelEntry.metadata.name, 'GPT-3.5 Turbo'); | ||
| assert.strictEqual(visibleModels[3].modelEntry.vendor, 'openai'); | ||
| assert.strictEqual(visibleModels[3].model.metadata.name, 'GPT-3.5 Turbo'); | ||
| assert.strictEqual(visibleModels[3].model.provider, 'openai'); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test assertions at lines 852-862 are accessing model.provider directly as a string (e.g., assert.strictEqual(visibleModels[0].model.provider, 'copilot')), but based on the type definitions, model.provider should be an ILanguageModelProvider object, not a string. These should likely be model.provider.vendor.vendor to access the vendor ID string.
See below for a potential fix:
assert.strictEqual(visibleModels[0].model.provider.vendor.vendor, 'copilot');
assert.strictEqual(visibleModels[1].model.metadata.name, 'GPT-4o');
assert.strictEqual(visibleModels[1].model.provider.vendor.vendor, 'copilot');
assert.strictEqual(visibleModels[2].model.metadata.name, 'Claude 3');
assert.strictEqual(visibleModels[2].model.provider.vendor.vendor, 'anthropic');
assert.strictEqual(visibleModels[3].model.metadata.name, 'GPT-3.5 Turbo');
assert.strictEqual(visibleModels[3].model.provider.vendor.vendor, 'openai');
| if (existing?.[property]) { | ||
| inputBox.value = String(existing?.[property]); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference error: at lines 896-897, the code accesses existing?.[property] which could be undefined, but then calls String(existing?.[property]) on line 897. If existing[property] is null or undefined, this will convert it to the string "null" or "undefined" rather than an empty string, which is likely not the intended behavior for displaying the previous value in an input box.
| if (a.models[0]?.provider.vendor.vendor === 'copilot') { return -1; } | ||
| if (b.models[0]?.provider.vendor.vendor === 'copilot') { return 1; } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition at line 412 checks for copilot vendor and sorts it first with if (a.models[0]?.provider.vendor.vendor === 'copilot'), but if a.models is an empty array (which can happen when there's only a status entry), this will cause a.models[0] to be undefined, potentially leading to incorrect sorting behavior. Add a check to ensure the models array has elements before accessing the first element.
| if (a.models[0]?.provider.vendor.vendor === 'copilot') { return -1; } | |
| if (b.models[0]?.provider.vendor.vendor === 'copilot') { return 1; } | |
| if (a.models.length > 0 && a.models[0]?.provider.vendor.vendor === 'copilot') { return -1; } | |
| if (b.models.length > 0 && b.models[0]?.provider.vendor.vendor === 'copilot') { return 1; } |
| async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void> { | ||
| const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId); | ||
| if (!vendor) { | ||
| throw new Error(`Vendor ${vendorId} not found.`); | ||
| } | ||
|
|
||
| const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration); | ||
| await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup); | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incomplete JSDoc comment: The method addLanguageModelsProviderGroup at lines 740-748 is a public API method but lacks JSDoc documentation explaining its parameters, purpose, and behavior. Given that this is part of the public service interface, proper documentation is essential for maintainability and API consumers.
| return result; | ||
| } | ||
|
|
||
| async updateLanguageModelsProviderGroup(toUpdate: ILanguageModelsProviderGroup): Promise<ILanguageModelsProviderGroup> { |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method signature in updateLanguageModelsProviderGroup only takes one parameter toUpdate, but the documentation comment and method call in languageModels.ts at line 732 suggest it should take two parameters (from and to). The signature should match the interface definition in languageModelsConfiguration.ts line 22 which declares updateLanguageModelsProviderGroup(from: ILanguageModelsProviderGroup, to: ILanguageModelsProviderGroup).
| 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.") | ||
| 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."), | ||
| deprecated: true, | ||
| deprecationMessage: localize('vscode.extension.contributes.languageModels.managementCommand.deprecated', "The managementCommand property is deprecated and will be removed in a future release. Use the new configuration property instead.") |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deprecation message at line 372 states "The managementCommand property is deprecated and will be removed in a future release." However, the code still relies heavily on checking managementCommand in multiple places (e.g., lines 711-714, 768). If this property is truly deprecated, there should be a clear migration path documented, and the deprecation timeline should be specified to help extension authors plan their updates.
| deprecationMessage: localize('vscode.extension.contributes.languageModels.managementCommand.deprecated', "The managementCommand property is deprecated and will be removed in a future release. Use the new configuration property instead.") | |
| deprecationMessage: localize('vscode.extension.contributes.languageModels.managementCommand.deprecated', "The managementCommand property is deprecated and will be removed in a future major release. Extension authors should instead describe all management options in the configuration schema and surface them through the configuration property (for example, using settings-based toggles or options) rather than relying on a dedicated management command.") |
No description provided.