mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
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:
committed by
GitHub
parent
f7b1318461
commit
e8df0398a5
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -38,4 +38,5 @@ export interface ILanguageModelsProviderGroup extends IStringDictionary<unknown>
|
||||
readonly name: string;
|
||||
readonly vendor: string;
|
||||
readonly range?: IRange;
|
||||
readonly settings?: IStringDictionary<IStringDictionary<unknown>>;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
|
||||
51
src/vscode-dts/vscode.proposed.chatProvider.d.ts
vendored
51
src/vscode-dts/vscode.proposed.chatProvider.d.ts
vendored
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user