From e8df0398a542b80e04fcdccf5d4788cfa84d8c1e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 18 Mar 2026 14:35:05 +0100 Subject: [PATCH] Add per-model configuration support for language models (#302771) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- .../workbench/api/common/extHost.protocol.ts | 4 +- .../api/common/extHostChatAgents2.ts | 2 + .../api/common/extHostChatSessions.ts | 2 +- .../api/common/extHostLanguageModels.ts | 8 +- .../api/common/extHostTypeConverters.ts | 4 +- .../chatManagement/chatModelsWidget.ts | 24 ++ .../languageModelsConfigurationService.ts | 52 ++- .../common/chatService/chatServiceImpl.ts | 1 + .../contrib/chat/common/languageModels.ts | 316 +++++++++++++++++- .../common/languageModelsConfiguration.ts | 1 + .../chat/common/participants/chatAgents.ts | 2 + .../tools/builtinTools/runSubagentTool.ts | 1 + .../chatModelsViewModel.test.ts | 15 + .../chat/test/common/languageModels.test.ts | 150 +++++++++ .../chat/test/common/languageModels.ts | 20 +- .../vscode.proposed.chatProvider.d.ts | 51 +++ 16 files changed, 625 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 564e9ba689b..ca2e16b8a65 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -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; $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void; - $startChatRequest(modelId: string, requestId: number, from: ExtensionIdentifier | undefined, messages: SerializableObjectWithBuffers, options: { [name: string]: any }, token: CancellationToken): Promise; + $startChatRequest(modelId: string, requestId: number, from: ExtensionIdentifier | undefined, messages: SerializableObjectWithBuffers, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise; $acceptResponsePart(requestId: number, chunk: SerializableObjectWithBuffers): Promise; $acceptResponseDone(requestId: number, error: SerializedError | undefined): Promise; $provideTokenLength(modelId: string, value: string | IChatMessage, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 860c7277766..12df8ec2edd 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -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, diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 62ff2c6c102..c7e15f34f8a 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -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); diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index cc76961ab15..9d2d8bc68c6 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -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, options: vscode.LanguageModelChatRequestOptions, token: CancellationToken): Promise { + async $startChatRequest(modelId: string, requestId: number, from: ExtensionIdentifier | undefined, messages: SerializableObjectWithBuffers, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise { 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 ); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ae28e6be0ee..97ccc59fb08 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -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, extension: IRelaxedExtensionDescription, logService: ILogService): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, modelConfiguration: IStringDictionary | undefined, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: Map, extension: IRelaxedExtensionDescription, logService: ILogService): vscode.ChatRequest { const toolReferences: IChatRequestVariableEntry[] = []; const variableReferences: IChatRequestVariableEntry[] = []; @@ -3438,6 +3439,7 @@ export namespace ChatAgentRequest { toolInvocationToken: Object.freeze({ sessionResource: request.sessionResource }) as never, tools, model, + modelConfiguration, editedFileEvents: request.editedFileEvents, modeInstructions: request.modeInstructions?.content, modeInstructions2: ChatRequestModeInstructions.to(request.modeInstructions), diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 3e5e9bf244c..f362516fa9a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -792,6 +792,21 @@ class ActionsColumnRenderer extends ModelsTableColumnRenderer 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; diff --git a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts index f89cca85b67..e5ad21a9bb5 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts @@ -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'] } }; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 1ee23e9326f..4d159c616b2 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -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, diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index d88d5a917f2..9347996e4b5 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -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; provideLanguageModelChatInfo(options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise; - sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: { [name: string]: unknown }, token: CancellationToken): Promise; + sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise; provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise; } export interface ILanguageModelChat { metadata: ILanguageModelChatMetadata; - sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: { [name: string]: unknown }, token: CancellationToken): Promise; + sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise; provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise; } @@ -313,6 +330,13 @@ export interface ILanguageModelChatInfoOptions { readonly configuration?: IStringDictionary; } +export interface ILanguageModelChatRequestOptions { + readonly modelOptions?: IStringDictionary; + readonly configuration?: IStringDictionary; + // 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; + sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise; computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise; + /** + * 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 | undefined; + + /** + * Updates the per-model configuration for the given model. + * Merges the provided values into the existing configuration. + */ + setModelConfiguration(modelId: string, values: IStringDictionary): Promise; + + /** + * 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 | undefined): Promise; removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise; configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise; + /** + * Opens the language models configuration file and navigates to + * or creates the per-model configuration for the given model. + */ + configureModel(modelId: string): Promise; + migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise; /** @@ -531,6 +580,7 @@ export class LanguageModelsService implements ILanguageModelsService { private readonly _modelCache = new Map(); private readonly _resolveLMSequencer = new SequencerByKey(); private _modelPickerUserPreferences: IStringDictionary = {}; + private readonly _modelConfigurations = new Map>(); private readonly _hasUserSelectableModels: IContextKey; private readonly _onLanguageModelChange = this._store.add(new Emitter()); @@ -821,11 +871,29 @@ export class LanguageModelsService implements ILanguageModelsService { } const groups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups(); + const perModelConfigurations = new Map>(); 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 { - const provider = this._providers.get(this._modelCache.get(modelId)?.vendor || ''); + async sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise { + 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 | undefined { + const userConfig = this._modelConfigurations.get(modelId); + const schema = metadata?.configurationSchema; + + if (!schema?.properties && !userConfig) { + return undefined; + } + + // Start with schema defaults + const defaults: IStringDictionary = {}; + 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 { @@ -947,6 +1062,124 @@ export class LanguageModelsService implements ILanguageModelsService { return provider.provideTokenCount(modelId, message, token); } + getModelConfiguration(modelId: string): IStringDictionary | undefined { + const metadata = this._modelCache.get(modelId); + return this._resolveModelConfigurationWithDefaults(modelId, metadata); + } + + async setModelConfiguration(modelId: string, values: IStringDictionary): Promise { + 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> | undefined) ?? {}; + let updatedSettings: IStringDictionary>; + 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 { const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId); @@ -992,6 +1225,63 @@ export class LanguageModelsService implements ILanguageModelsService { } } + async configureModel(modelId: string): Promise { + 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 | undefined): Promise { 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> { if (!schema) { return {}; @@ -1299,7 +1597,7 @@ export class LanguageModelsService implements ILanguageModelsService { const result: IStringDictionary = {}; 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]; diff --git a/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts b/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts index 4f0dfa41691..86c0d307537 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts @@ -38,4 +38,5 @@ export interface ILanguageModelsProviderGroup extends IStringDictionary readonly name: string; readonly vendor: string; readonly range?: IRange; + readonly settings?: IStringDictionary>; } diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 96c673ab22c..df479623ad9 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -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; userSelectedTools?: UserSelectedTools; modeInstructions?: IChatRequestModeInstructions; editedFileEvents?: IChatAgentEditedFileEvent[]; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 2710319a7de..2dcdda1d4ce 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -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, diff --git a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts index 46e8316eef2..307e6955d7a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts @@ -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 | undefined { + return undefined; + } + + async setModelConfiguration(_modelId: string, _values: IStringDictionary): Promise { + } + + getModelConfigurationActions(_modelId: string): IAction[] { + return []; + } + async configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise { } + async configureModel(_modelId: string): Promise { + } + async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary | undefined): Promise { } diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 5790fab95dd..6908d48f9d4 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -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() { + override activateByEvent() { + return Promise.resolve(); + } + }, + new NullLogService(), + new TestStorageService(), + new MockContextKeyService(), + new class extends mock() { + 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() { }, + new TestSecretStorageService(), + new class extends mock() { override readonly version = '1.100.0'; }, + new class extends mock() { }, + ); + + 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(); + 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 } }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.ts index 8ead7eab0a9..51ac576ae1a 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.ts @@ -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 { + sendChatRequest(identifier: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise { 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 | undefined { + return undefined; + } + + async setModelConfiguration(_modelId: string, _values: IStringDictionary): Promise { + } + + getModelConfigurationActions(_modelId: string): IAction[] { + return []; + } + async configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise { } + async configureModel(_modelId: string): Promise { + } + async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary | undefined): Promise { } diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index b19b106205b..c20711a3305 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -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 & { + /** + * 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 { provideLanguageModelChatInformation(options: PrepareLanguageModelChatModelOptions, token: CancellationToken): ProviderResult; provideLanguageModelChatResponse(model: T, messages: readonly LanguageModelChatRequestMessage[], options: ProvideLanguageModelChatResponseOptions, progress: Progress, token: CancellationToken): Thenable; @@ -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 }; + } }