mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Add model vendor filtering for chat session model picker (#295124)
* Add model vendor filtering for chat session model picker * Udpates * Refactor chat session model handling to use targetChatSessionType for model filtering * Updates * Updates
This commit is contained in:
@@ -228,6 +228,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape {
|
||||
isDefaultForLocation,
|
||||
isUserSelectable: m.isUserSelectable,
|
||||
statusIcon: m.statusIcon,
|
||||
targetChatSessionType: m.targetChatSessionType,
|
||||
modelPickerCategory: m.category ?? DEFAULT_MODEL_PICKER_CATEGORY,
|
||||
capabilities: m.capabilities ? {
|
||||
vision: m.capabilities.imageInput,
|
||||
|
||||
@@ -417,7 +417,9 @@ export class OpenModelPickerAction extends Action2 {
|
||||
group: 'navigation',
|
||||
when:
|
||||
ContextKeyExpr.and(
|
||||
ChatContextKeys.lockedToCodingAgent.negate(),
|
||||
ContextKeyExpr.or(
|
||||
ChatContextKeys.lockedToCodingAgent.negate(),
|
||||
ChatContextKeys.chatSessionHasTargetedModels),
|
||||
ContextKeyExpr.or(
|
||||
ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Chat),
|
||||
ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.EditorInline),
|
||||
|
||||
@@ -207,6 +207,11 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsEx
|
||||
customAgentTarget: {
|
||||
description: localize('chatSessionsExtPoint.customAgentTarget', 'When set, the chat session will show a filtered mode picker that prefers custom agents whose target property matches this value. Custom agents without a target property are still shown in all session types. This enables the use of standard agent/mode with contributed sessions.'),
|
||||
type: 'string'
|
||||
},
|
||||
requiresCustomModels: {
|
||||
description: localize('chatSessionsExtPoint.requiresCustomModels', 'When set, the chat session will show a filtered model picker that prefers custom models. This enables the use of standard model picker with contributed sessions.'),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
required: ['type', 'name', 'displayName', 'description'],
|
||||
@@ -1122,6 +1127,11 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
return contribution?.customAgentTarget ?? Target.Undefined;
|
||||
}
|
||||
|
||||
public requiresCustomModelsForSessionType(chatSessionType: string): boolean {
|
||||
const contribution = this._contributions.get(chatSessionType)?.contribution;
|
||||
return !!contribution?.requiresCustomModels;
|
||||
}
|
||||
|
||||
public getContentProviderSchemes(): string[] {
|
||||
return Array.from(this._contentProviders.keys());
|
||||
}
|
||||
|
||||
@@ -353,6 +353,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
private chatSessionOptionsValid: IContextKey<boolean>;
|
||||
private agentSessionTypeKey: IContextKey<string>;
|
||||
private chatSessionHasCustomAgentTarget: IContextKey<boolean>;
|
||||
private chatSessionHasTargetedModels: IContextKey<boolean>;
|
||||
private modelWidget: ModelPickerActionItem | undefined;
|
||||
private modeWidget: ModePickerActionItem | undefined;
|
||||
private sessionTargetWidget: SessionTypePickerActionItem | undefined;
|
||||
@@ -473,6 +474,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
private _emptyInputState: ObservableMemento<IChatModelInputState | undefined>;
|
||||
private _chatSessionIsEmpty = false;
|
||||
private _pendingDelegationTarget: AgentSessionProviders | undefined = undefined;
|
||||
private _currentSessionType: string | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
// private readonly editorOptions: ChatEditorOptions, // TODO this should be used
|
||||
@@ -576,6 +578,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
}
|
||||
}
|
||||
this.chatSessionHasCustomAgentTarget = ChatContextKeys.chatSessionHasCustomAgentTarget.bindTo(contextKeyService);
|
||||
this.chatSessionHasTargetedModels = ChatContextKeys.chatSessionHasTargetedModels.bindTo(contextKeyService);
|
||||
|
||||
this.history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, this.location));
|
||||
|
||||
@@ -618,8 +621,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
|
||||
// We've changed models and the current one is no longer available. Select a new one
|
||||
const selectedModel = this._currentLanguageModel ? this.getModels().find(m => m.identifier === this._currentLanguageModel.get()?.identifier) : undefined;
|
||||
const selectedModelNotAvailable = this._currentLanguageModel && (!selectedModel?.metadata.isUserSelectable);
|
||||
if (!this.currentLanguageModel || selectedModelNotAvailable) {
|
||||
if (!this.currentLanguageModel || !selectedModel) {
|
||||
this.setCurrentLanguageModelToDefault();
|
||||
}
|
||||
}));
|
||||
@@ -680,10 +682,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
}
|
||||
|
||||
private getSelectedModelStorageKey(): string {
|
||||
const sessionType = this._currentSessionType;
|
||||
if (sessionType && this.hasModelsTargetingSessionType()) {
|
||||
return `chat.currentLanguageModel.${this.location}.${sessionType}`;
|
||||
}
|
||||
return `chat.currentLanguageModel.${this.location}`;
|
||||
}
|
||||
|
||||
private getSelectedModelIsDefaultStorageKey(): string {
|
||||
const sessionType = this._currentSessionType;
|
||||
if (sessionType && this.hasModelsTargetingSessionType()) {
|
||||
return `chat.currentLanguageModel.${this.location}.${sessionType}.isDefault`;
|
||||
}
|
||||
return `chat.currentLanguageModel.${this.location}.isDefault`;
|
||||
}
|
||||
|
||||
@@ -922,11 +932,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
}
|
||||
}
|
||||
|
||||
// Sync selected model
|
||||
// Sync selected model - validate it belongs to the current session's model pool
|
||||
if (state?.selectedModel) {
|
||||
const lm = this._currentLanguageModel.get();
|
||||
if (!lm || lm.identifier !== state.selectedModel.identifier) {
|
||||
this.setCurrentLanguageModel(state.selectedModel);
|
||||
if (this.isModelValidForCurrentSession(state.selectedModel)) {
|
||||
this.setCurrentLanguageModel(state.selectedModel);
|
||||
} else {
|
||||
// Model from state doesn't belong to this session's pool - use default
|
||||
this.setCurrentLanguageModelToDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -992,7 +1007,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
|
||||
private checkModelSupported(): void {
|
||||
const lm = this._currentLanguageModel.get();
|
||||
if (lm && (!this.modelSupportedForDefaultAgent(lm) || !this.modelSupportedForInlineChat(lm))) {
|
||||
if (lm && (!this.modelSupportedForDefaultAgent(lm) || !this.modelSupportedForInlineChat(lm) || !this.isModelValidForCurrentSession(lm))) {
|
||||
this.setCurrentLanguageModelToDefault();
|
||||
}
|
||||
}
|
||||
@@ -1049,12 +1064,79 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this.storageService.store(CachedLanguageModelsKey, models, StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
}
|
||||
models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
||||
return models.filter(entry => entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry) && this.modelSupportedForInlineChat(entry));
|
||||
|
||||
const sessionType = this.getCurrentSessionType();
|
||||
if (sessionType) {
|
||||
// Session has a specific chat session type - show only models that target
|
||||
// this session type, if any such models exist.
|
||||
const targeted = models.filter(entry => entry.metadata?.targetChatSessionType === sessionType);
|
||||
if (targeted.length > 0) {
|
||||
return targeted;
|
||||
}
|
||||
}
|
||||
|
||||
// No session type or no targeted models - show general models (those without
|
||||
// a targetChatSessionType) filtered by the standard criteria.
|
||||
return models.filter(entry => !entry.metadata?.targetChatSessionType && entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry) && this.modelSupportedForInlineChat(entry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chat session type for the current session, if any.
|
||||
* Uses the delegate or `getChatSessionFromInternalUri` to determine the session type.
|
||||
*/
|
||||
private getCurrentSessionType(): string | undefined {
|
||||
const delegateSessionType = this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.();
|
||||
if (delegateSessionType) {
|
||||
return delegateSessionType;
|
||||
}
|
||||
const sessionResource = this._widget?.viewModel?.model.sessionResource;
|
||||
const ctx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined;
|
||||
return ctx?.chatSessionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any registered models target the current session type.
|
||||
* This is used to set the context key that controls model picker visibility.
|
||||
*/
|
||||
private hasModelsTargetingSessionType(): boolean {
|
||||
const sessionType = this.getCurrentSessionType();
|
||||
if (!sessionType) {
|
||||
return false;
|
||||
}
|
||||
return this.languageModelsService.getLanguageModelIds().some(modelId => {
|
||||
const metadata = this.languageModelsService.lookupLanguageModel(modelId);
|
||||
return metadata?.targetChatSessionType === sessionType;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model is valid for the current session's model pool.
|
||||
* If the session has targeted models, the model must target this session type.
|
||||
* If no models target this session, the model must not have a targetChatSessionType.
|
||||
*/
|
||||
private isModelValidForCurrentSession(model: ILanguageModelChatMetadataAndIdentifier): boolean {
|
||||
if (this.hasModelsTargetingSessionType()) {
|
||||
// Session has targeted models - model must match
|
||||
return model.metadata.targetChatSessionType === this.getCurrentSessionType();
|
||||
}
|
||||
// No targeted models - model must not be session-specific
|
||||
return !model.metadata.targetChatSessionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the current model belongs to the current session's pool.
|
||||
* Called when switching sessions to prevent cross-contamination.
|
||||
*/
|
||||
private checkModelInSessionPool(): void {
|
||||
const lm = this._currentLanguageModel.get();
|
||||
if (lm && !this.isModelValidForCurrentSession(lm)) {
|
||||
this.setCurrentLanguageModelToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private setCurrentLanguageModelToDefault() {
|
||||
const allModels = this.getModels();
|
||||
const defaultModel = allModels.find(m => m.metadata.isDefaultForLocation[this.location]) || allModels.find(m => m.metadata.isUserSelectable);
|
||||
const defaultModel = allModels.find(m => m.metadata.isDefaultForLocation[this.location]) || allModels[0];
|
||||
if (defaultModel) {
|
||||
this.setCurrentLanguageModel(defaultModel);
|
||||
}
|
||||
@@ -1439,6 +1521,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
const customAgentTarget = ctx && this.chatSessionsService.getCustomAgentTargetForSessionType(ctx.chatSessionType);
|
||||
this.chatSessionHasCustomAgentTarget.set(customAgentTarget !== Target.Undefined);
|
||||
|
||||
// Check if this session type requires custom models
|
||||
const requiresCustomModels = ctx && this.chatSessionsService.requiresCustomModelsForSessionType(ctx.chatSessionType);
|
||||
this.chatSessionHasTargetedModels.set(!!requiresCustomModels);
|
||||
|
||||
// Handle agent option from session - set initial mode
|
||||
if (customAgentTarget) {
|
||||
const agentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, agentOptionId);
|
||||
@@ -1761,6 +1847,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this.tryUpdateWidgetController();
|
||||
this.updateContextUsageWidget();
|
||||
this.clearQuestionCarousel();
|
||||
|
||||
// Track the current session type and re-initialize model selection
|
||||
// when the session type changes (different session types may have
|
||||
// different model pools via targetChatSessionType).
|
||||
const newSessionType = this.getCurrentSessionType();
|
||||
if (newSessionType !== this._currentSessionType) {
|
||||
this._currentSessionType = newSessionType;
|
||||
this.initSelectedModel();
|
||||
}
|
||||
|
||||
// Validate that the current model belongs to the new session's pool
|
||||
this.checkModelInSessionPool();
|
||||
}));
|
||||
|
||||
let elements;
|
||||
|
||||
@@ -58,6 +58,12 @@ export namespace ChatContextKeys {
|
||||
* which means the mode picker should be shown with filtered custom agents.
|
||||
*/
|
||||
export const chatSessionHasCustomAgentTarget = new RawContextKey<boolean>('chatSessionHasCustomAgentTarget', false, { type: 'boolean', description: localize('chatSessionHasCustomAgentTarget', "True when the chat session has a customAgentTarget defined to filter modes.") });
|
||||
/**
|
||||
* True when the current chat session has models that specifically target it
|
||||
* via `targetChatSessionType`, which means the model picker should be shown
|
||||
* even when the widget is locked to a coding agent.
|
||||
*/
|
||||
export const chatSessionHasTargetedModels = new RawContextKey<boolean>('chatSessionHasTargetedModels', false, { type: 'boolean', description: localize('chatSessionHasTargetedModels', "True when the chat session has language models that target it via targetChatSessionType.") });
|
||||
export const agentSupportsAttachments = new RawContextKey<boolean>('agentSupportsAttachments', false, { type: 'boolean', description: localize('agentSupportsAttachments', "True when the chat agent supports attachments.") });
|
||||
export const withinEditSessionDiff = new RawContextKey<boolean>('withinEditSessionDiff', false, { type: 'boolean', description: localize('withinEditSessionDiff', "True when the chat widget dispatches to the edit session chat.") });
|
||||
export const filePartOfEditSession = new RawContextKey<boolean>('filePartOfEditSession', false, { type: 'boolean', description: localize('filePartOfEditSession', "True when the chat widget is within a file with an edit session.") });
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface IChatSessionsExtensionPoint {
|
||||
readonly capabilities?: IChatAgentAttachmentCapabilities;
|
||||
readonly commands?: IChatSessionCommandContribution[];
|
||||
readonly canDelegate?: boolean;
|
||||
readonly isReadOnly?: boolean;
|
||||
/**
|
||||
* When set, the chat session will show a filtered mode picker with custom agents
|
||||
* that have a matching `target` property. This enables contributed chat sessions
|
||||
@@ -92,6 +93,7 @@ export interface IChatSessionsExtensionPoint {
|
||||
* Custom agents without a `target` property are also shown in all filtered lists
|
||||
*/
|
||||
readonly customAgentTarget?: Target;
|
||||
readonly requiresCustomModels?: boolean;
|
||||
}
|
||||
|
||||
export interface IChatSessionItem {
|
||||
@@ -273,6 +275,10 @@ export interface IChatSessionsService {
|
||||
*/
|
||||
getCustomAgentTargetForSessionType(chatSessionType: string): Target;
|
||||
|
||||
/**
|
||||
* Returns whether the session type requires custom models. When true, the model picker should show filtered custom models.
|
||||
*/
|
||||
requiresCustomModelsForSessionType(chatSessionType: string): boolean;
|
||||
onDidChangeOptionGroups: Event<string>;
|
||||
|
||||
getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined;
|
||||
|
||||
@@ -195,6 +195,12 @@ export interface ILanguageModelChatMetadata {
|
||||
readonly agentMode?: boolean;
|
||||
readonly editTools?: ReadonlyArray<string>;
|
||||
};
|
||||
/**
|
||||
* 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
|
||||
* when the user is in a session matching this type.
|
||||
*/
|
||||
readonly targetChatSessionType?: string;
|
||||
}
|
||||
|
||||
export namespace ILanguageModelChatMetadata {
|
||||
|
||||
@@ -205,6 +205,10 @@ export class MockChatSessionsService implements IChatSessionsService {
|
||||
return this.contributions.find(c => c.type === chatSessionType)?.customAgentTarget ?? Target.Undefined;
|
||||
}
|
||||
|
||||
requiresCustomModelsForSessionType(chatSessionType: string): boolean {
|
||||
return this.contributions.find(c => c.type === chatSessionType)?.requiresCustomModels ?? false;
|
||||
}
|
||||
|
||||
getContentProviderSchemes(): string[] {
|
||||
return Array.from(this.contentProviders.keys());
|
||||
}
|
||||
|
||||
@@ -65,6 +65,15 @@ declare module 'vscode' {
|
||||
readonly category?: { label: string; order: number };
|
||||
|
||||
readonly statusIcon?: ThemeIcon;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* when the user is in a session matching this type.
|
||||
*
|
||||
* The value must match a `type` declared in a `chatSessions` extension contribution.
|
||||
*/
|
||||
readonly targetChatSessionType?: string;
|
||||
}
|
||||
|
||||
export interface LanguageModelChatCapabilities {
|
||||
|
||||
Reference in New Issue
Block a user