Add per-model configuration support for language models (#302771)

* Add per-model configuration support to LanguageModelsService

* Update chatProvider version to 5 and add configuration schema support for per-model options

* Add per-model configuration support to chat request options

* Revert version number in chatProvider declaration from 5 to 4

* Refactor chat request options to use ILanguageModelChatRequestOptions interface and revert chatProvider version to 4

* Add per-model configuration support in LanguageModelsService and update ActionsColumnRenderer

* Enhance per-model configuration support by merging schema defaults with user config in sendChatRequest

* Add per-model configuration actions and update LanguageModelsService methods

* Optimize model resolution for default vendors by applying per-model configurations directly to resolved models

* Add per-model configuration actions for single model selection in ChatModelsWidget

* Improve label generation for model configuration actions by using propSchema.title and enhancing formatting

* Enhance model configuration action labels to indicate default values in LanguageModelsService

* Add languageModelsService to model action creation for enhanced toolbar actions

* Integrate languageModelsService into model action creation for improved model picker functionality

* Add configuration toolbar actions for models in chat model picker

* Refactor model configuration action handling to utilize languageModelsService for toolbar actions

* Add toolbar actions to model item creation in chat model picker

* Refactor action item rendering to flatten SubmenuActions into a single gear button for context menu access

* Enhance toolbar action interaction by preventing selection on click and updating visibility styles

* Refactor ActionItemRenderer to use a gear button for context menu access and improve toolbar action handling

* Refactor ActionItemRenderer to use inline menu for SubmenuActions and improve context menu handling

* Refactor ActionItemRenderer to use defaultMenuStyles for menu configuration

* Refactor ActionItemRenderer to display a gear button for SubmenuActions that opens an inline menu

* Add submenu support to ActionListItem and update styles for submenu indicator

* Enhance submenu handling in ActionItemRenderer with improved mouse event management and layout adjustments

* Refactor ActionItemRenderer to streamline submenu handling and improve hover interactions

* Prevent hover display for items with submenu actions to avoid conflicts

* Add hardcoded submenu actions for debugging in createModelItem function

* Refactor submenu positioning in ActionList to improve layout and visibility

* Fix submenu positioning to prevent overflow clipping and ensure visibility

* Refactor submenu positioning to append within action list's DOM context and adjust for transform offsets

* Refactor submenu positioning to append within row element and adjust layout for transform offsets

* Enhance submenu structure by adding rowElement to support fixed-position rendering and adjust overflow handling

* Refactor submenu handling to remove rowElement from submenu state and adjust positioning logic for improved layout

* Refactor submenu positioning to use bounding rectangles for accurate placement relative to the row element

* Refactor submenu positioning to append within action list's DOM context for improved layout

* Refactor submenu positioning to append within action-widget container and use fixed coordinates for accurate placement

* Refactor submenu positioning to append directly to the action list's DOM node and adjust placement logic for improved layout

* Refactor ActionList to set position of domNode to relative for accurate submenu positioning

* Remove hardcoded submenu actions from createModelItem and use action's toolbarActions instead

* Prevent hiding submenu when hovering over it to improve user interaction

* Add submenu indicator visibility handling and CSS class for submenu items

* Prevent hiding action widget when focus moves to a submenu

* Always render submenu indicator icon for consistent width and alignment

* Improve focus handling in ActionWidgetService to prevent premature hiding

* Refactor focus handling in ActionWidgetService to improve submenu interaction

* Prevent submenu from being focusable to maintain action widget focus

* Adjust submenu positioning logic to display correctly based on available space

* Add hasActiveSubmenu method to ActionList and update focus handling in ActionWidgetService

* Update hasActiveSubmenu method to account for scheduled submenu visibility

* Adjust submenu positioning to prevent overflow above the action list

* Remove scheduled submenu visibility check from hasActiveSubmenu method

* Enhance submenu positioning logic to dynamically determine expand direction based on available space

* Implement cleanupSubmenu method and update focus handling to ensure proper submenu cleanup on focus loss

* Refactor submenu handling: remove Menu widget and implement direct DOM rendering for submenu items

* Add check icon to submenu items and adjust styling for improved layout

* Immediately clean up submenu on hover change to enhance user experience

* Update submenu styles: increase min-width and adjust padding for better layout

* Refactor hover handling in ActionList: avoid showing hover for submenu items and adjust submenu display logic on hover

* Enhance submenu item rendering: add group label display and adjust styles for improved layout

* Refactor hover handling in ActionList: improve logic to skip re-rendering for active submenus

* Update submenu min-width: increase from 160px to 220px for improved layout

* Update submenu styles: change min-width to max-content for better adaptability

* Update action widget styles: set min-width to 200px for improved layout

* Improve submenu hover handling: prevent hiding when hovering over the same row or submenu

* Update submenu item hover styles: change background and text colors for better visibility

* Fix submenu selector handling: update class name for submenu detection and prevent default mouse down event

* Hide submenu after action execution to improve user experience

* Update submenu item hover styles: change background and text colors for improved visibility

* Refactor LanguageModelsService: update group retrieval logic to use configuration service as the source of truth

* Refactor LanguageModelsService: enhance model configuration handling by removing default properties and managing group updates

* Add model configuration description retrieval to LanguageModelsService

* Refactor LanguageModelsService: update property schema handling to use showInPicker and enhance description formatting

* Add showInPicker property to IJSONSchema and update LanguageModelsService to utilize it

* Refactor LanguageModelsService: update showInPicker handling to improve type safety and simplify value retrieval

* Add showInPicker property to ILanguageModelConfigurationSchemaProperty and update configurationSchema type in ILanguageModelChatMetadata

* Enhance model hover content: include configuration properties marked with showInPicker

* Refactor buildModelPickerItems: pass languageModelsService to createModelItem for improved functionality

* Refactor submenu handling in ActionList: add delay for hiding submenu to improve user experience

* Refactor language model options: rename configuration to modelConfiguration for consistency

* Refactor language model options: rename modelConfiguration to configuration for consistency

* Enhance ChatLanguageModelsDataContribution: add per-model configuration schemas for improved schema validation

* Enhance language model settings management: implement model settings update mechanism and extend interfaces for configuration handling

* Enhance language model integration: add model configuration handling in chat agent requests and update type definitions

* Refactor language model settings handling: remove unused model settings methods and update model configuration retrieval in chat requests

* Refactor language model configuration handling: rename configuration to modelConfiguration for consistency

* Enhance RunSubagentTool: add model configuration retrieval for user-selected model

* Refactor language model configuration: rename 'models' to 'settings' for consistency

* Enhance ActionList: prevent hover display when a submenu is active and adjust submenu positioning to avoid scrollbar overlap

* Enhance ActionList: add description text element and style submenu indicator for better interaction

* Enhance ActionItemRenderer and CSS: adjust submenu indicator positioning and improve hover feedback for submenu items

* Enhance ActionItemRenderer and CSS: wrap description and submenu indicator for improved hover feedback

* Enhance ActionWidget CSS: adjust description group visibility and padding for submenu items

* Refactor ActionItemRenderer: rearrange description group and toolbar for improved layout and hover feedback

* Enhance ActionWidget CSS: adjust padding for submenu indicator for improved layout

* Enhance ActionList hover behavior: update submenu display logic to trigger on description group hover

* Enhance ActionWidget CSS: update submenu indicator visibility on row hover for improved user interaction

* Refactor ActionList: remove submenu check from hover display logic for improved user experience

* Refactor ActionList hover logic: improve submenu visibility handling during mouse movement

* Enhance LanguageModel configuration: add support for enum item labels and descriptions in model picker

* Enhance ActionList submenu rendering: add group headers and descriptions for improved clarity

* Enhance ModelPicker: update label rendering to include model configuration descriptions and re-render on model changes

* Enhance ActionList hover functionality: show hover content in submenu when no submenu actions are present and add CSS styles for hover section

* Enhance ActionList submenu hover styles: adjust padding, font size, and colors for improved readability

* Refactor ActionList submenu rendering: improve separator logic and remove unused header styles

* Enhance ActionList submenu rendering: add group headers with labels and separators for improved organization

* Enhance ActionList submenu hover styles: adjust padding for improved layout

* Refactor ActionList rendering: remove unused description group and submenu indicator styles for cleaner layout

* Enhance ActionList submenu hover styles: adjust padding, font size, and line height for improved readability

* Enhance action list submenu hover styles: adjust padding, font size, and line height for improved readability and layout

* Enhance action list submenu styles: adjust padding for improved layout

* Enhance action list submenu group label styles: increase font size for better visibility

* Update action list submenu group label color for improved visibility

* Enhance ActionList submenu positioning and overflow handling for improved layout and visibility

* Enhance action list item layout: add right padding for improved spacing

* Refactor ActionList submenu: simplify styles and remove unused properties

* Enhance ActionList hover behavior: switch to instant hover display for improved responsiveness

* Enhance ActionList hover behavior: implement hover delay for improved user experience

* Refactor ActionList hover behavior: switch to delayed hover display for improved user experience

* Enhance ModelPickerActionItem: re-render label on model configuration changes

* Enhance ModelPickerWidget: re-register label rendering on language model changes

* Refactor ILanguageModelConfigurationSchemaProperty: rename showInPicker to isVisible for clarity

* Refactor ILanguageModelConfigurationSchemaProperty: rename isVisible to isPrimary for clarity

* Refactor ILanguageModelConfigurationSchemaProperty: rename isPrimary to showInDescription for clarity

* Refactor ILanguageModelConfigurationSchemaProperty: rename showInDescription to showInModelPicker for clarity

* Refactor ILanguageModelConfigurationSchemaProperty: rename showInModelPicker to pinToModelPicker for clarity

* Refactor ILanguageModelConfigurationSchemaProperty: change pinToModelPicker to group for improved clarity

* Refactor LanguageModelConfigurationSchemaProperty and LanguageModelConfigurationSchema: update configurationSchema type and add detailed property definitions

* Refactor LanguageModelConfigurationSchema: simplify schema property definition and enhance documentation

* Refactor LanguageModelConfigurationSchema: enhance documentation for properties and clarify enumItemLabels and group usage

* Refactor ILanguageModelConfigurationSchema: inline schema property definition and enhance documentation for group and enumItemLabels

* Refactor ILanguageModelsService: enhance documentation for getModelConfigurationDescription to clarify its purpose and usage

* Refactor language model configuration handling: replace direct service method calls with utility function for improved clarity and maintainability

* Refactor ActionItemRenderer: remove unused descriptionText property and streamline description handling

* Refactor ActionItemRenderer: streamline description element creation by removing redundant code

* Refactor ActionList: enhance hover content rendering and streamline submenu actions handling

* Refactor ActionListHoverContent: extract group rendering logic into ActionListHoverGroup class

* Refactor ActionListHoverContent and ActionListHoverGroup: enhance event handling and streamline disposables management

* Refactor ActionListHoverGroup and ActionListHoverContent: integrate Menu widget for submenu actions and remove unused CSS styles

* Refactor ActionListHoverGroup and ActionListHoverContent: remove unused Menu widget and enhance submenu item styling

* Enhance ActionList keyboard navigation and accessibility: add support for submenu actions with arrow keys and improve focus handling for submenu items

* Add OpenModelConfigPickerAction and model configuration picker UI component

* Refactor ModelConfigPickerActionItem render method: streamline DOM node creation and class assignment

* Enhance ModelConfigPickerActionItem: add disabled state handling for navigation actions

* Enhance ModelConfigPickerActionItem: add header for submenu actions with group label

* Enhance ModelConfigPickerActionItem: update submenu header to use action label as group title

* Refactor ModelConfigPickerActionItem and ModelPickerActionItem: simplify navigation properties handling and remove unused language models service references

* Enhance ModelConfigPickerActionItem: add navigation-group configuration values display in model picker

* Enhance ModelConfigPickerActionItem and language model interfaces: add support for enum icons in model configuration

* Refactor ModelConfigPickerActionItem and language model interfaces: replace enumIcons with a single icon property for configuration

* Refactor ModelConfigPickerActionItem and language model interfaces: remove icon property from configuration schema

* Refactor ActionList and ActionWidgetService: remove unused submenu actions and related hover logic

* Clarify comment in OpenModelConfigPickerAction: specify that the picker is opened by the ModelConfigPickerActionItem view item on click

* Remove model config picker UI: delete ModelConfigPickerActionItem, remove OpenModelConfigPickerAction and chatModelHasNavigationConfig context key

The config picker dropdown button in the chat input has been removed while keeping the underlying per-model configuration API, settings support, and model management editor intact.

Also fix ILanguageModelConfigurationSchema to not include boolean in properties type (incompatible with IJSONSchema), and add showUnavailableFeatured/showFeatured to IModelPickerDelegate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Refactor LanguageModelConfigurationSchema: adjust properties type definition for clarity

* Refactor LanguageModelsService: merge configuration options and simplify default checks

* Revert newChatViewPane changes and fix code review issues

- Make showUnavailableFeatured/showFeatured optional on IModelPickerDelegate
  (defaults to true), removing the need to change newChatViewPane.ts
- Fix sendChatRequest merge order: caller's configuration takes precedence
  over stored model config
- Remove dead typeof propSchema !== 'boolean' checks after type cleanup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert unrelated changes to chatModelPicker and modelPickerActionItem

These files were modified to add showUnavailableFeatured/showFeatured
which already landed on main separately. Revert to merge-base versions
to keep the diff clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert unrelated changes to chatInputPart.ts

Revert to merge-base version — the diff was from main changes
(delegation picker, showUnavailableFeatured/showFeatured) that
will come in on merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Sandeep Somavarapu
2026-03-18 14:35:05 +01:00
committed by GitHub
parent f7b1318461
commit e8df0398a5
16 changed files with 625 additions and 28 deletions

View File

@@ -63,7 +63,7 @@ import { ChatResponseClearToPreviousToolInvocationReason, IChatContentInlineRefe
import { IChatNewSessionRequest, IChatSessionItem, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js';
import { IChatRequestVariableValue } from '../../contrib/chat/common/attachments/chatVariables.js';
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
import { IChatMessage, IChatResponsePart, ILanguageModelChatInfoOptions, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js';
import { IChatMessage, IChatResponsePart, ILanguageModelChatInfoOptions, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatRequestOptions, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js';
import { IPreparedToolInvocation, IStreamedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/tools/languageModelToolsService.js';
import { IPromptFileContext, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js';
@@ -1394,7 +1394,7 @@ export interface MainThreadLanguageModelsShape extends IDisposable {
export interface ExtHostLanguageModelsShape {
$provideLanguageModelChatInfo(vendor: string, options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise<ILanguageModelChatMetadataAndIdentifier[]>;
$updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void;
$startChatRequest(modelId: string, requestId: number, from: ExtensionIdentifier | undefined, messages: SerializableObjectWithBuffers<IChatMessage[]>, options: { [name: string]: any }, token: CancellationToken): Promise<void>;
$startChatRequest(modelId: string, requestId: number, from: ExtensionIdentifier | undefined, messages: SerializableObjectWithBuffers<IChatMessage[]>, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<void>;
$acceptResponsePart(requestId: number, chunk: SerializableObjectWithBuffers<IChatResponsePart | IChatResponsePart[]>): Promise<void>;
$acceptResponseDone(requestId: number, error: SerializedError | undefined): Promise<void>;
$provideTokenLength(modelId: string, value: string | IChatMessage, token: CancellationToken): Promise<number>;

View File

@@ -675,6 +675,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
request,
location,
model,
request.modelConfiguration,
this.getDiagnosticsWhenEnabled(detector.extension),
tools,
detector.extension,
@@ -776,6 +777,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
request,
location,
model,
request.modelConfiguration,
this.getDiagnosticsWhenEnabled(agent.extension),
tools,
agent.extension,

View File

@@ -640,7 +640,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
return {};
}
const chatRequest = typeConvert.ChatAgentRequest.to(request, undefined, await this.getModelForRequest(request, entry.sessionObj.extension), [], new Map(), entry.sessionObj.extension, this._logService);
const chatRequest = typeConvert.ChatAgentRequest.to(request, undefined, await this.getModelForRequest(request, entry.sessionObj.extension), request.modelConfiguration, [], new Map(), entry.sessionObj.extension, this._logService);
const stream = entry.sessionObj.getActiveRequestStream(request);
await entry.sessionObj.session.requestHandler(chatRequest, { history, yieldRequested: false }, stream.apiObject, token);

View File

@@ -11,13 +11,14 @@ import { SerializedError, transformErrorForSerialization, transformErrorFromSeri
import { Emitter, Event } from '../../../base/common/event.js';
import { Iterable } from '../../../base/common/iterator.js';
import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { IJSONSchema } from '../../../base/common/jsonSchema.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../platform/log/common/log.js';
import { Progress } from '../../../platform/progress/common/progress.js';
import { IChatMessage, IChatResponsePart, ILanguageModelChatInfoOptions, ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../../contrib/chat/common/languageModels.js';
import { IChatMessage, IChatResponsePart, ILanguageModelChatInfoOptions, ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatRequestOptions } from '../../contrib/chat/common/languageModels.js';
import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../contrib/chat/common/widget/input/modelPickerWidget.js';
import { INTERNAL_AUTH_PROVIDER_PREFIX } from '../../services/authentication/common/authentication.js';
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
@@ -229,6 +230,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape {
isUserSelectable: m.isUserSelectable,
statusIcon: m.statusIcon,
targetChatSessionType: m.targetChatSessionType,
configurationSchema: m.configurationSchema as IJSONSchema | undefined,
modelPickerCategory: m.category ?? DEFAULT_MODEL_PICKER_CATEGORY,
capabilities: m.capabilities ? {
vision: m.capabilities.imageInput,
@@ -258,7 +260,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape {
return modelMetadataAndIdentifier;
}
async $startChatRequest(modelId: string, requestId: number, from: ExtensionIdentifier | undefined, messages: SerializableObjectWithBuffers<IChatMessage[]>, options: vscode.LanguageModelChatRequestOptions, token: CancellationToken): Promise<void> {
async $startChatRequest(modelId: string, requestId: number, from: ExtensionIdentifier | undefined, messages: SerializableObjectWithBuffers<IChatMessage[]>, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<void> {
const knownModel = this._localModels.get(modelId);
if (!knownModel) {
throw new Error('Model not found');
@@ -320,7 +322,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape {
knownModel.info,
messages.value.map(typeConvert.LanguageModelChatMessage2.to),
// todo@connor4312: move `core` -> `undefined` after 1.111 Insiders is out
{ ...options, modelOptions: options.modelOptions ?? {}, requestInitiator: from ? ExtensionIdentifier.toKey(from) : 'core', toolMode: options.toolMode ?? extHostTypes.LanguageModelChatToolMode.Auto },
{ ...options, modelOptions: options.modelOptions ?? {}, modelConfiguration: options.configuration, requestInitiator: from ? ExtensionIdentifier.toKey(from) : 'core', toolMode: options.toolMode ?? extHostTypes.LanguageModelChatToolMode.Auto },
progress,
token
);

View File

@@ -6,6 +6,7 @@
import type * as vscode from 'vscode';
import { asArray, coalesce, isNonEmptyArray } from '../../../base/common/arrays.js';
import { VSBuffer, encodeBase64 } from '../../../base/common/buffer.js';
import { IStringDictionary } from '../../../base/common/collections.js';
import { IDataTransferFile, IDataTransferItem, UriList } from '../../../base/common/dataTransfer.js';
import { createSingleCallFunction } from '../../../base/common/functional.js';
import * as htmlContent from '../../../base/common/htmlContent.js';
@@ -3403,7 +3404,7 @@ export namespace ChatResponsePart {
}
export namespace ChatAgentRequest {
export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: Map<vscode.LanguageModelToolInformation, boolean>, extension: IRelaxedExtensionDescription, logService: ILogService): vscode.ChatRequest {
export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, modelConfiguration: IStringDictionary<unknown> | undefined, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: Map<vscode.LanguageModelToolInformation, boolean>, extension: IRelaxedExtensionDescription, logService: ILogService): vscode.ChatRequest {
const toolReferences: IChatRequestVariableEntry[] = [];
const variableReferences: IChatRequestVariableEntry[] = [];
@@ -3438,6 +3439,7 @@ export namespace ChatAgentRequest {
toolInvocationToken: Object.freeze<IToolInvocationContext>({ sessionResource: request.sessionResource }) as never,
tools,
model,
modelConfiguration,
editedFileEvents: request.editedFileEvents,
modeInstructions: request.modeInstructions?.content,
modeInstructions2: ChatRequestModeInstructions.to(request.modeInstructions),

View File

@@ -792,6 +792,21 @@ class ActionsColumnRenderer extends ModelsTableColumnRenderer<IActionsColumnTemp
}
override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IActionsColumnTemplateData): void {
const configActions = this.languageModelsService.getModelConfigurationActions(entry.model.identifier);
if (configActions.length === 0 && !entry.model.metadata.configurationSchema) {
return;
}
const secondaryActions: IAction[] = [...configActions];
// Always add "Configure..." as fallback for complex properties
secondaryActions.push(toAction({
id: 'configureModel',
label: localize('models.configureModel', 'Configure...'),
run: () => this.languageModelsService.configureModel(entry.model.identifier)
}));
templateData.actionBar.setActions([], secondaryActions);
}
}
@@ -1181,6 +1196,15 @@ export class ChatModelsWidget extends Disposable {
run: () => this.viewModel.setModelsVisibility(selectedModelEntries, true)
}));
// Show per-model configuration actions for a single model
if (selectedModelEntries.length === 1) {
const configActions = this.languageModelsService.getModelConfigurationActions(selectedModelEntries[0].model.identifier);
if (configActions.length) {
actions.push(new Separator());
actions.push(...configActions);
}
}
// Show configure action if all models are from the same group
configureGroup = selectedModelEntries[0].model.provider.group.name;
configureVendor = selectedModelEntries[0].model.provider.vendor;

View File

@@ -8,6 +8,7 @@ import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { Mutable } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
@@ -315,6 +316,32 @@ export class ChatLanguageModelsDataContribution extends Disposable implements IW
private updateSchema(registry: IJSONContributionRegistry): void {
const vendors = this.languageModelsService.getVendors();
// Build per-model configuration schemas
const modelSchemas: IJSONSchema[] = [];
const modelIds = this.languageModelsService.getLanguageModelIds();
for (const modelId of modelIds) {
const metadata = this.languageModelsService.lookupLanguageModel(modelId);
if (metadata?.configurationSchema) {
modelSchemas.push({
if: {
properties: {
vendor: { const: metadata.vendor }
}
},
then: {
properties: {
settings: {
type: 'object',
properties: {
[metadata.id]: metadata.configurationSchema
}
}
}
}
});
}
}
const schema: IJSONSchema = {
type: 'array',
items: {
@@ -323,16 +350,23 @@ export class ChatLanguageModelsDataContribution extends Disposable implements IW
type: 'string',
enum: vendors.map(v => v.vendor)
},
name: { type: 'string' }
name: { type: 'string' },
settings: {
type: 'object',
description: localize('settings.perModelConfig', "Per-model settings"),
}
},
allOf: vendors.map(vendor => ({
if: {
properties: {
vendor: { const: vendor.vendor }
}
},
then: vendor.configuration
})),
allOf: [
...vendors.map(vendor => ({
if: {
properties: {
vendor: { const: vendor.vendor }
}
},
then: vendor.configuration
})),
...modelSchemas
],
required: ['vendor', 'name']
}
};

View File

@@ -1129,6 +1129,7 @@ export class ChatService extends Disposable implements IChatService {
acceptedConfirmationData: options?.acceptedConfirmationData,
rejectedConfirmationData: options?.rejectedConfirmationData,
userSelectedModelId: options?.userSelectedModelId,
modelConfiguration: options?.userSelectedModelId ? this.languageModelsService.getModelConfiguration(options.userSelectedModelId) : undefined,
userSelectedTools: options?.userSelectedTools?.get(),
modeInstructions: options?.modeInfo?.modeInstructions,
permissionLevel: options?.modeInfo?.permissionLevel,

View File

@@ -18,6 +18,7 @@ import { equals } from '../../../../base/common/objects.js';
import Severity from '../../../../base/common/severity.js';
import { format, isFalsyOrWhitespace } from '../../../../base/common/strings.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { IAction, SubmenuAction } from '../../../../base/common/actions.js';
import { isObject, isString } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { generateUuid } from '../../../../base/common/uuid.js';
@@ -169,6 +170,17 @@ export type IChatResponsePart = IChatResponseTextPart | IChatResponseToolUsePart
export type IExtendedChatResponsePart = IChatResponsePullRequestPart;
export interface ILanguageModelConfigurationSchema extends IJSONSchema {
properties?: {
[key: string]: IJSONSchema & {
/** When set to `'navigation'`, the property is shown as a primary action in the model picker. */
group?: string;
/** Labels for enum values. If provided, these are shown instead of the raw enum values. */
enumItemLabels?: string[];
};
};
}
export interface ILanguageModelChatMetadata {
readonly extension: ExtensionIdentifier;
@@ -204,6 +216,11 @@ export interface ILanguageModelChatMetadata {
* when the user is in a session matching this type.
*/
readonly targetChatSessionType?: string;
/**
* An optional JSON schema describing the per-model configuration options.
* Used to validate user-provided per-model configuration in `chatLanguageModels.json`.
*/
readonly configurationSchema?: ILanguageModelConfigurationSchema;
}
export namespace ILanguageModelChatMetadata {
@@ -263,13 +280,13 @@ export async function getTextResponseFromStream(response: ILanguageModelChatResp
export interface ILanguageModelChatProvider {
readonly onDidChange: Event<void>;
provideLanguageModelChatInfo(options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise<ILanguageModelChatMetadataAndIdentifier[]>;
sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: { [name: string]: unknown }, token: CancellationToken): Promise<ILanguageModelChatResponse>;
sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;
}
export interface ILanguageModelChat {
metadata: ILanguageModelChatMetadata;
sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: { [name: string]: unknown }, token: CancellationToken): Promise<ILanguageModelChatResponse>;
sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise<number>;
}
@@ -313,6 +330,13 @@ export interface ILanguageModelChatInfoOptions {
readonly configuration?: IStringDictionary<unknown>;
}
export interface ILanguageModelChatRequestOptions {
readonly modelOptions?: IStringDictionary<unknown>;
readonly configuration?: IStringDictionary<unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly [name: string]: any;
}
export interface ILanguageModelsGroup {
readonly group?: ILanguageModelsProviderGroup;
readonly modelIdentifiers: string[];
@@ -354,17 +378,42 @@ export interface ILanguageModelsService {
deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse>;
sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;
/**
* Returns the resolved per-model configuration for the given model identifier.
* Includes schema defaults with user overrides applied on top.
* Returns undefined if the model has no configuration schema and no user config.
*/
getModelConfiguration(modelId: string): IStringDictionary<unknown> | undefined;
/**
* Updates the per-model configuration for the given model.
* Merges the provided values into the existing configuration.
*/
setModelConfiguration(modelId: string, values: IStringDictionary<unknown>): Promise<void>;
/**
* Returns actions for configuring the given model based on its configuration schema.
* For enum properties, returns submenu actions with checkable values.
* Returns an empty array if the model has no configuration schema.
*/
getModelConfigurationActions(modelId: string): IAction[];
addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void>;
removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void>;
configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise<void>;
/**
* Opens the language models configuration file and navigates to
* or creates the per-model configuration for the given model.
*/
configureModel(modelId: string): Promise<void>;
migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise<void>;
/**
@@ -531,6 +580,7 @@ export class LanguageModelsService implements ILanguageModelsService {
private readonly _modelCache = new Map<string, ILanguageModelChatMetadata>();
private readonly _resolveLMSequencer = new SequencerByKey<string>();
private _modelPickerUserPreferences: IStringDictionary<boolean> = {};
private readonly _modelConfigurations = new Map<string, IStringDictionary<unknown>>();
private readonly _hasUserSelectableModels: IContextKey<boolean>;
private readonly _onLanguageModelChange = this._store.add(new Emitter<string>());
@@ -821,11 +871,29 @@ export class LanguageModelsService implements ILanguageModelsService {
}
const groups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
const perModelConfigurations = new Map<string, IStringDictionary<unknown>>();
for (const group of groups) {
if (group.vendor !== vendorId) {
continue;
}
// For the default vendor, groups that only have per-model config
// should not trigger a separate model resolution call.
// Instead, apply the per-model config to the already-resolved models.
if (vendor.isDefault && !vendor.configuration) {
if (group.settings) {
for (const model of allModels) {
const modelConfig = group.settings[model.metadata.id];
if (modelConfig) {
// Store raw config (without resolving secrets) to avoid leaking secrets on persist
perModelConfigurations.set(model.identifier, { ...modelConfig });
}
}
}
languageModelsGroups.push({ group, modelIdentifiers: [] });
continue;
}
const configuration = await this._resolveConfiguration(group, vendor.configuration);
try {
@@ -834,6 +902,17 @@ export class LanguageModelsService implements ILanguageModelsService {
allModels.push(...models);
languageModelsGroups.push({ group, modelIdentifiers: models.map(m => m.identifier) });
}
// Collect per-model configurations from the group
if (group.settings) {
for (const model of models) {
const modelConfig = group.settings[model.metadata.id];
if (modelConfig) {
// Store raw config (without resolving secrets) to avoid leaking secrets on persist
perModelConfigurations.set(model.identifier, { ...modelConfig });
}
}
}
} catch (error) {
languageModelsGroups.push({
group,
@@ -861,6 +940,14 @@ export class LanguageModelsService implements ILanguageModelsService {
this._logService.trace(`[LM] Resolved language models for vendor ${vendorId}`, allModels);
hasChanges = hasChanges || oldModels.size > 0;
// Update per-model configurations for this vendor
this._clearModelConfigurations(vendorId);
for (const [identifier, config] of perModelConfigurations) {
if (this._modelCache.has(identifier)) {
this._modelConfigurations.set(identifier, config);
}
}
if (hasChanges) {
this._onLanguageModelChange.fire(vendorId);
} else {
@@ -926,13 +1013,41 @@ export class LanguageModelsService implements ILanguageModelsService {
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse> {
const provider = this._providers.get(this._modelCache.get(modelId)?.vendor || '');
async sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse> {
const metadata = this._modelCache.get(modelId);
const provider = this._providers.get(metadata?.vendor || '');
if (!provider) {
throw new Error(`Chat provider for model ${modelId} is not registered.`);
}
return provider.sendChatRequest(modelId, messages, from, options, token);
const configuration = this.getModelConfiguration(modelId);
const mergedOptions = configuration ? { ...options, configuration: { ...configuration, ...options.configuration } } : options;
return provider.sendChatRequest(modelId, messages, from, mergedOptions, token);
}
private _resolveModelConfigurationWithDefaults(modelId: string, metadata: ILanguageModelChatMetadata | undefined): IStringDictionary<unknown> | undefined {
const userConfig = this._modelConfigurations.get(modelId);
const schema = metadata?.configurationSchema;
if (!schema?.properties && !userConfig) {
return undefined;
}
// Start with schema defaults
const defaults: IStringDictionary<unknown> = {};
if (schema?.properties) {
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (propSchema.default !== undefined) {
defaults[key] = propSchema.default;
}
}
}
if (!userConfig && Object.keys(defaults).length === 0) {
return undefined;
}
// User config overrides defaults
return { ...defaults, ...userConfig };
}
computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number> {
@@ -947,6 +1062,124 @@ export class LanguageModelsService implements ILanguageModelsService {
return provider.provideTokenCount(modelId, message, token);
}
getModelConfiguration(modelId: string): IStringDictionary<unknown> | undefined {
const metadata = this._modelCache.get(modelId);
return this._resolveModelConfigurationWithDefaults(modelId, metadata);
}
async setModelConfiguration(modelId: string, values: IStringDictionary<unknown>): Promise<void> {
const metadata = this._modelCache.get(modelId);
if (!metadata) {
return;
}
// Find the group from the configuration service (source of truth)
const allGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
let group: ILanguageModelsProviderGroup | undefined;
// First try to find a group that already has config for this model
group = allGroups.find(g => g.vendor === metadata.vendor && g.settings?.[metadata.id] !== undefined);
// If not found, find any group for this vendor
if (!group) {
group = allGroups.find(g => g.vendor === metadata.vendor);
}
// Merge new values into existing config, removing properties set to their schema default
const existingConfig = this._modelConfigurations.get(modelId) ?? {};
const updatedConfig = { ...existingConfig, ...values };
const schema = metadata.configurationSchema;
if (schema?.properties) {
for (const [key, value] of Object.entries(updatedConfig)) {
const propSchema = schema.properties[key];
if (propSchema?.default !== undefined && propSchema.default === value) {
delete updatedConfig[key];
}
}
}
if (group) {
const existingSettings = (group.settings as IStringDictionary<IStringDictionary<unknown>> | undefined) ?? {};
let updatedSettings: IStringDictionary<IStringDictionary<unknown>>;
if (Object.keys(updatedConfig).length === 0) {
updatedSettings = { ...existingSettings };
delete updatedSettings[metadata.id];
} else {
updatedSettings = { ...existingSettings, [metadata.id]: updatedConfig };
}
const updatedGroup: ILanguageModelsProviderGroup = {
...group,
settings: Object.keys(updatedSettings).length > 0 ? updatedSettings : undefined
};
if (!updatedGroup.settings && Object.keys(updatedGroup).filter(k => k !== 'name' && k !== 'vendor' && k !== 'range' && k !== 'settings').length === 0) {
// Remove the group entirely if it only had model config
await this._languageModelsConfigurationService.removeLanguageModelsProviderGroup(group);
} else {
await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(group, updatedGroup);
}
} else if (Object.keys(updatedConfig).length > 0) {
// Only create a new group if there's non-default config
const vendor = this.getVendors().find(v => v.vendor === metadata.vendor);
if (!vendor) {
return;
}
const newGroup: ILanguageModelsProviderGroup = {
name: vendor.displayName,
vendor: metadata.vendor,
settings: { [metadata.id]: updatedConfig }
};
await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(newGroup);
}
// Update the in-memory cache
if (Object.keys(updatedConfig).length > 0) {
this._modelConfigurations.set(modelId, updatedConfig);
} else {
this._modelConfigurations.delete(modelId);
}
}
getModelConfigurationActions(modelId: string): IAction[] {
const metadata = this._modelCache.get(modelId);
const schema = metadata?.configurationSchema;
if (!schema?.properties) {
return [];
}
const actions: IAction[] = [];
const currentConfig = this._modelConfigurations.get(modelId) ?? {};
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (!propSchema.enum || !Array.isArray(propSchema.enum)) {
continue;
}
const currentValue = currentConfig[key] ?? propSchema.default;
const label = (typeof propSchema.title === 'string' ? propSchema.title : undefined)
?? key.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/^./, s => s.toUpperCase());
const defaultValue = propSchema.default;
const enumItemLabels = propSchema.enumItemLabels;
const enumDescriptions = propSchema.enumDescriptions;
const enumActions: IAction[] = propSchema.enum.map((value: unknown, index: number) => {
const itemLabel = enumItemLabels?.[index] ?? String(value);
const displayLabel = value === defaultValue ? localize('models.enumDefault', "{0} (default)", itemLabel) : itemLabel;
const tooltip = enumDescriptions?.[index] ?? '';
return {
id: `configureModel.${key}.${value}`,
label: displayLabel,
class: undefined,
enabled: true,
tooltip,
checked: currentValue === value,
run: () => this.setModelConfiguration(modelId, { [key]: value })
};
});
actions.push(new SubmenuAction(`configureModel.${key}`, label, enumActions));
}
return actions;
}
async configureLanguageModelsProviderGroup(vendorId: string, providerGroupName?: string): Promise<void> {
const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
@@ -992,6 +1225,63 @@ export class LanguageModelsService implements ILanguageModelsService {
}
}
async configureModel(modelId: string): Promise<void> {
const metadata = this._modelCache.get(modelId);
if (!metadata || !metadata.configurationSchema) {
return;
}
// Find the group that contains this model
const vendorGroups = this._modelsGroups.get(metadata.vendor);
let group: ILanguageModelsProviderGroup | undefined;
if (vendorGroups) {
for (const vg of vendorGroups) {
if (vg.modelIdentifiers.includes(modelId) && vg.group) {
group = vg.group;
break;
}
}
}
// If the model doesn't belong to any configured group, create one
if (!group) {
const vendor = this.getVendors().find(v => v.vendor === metadata.vendor);
if (!vendor) {
return;
}
const groupName = vendor.displayName;
const newGroup: ILanguageModelsProviderGroup = { name: groupName, vendor: metadata.vendor, settings: { [metadata.id]: {} } };
group = await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(newGroup);
await this._resolveAllLanguageModels(metadata.vendor, true);
}
// Generate a snippet for the model's configuration schema
const snippet = this._getModelConfigurationSnippet(metadata.id, metadata.configurationSchema);
await this._languageModelsConfigurationService.configureLanguageModels({ group, snippet });
}
private _getModelConfigurationSnippet(modelId: string, schema: ILanguageModelConfigurationSchema): string {
const properties: string[] = [];
if (schema.properties) {
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (propSchema.defaultSnippets?.[0]) {
const snippet = propSchema.defaultSnippets[0];
let bodyText = snippet.bodyText ?? JSON.stringify(snippet.body, null, '\t\t\t');
bodyText = bodyText.replace(/"(\^[^"]*)"/g, (_, value) => value.substring(1));
properties.push(`\t\t\t"${key}": ${bodyText}`);
} else if (propSchema.default !== undefined) {
properties.push(`\t\t\t"${key}": ${JSON.stringify(propSchema.default)}`);
} else {
properties.push(`\t\t\t"${key}": $\{${key}\}`);
}
}
}
const modelContent = properties.length > 0
? `{\n${properties.join(',\n')}\n\t\t}`
: '{\n\t\t\t$0\n\t\t}';
return `"settings": {\n\t\t"${modelId}": ${modelContent}\n\t}`;
}
async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void> {
const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
if (!vendor) {
@@ -1292,6 +1582,14 @@ export class LanguageModelsService implements ILanguageModelsService {
return removed;
}
private _clearModelConfigurations(vendor: string): void {
for (const [id] of this._modelConfigurations) {
if (this._modelCache.get(id)?.vendor === vendor || id.startsWith(`${vendor}/`)) {
this._modelConfigurations.delete(id);
}
}
}
private async _resolveConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise<IStringDictionary<unknown>> {
if (!schema) {
return {};
@@ -1299,7 +1597,7 @@ export class LanguageModelsService implements ILanguageModelsService {
const result: IStringDictionary<unknown> = {};
for (const key in group) {
if (key === 'vendor' || key === 'name' || key === 'range') {
if (key === 'vendor' || key === 'name' || key === 'range' || key === 'settings') {
continue;
}
let value = group[key];

View File

@@ -38,4 +38,5 @@ export interface ILanguageModelsProviderGroup extends IStringDictionary<unknown>
readonly name: string;
readonly vendor: string;
readonly range?: IRange;
readonly settings?: IStringDictionary<IStringDictionary<unknown>>;
}

View File

@@ -5,6 +5,7 @@
import { findLast } from '../../../../../base/common/arraysFind.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { IStringDictionary } from '../../../../../base/common/collections.js';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
import { Iterable } from '../../../../../base/common/iterator.js';
@@ -147,6 +148,7 @@ export interface IChatAgentRequest {
acceptedConfirmationData?: unknown[];
rejectedConfirmationData?: unknown[];
userSelectedModelId?: string;
modelConfiguration?: IStringDictionary<unknown>;
userSelectedTools?: UserSelectedTools;
modeInstructions?: IChatRequestModeInstructions;
editedFileEvents?: IChatAgentEditedFileEvent[];

View File

@@ -289,6 +289,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
subAgentInvocationId: invocation.callId,
subAgentName: subAgentName,
userSelectedModelId: modeModelId,
modelConfiguration: modeModelId ? this.languageModelsService.getModelConfiguration(modeModelId) : undefined,
userSelectedTools: modeTools,
modeInstructions,
parentRequestId: invocation.chatRequestId,

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { IAction } from '../../../../../../base/common/actions.js';
import { Emitter, Event } from '../../../../../../base/common/event.js';
import { IDisposable } from '../../../../../../base/common/lifecycle.js';
import { observableValue } from '../../../../../../base/common/observable.js';
@@ -123,9 +124,23 @@ class MockLanguageModelsService implements ILanguageModelsService {
throw new Error('Method not implemented.');
}
getModelConfiguration(_modelId: string): IStringDictionary<unknown> | undefined {
return undefined;
}
async setModelConfiguration(_modelId: string, _values: IStringDictionary<unknown>): Promise<void> {
}
getModelConfigurationActions(_modelId: string): IAction[] {
return [];
}
async configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise<void> {
}
async configureModel(_modelId: string): Promise<void> {
}
async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void> {
}

View File

@@ -993,3 +993,153 @@ suite('LanguageModels - Vendor Change Events', function () {
assert.strictEqual(eventFired, false, 'Should not fire event when vendor list is unchanged');
});
});
suite('LanguageModels - Per-Model Configuration', function () {
let languageModelsService: LanguageModelsService;
const disposables = new DisposableStore();
let receivedOptions: { [name: string]: unknown } | undefined;
setup(async function () {
receivedOptions = undefined;
languageModelsService = new LanguageModelsService(
new class extends mock<IExtensionService>() {
override activateByEvent() {
return Promise.resolve();
}
},
new NullLogService(),
new TestStorageService(),
new MockContextKeyService(),
new class extends mock<ILanguageModelsConfigurationService>() {
override onDidChangeLanguageModelGroups = Event.None;
override getLanguageModelsProviderGroups() {
return [{
vendor: 'config-vendor',
name: 'default',
settings: {
'model-a': { temperature: 0.7, reasoningEffort: 'high' },
'model-b': { temperature: 0.2 }
}
}];
}
},
new class extends mock<IQuickInputService>() { },
new TestSecretStorageService(),
new class extends mock<IProductService>() { override readonly version = '1.100.0'; },
new class extends mock<IRequestService>() { },
);
languageModelsService.deltaLanguageModelChatProviderDescriptors([
{ vendor: 'config-vendor', displayName: 'Config Vendor', configuration: undefined, managementCommand: undefined, when: undefined }
], []);
disposables.add(languageModelsService.registerLanguageModelProvider('config-vendor', {
onDidChange: Event.None,
provideLanguageModelChatInfo: async (options) => {
if (options.group) {
return [{
metadata: {
extension: nullExtensionDescription.identifier,
name: 'Model A',
vendor: 'config-vendor',
family: 'family-a',
version: '1.0',
id: 'model-a',
maxInputTokens: 100,
maxOutputTokens: 100,
modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY,
isDefaultForLocation: {},
configurationSchema: {
type: 'object',
properties: {
temperature: { type: 'number', default: 0.5 },
reasoningEffort: { type: 'string', default: 'medium' },
maxTokens: { type: 'number', default: 4096 }
}
}
} satisfies ILanguageModelChatMetadata,
identifier: 'config-vendor/default/model-a'
}, {
metadata: {
extension: nullExtensionDescription.identifier,
name: 'Model B',
vendor: 'config-vendor',
family: 'family-b',
version: '1.0',
id: 'model-b',
maxInputTokens: 100,
maxOutputTokens: 100,
modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY,
isDefaultForLocation: {}
} satisfies ILanguageModelChatMetadata,
identifier: 'config-vendor/default/model-b'
}];
}
return [];
},
sendChatRequest: async (_modelId, _messages, _from, options) => {
receivedOptions = options;
const defer = new DeferredPromise();
const stream = new AsyncIterableSource<IChatResponsePart>();
stream.resolve();
defer.complete(undefined);
return { stream: stream.asyncIterable, result: defer.p };
},
provideTokenCount: async () => { throw new Error(); }
}));
await languageModelsService.selectLanguageModels({});
});
teardown(function () {
languageModelsService.dispose();
disposables.clear();
});
ensureNoDisposablesAreLeakedInTestSuite();
test('getModelConfiguration returns per-model config from group', function () {
const configA = languageModelsService.getModelConfiguration('config-vendor/default/model-a');
assert.deepStrictEqual(configA, { temperature: 0.7, reasoningEffort: 'high', maxTokens: 4096 });
const configB = languageModelsService.getModelConfiguration('config-vendor/default/model-b');
assert.deepStrictEqual(configB, { temperature: 0.2 });
});
test('getModelConfiguration returns undefined for unknown model', function () {
const config = languageModelsService.getModelConfiguration('config-vendor/default/model-c');
assert.strictEqual(config, undefined);
});
test('sendChatRequest merges schema defaults with user config', async function () {
const cts = disposables.add(new CancellationTokenSource());
const request = await languageModelsService.sendChatRequest(
'config-vendor/default/model-a',
nullExtensionDescription.identifier,
[{ role: ChatMessageRole.User, content: [{ type: 'text', value: 'hello' }] }],
{},
cts.token
);
await request.result;
// User config overrides defaults: temperature=0.7 (not 0.5), reasoningEffort='high' (not 'medium')
// Schema default maxTokens=4096 is included since user didn't override it
assert.deepStrictEqual(receivedOptions, { configuration: { temperature: 0.7, reasoningEffort: 'high', maxTokens: 4096 } });
});
test('sendChatRequest passes user config when model has no schema', async function () {
const cts = disposables.add(new CancellationTokenSource());
const request = await languageModelsService.sendChatRequest(
'config-vendor/default/model-b',
nullExtensionDescription.identifier,
[{ role: ChatMessageRole.User, content: [{ type: 'text', value: 'hello' }] }],
{},
cts.token
);
await request.result;
assert.deepStrictEqual(receivedOptions, { configuration: { temperature: 0.2 } });
});
});

View File

@@ -8,8 +8,9 @@ import { IStringDictionary } from '../../../../../base/common/collections.js';
import { Event } from '../../../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
import { observableValue } from '../../../../../base/common/observable.js';
import { IAction } from '../../../../../base/common/actions.js';
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
import { IChatMessage, IModelsControlManifest, ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatProvider, ILanguageModelChatResponse, ILanguageModelChatSelector, ILanguageModelProviderDescriptor, ILanguageModelsGroup, ILanguageModelsService, IUserFriendlyLanguageModel } from '../../common/languageModels.js';
import { IChatMessage, IModelsControlManifest, ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatProvider, ILanguageModelChatRequestOptions, ILanguageModelChatResponse, ILanguageModelChatSelector, ILanguageModelProviderDescriptor, ILanguageModelsGroup, ILanguageModelsService, IUserFriendlyLanguageModel } from '../../common/languageModels.js';
import { ILanguageModelsProviderGroup } from '../../common/languageModelsConfiguration.js';
export class NullLanguageModelsService implements ILanguageModelsService {
@@ -66,8 +67,7 @@ export class NullLanguageModelsService implements ILanguageModelsService {
return [];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendChatRequest(identifier: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<ILanguageModelChatResponse> {
sendChatRequest(identifier: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse> {
throw new Error('Method not implemented.');
}
@@ -75,10 +75,24 @@ export class NullLanguageModelsService implements ILanguageModelsService {
throw new Error('Method not implemented.');
}
getModelConfiguration(_modelId: string): IStringDictionary<unknown> | undefined {
return undefined;
}
async setModelConfiguration(_modelId: string, _values: IStringDictionary<unknown>): Promise<void> {
}
getModelConfigurationActions(_modelId: string): IAction[] {
return [];
}
async configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise<void> {
}
async configureModel(_modelId: string): Promise<void> {
}
async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void> {
}

View File

@@ -17,6 +17,15 @@ declare module 'vscode' {
* `undefined` if the request was initiated by other functionality in the editor.
*/
readonly requestInitiator: string;
/**
* Per-model configuration provided by the user. This contains values configured
* in the user's language models configuration file, validated against the model's
* {@linkcode LanguageModelChatInformation.configurationSchema configurationSchema}.
*/
readonly modelConfiguration?: {
readonly [key: string]: any;
};
}
/**
@@ -67,6 +76,14 @@ declare module 'vscode' {
readonly statusIcon?: ThemeIcon;
/**
* An optional JSON schema describing the configuration options for this model.
* When set, users can specify per-model configuration in their language models
* configuration file. The configured values are merged into the request options
* when sending chat requests to this model.
*/
readonly configurationSchema?: LanguageModelConfigurationSchema;
/**
* When set, this model is only shown in the model picker for the specified chat session type.
* Models with this property are excluded from the general model picker and only appear
@@ -98,6 +115,28 @@ declare module 'vscode' {
export type LanguageModelResponsePart2 = LanguageModelResponsePart | LanguageModelDataPart | LanguageModelThinkingPart;
/**
* A [JSON Schema](https://json-schema.org) describing configuration options for a language model.
* Each property in `properties` defines a configurable option using standard JSON Schema fields
* plus additional display hints.
*/
export type LanguageModelConfigurationSchema = {
readonly properties?: {
readonly [key: string]: Record<string, any> & {
/**
* Human-readable labels for enum values, shown instead of the raw values.
* Must have the same length and order as `enum`.
*/
readonly enumItemLabels?: string[];
/**
* The group this property belongs to. When set to `'navigation'`, the property
* is shown as a primary action in the model picker.
*/
readonly group?: string;
};
};
};
export interface LanguageModelChatProvider<T extends LanguageModelChatInformation = LanguageModelChatInformation> {
provideLanguageModelChatInformation(options: PrepareLanguageModelChatModelOptions, token: CancellationToken): ProviderResult<T[]>;
provideLanguageModelChatResponse(model: T, messages: readonly LanguageModelChatRequestMessage[], options: ProvideLanguageModelChatResponseOptions, progress: Progress<LanguageModelResponsePart2>, token: CancellationToken): Thenable<void>;
@@ -115,4 +154,16 @@ declare module 'vscode' {
readonly [key: string]: any;
};
}
export interface ChatRequest {
/**
* Per-model configuration provided by the user. Contains resolved values based on the model's
* {@linkcode LanguageModelChatInformation.configurationSchema configurationSchema},
* with user overrides applied on top of schema defaults.
*
* This is the same data that is sent as {@linkcode ProvideLanguageModelChatResponseOptions.configuration}
* when the model is invoked via the language model API.
*/
readonly modelConfiguration?: { readonly [key: string]: any };
}
}