mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Support model as string[] in custom agent definitions (#289655)
* Initial plan * Support model as string or string[] in custom agents - Updated ICustomAgent.model to support string | string[] | undefined - Updated IChatModeData.model and IChatMode.model accordingly - Added getStringOrStringArrayAttribute method in PromptHeader - Updated validateModel in promptValidator to handle both string and array types - Added comprehensive tests for model array validation Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * Support model as string or string[] in custom agents - verified - All code compiles successfully with 0 errors - Tests verified to compile and be comprehensive - Backward compatible with single string models - Type-safe implementation with proper validation Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * Fix: Include empty strings in model array parsing - Updated getStringOrStringArrayAttribute to include empty strings - Allows downstream validation to catch empty string errors - Addresses code review feedback Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * Fix: Update model validation in isCachedChatModeData to allow arrays - Modified isCachedChatModeData to validate model as string | string[] - Fixes test failures related to cached mode data validation - All chat tests now passing Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * update * add lookupLanguageModelByQualifiedName * fix completion and hover * Update src/vs/workbench/contrib/chat/common/languageModels.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix test * update * update * fix --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> Co-authored-by: Martin Aeschlimann <martinae@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -624,9 +624,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this._register(autorun(r => {
|
||||
const mode = this._currentModeObservable.read(r);
|
||||
this.chatModeKindKey.set(mode.kind);
|
||||
const model = mode.model?.read(r);
|
||||
if (model) {
|
||||
this.switchModelByQualifiedName(model);
|
||||
const models = mode.model?.read(r);
|
||||
if (models) {
|
||||
this.switchModelByQualifiedName(models);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -722,17 +722,20 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
}
|
||||
}
|
||||
|
||||
public switchModelByQualifiedName(qualifiedModelName: string): boolean {
|
||||
public switchModelByQualifiedName(qualifiedModelNames: readonly string[]): boolean {
|
||||
const models = this.getModels();
|
||||
const model = models.find(m => ILanguageModelChatMetadata.matchesQualifiedName(qualifiedModelName, m.metadata));
|
||||
if (model) {
|
||||
this.setCurrentLanguageModel(model);
|
||||
return true;
|
||||
for (const qualifiedModelName of qualifiedModelNames) {
|
||||
const model = models.find(m => ILanguageModelChatMetadata.matchesQualifiedName(qualifiedModelName, m.metadata));
|
||||
if (model) {
|
||||
this.setCurrentLanguageModel(model);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this.logService.warn(`[chat] Model "${qualifiedModelName}" not found. Use format "<name> (<vendor>)", e.g. "GPT-4o (copilot)".`);
|
||||
this.logService.warn(`[chat] Node of the models "${qualifiedModelNames.join(', ')}" not found. Use format "<name> (<vendor>)", e.g. "GPT-4o (copilot)".`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public switchToNextModel(): void {
|
||||
const models = this.getModels();
|
||||
if (models.length > 0) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { IHandOff } from './promptSyntax/promptFileParser.js';
|
||||
import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js';
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { isString } from '../../../../base/common/types.js';
|
||||
|
||||
export const IChatModeService = createDecorator<IChatModeService>('chatModeService');
|
||||
export interface IChatModeService {
|
||||
@@ -116,7 +117,7 @@ export class ChatModeService extends Disposable implements IChatModeService {
|
||||
name: cachedMode.name,
|
||||
description: cachedMode.description,
|
||||
tools: cachedMode.customTools,
|
||||
model: cachedMode.model,
|
||||
model: isString(cachedMode.model) ? [cachedMode.model] : cachedMode.model,
|
||||
argumentHint: cachedMode.argumentHint,
|
||||
agentInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] },
|
||||
handOffs: cachedMode.handOffs,
|
||||
@@ -236,7 +237,7 @@ export interface IChatModeData {
|
||||
readonly description?: string;
|
||||
readonly kind: ChatModeKind;
|
||||
readonly customTools?: readonly string[];
|
||||
readonly model?: string;
|
||||
readonly model?: readonly string[] | string;
|
||||
readonly argumentHint?: string;
|
||||
readonly modeInstructions?: IChatModeInstructions;
|
||||
readonly body?: string; /* deprecated */
|
||||
@@ -258,7 +259,7 @@ export interface IChatMode {
|
||||
readonly kind: ChatModeKind;
|
||||
readonly customTools?: IObservable<readonly string[] | undefined>;
|
||||
readonly handOffs?: IObservable<readonly IHandOff[] | undefined>;
|
||||
readonly model?: IObservable<string | undefined>;
|
||||
readonly model?: IObservable<readonly string[] | undefined>;
|
||||
readonly argumentHint?: IObservable<string | undefined>;
|
||||
readonly modeInstructions?: IObservable<IChatModeInstructions>;
|
||||
readonly uri?: IObservable<URI>;
|
||||
@@ -291,7 +292,7 @@ function isCachedChatModeData(data: unknown): data is IChatModeData {
|
||||
(mode.description === undefined || typeof mode.description === 'string') &&
|
||||
(mode.customTools === undefined || Array.isArray(mode.customTools)) &&
|
||||
(mode.modeInstructions === undefined || (typeof mode.modeInstructions === 'object' && mode.modeInstructions !== null)) &&
|
||||
(mode.model === undefined || typeof mode.model === 'string') &&
|
||||
(mode.model === undefined || typeof mode.model === 'string' || Array.isArray(mode.model)) &&
|
||||
(mode.argumentHint === undefined || typeof mode.argumentHint === 'string') &&
|
||||
(mode.handOffs === undefined || Array.isArray(mode.handOffs)) &&
|
||||
(mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)) &&
|
||||
@@ -307,7 +308,7 @@ export class CustomChatMode implements IChatMode {
|
||||
private readonly _customToolsObservable: ISettableObservable<readonly string[] | undefined>;
|
||||
private readonly _modeInstructions: ISettableObservable<IChatModeInstructions>;
|
||||
private readonly _uriObservable: ISettableObservable<URI>;
|
||||
private readonly _modelObservable: ISettableObservable<string | undefined>;
|
||||
private readonly _modelObservable: ISettableObservable<readonly string[] | undefined>;
|
||||
private readonly _argumentHintObservable: ISettableObservable<string | undefined>;
|
||||
private readonly _handoffsObservable: ISettableObservable<readonly IHandOff[] | undefined>;
|
||||
private readonly _targetObservable: ISettableObservable<string | undefined>;
|
||||
@@ -337,7 +338,7 @@ export class CustomChatMode implements IChatMode {
|
||||
return this._customToolsObservable;
|
||||
}
|
||||
|
||||
get model(): IObservable<string | undefined> {
|
||||
get model(): IObservable<readonly string[] | undefined> {
|
||||
return this._modelObservable;
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,11 @@ export interface ILanguageModelsService {
|
||||
|
||||
lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined;
|
||||
|
||||
/**
|
||||
* Find a model by its qualified name. The qualified name is what is used in prompt and agent files and is in the format "Model Name (Vendor)".
|
||||
*/
|
||||
lookupLanguageModelByQualifiedName(qualifiedName: string): ILanguageModelChatMetadata | undefined;
|
||||
|
||||
getLanguageModelGroups(vendor: string): ILanguageModelsGroup[];
|
||||
|
||||
/**
|
||||
@@ -637,6 +642,15 @@ export class LanguageModelsService implements ILanguageModelsService {
|
||||
return model;
|
||||
}
|
||||
|
||||
lookupLanguageModelByQualifiedName(referenceName: string): ILanguageModelChatMetadata | undefined {
|
||||
for (const model of this._modelCache.values()) {
|
||||
if (ILanguageModelChatMetadata.matchesQualifiedName(referenceName, model)) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _resolveAllLanguageModels(vendorId: string, silent: boolean): Promise<void> {
|
||||
|
||||
const vendor = this._vendors.get(vendorId);
|
||||
|
||||
@@ -158,6 +158,13 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
|
||||
}
|
||||
|
||||
if (promptType === PromptsType.prompt || promptType === PromptsType.agent) {
|
||||
if (attribute.key === PromptHeaderAttributes.model) {
|
||||
if (attribute.value.type === 'array') {
|
||||
// if the position is inside the tools metadata, we provide tool name completions
|
||||
const getValues = async () => this.getModelNames(promptType === PromptsType.agent);
|
||||
return this.provideArrayCompletions(model, position, attribute, getValues);
|
||||
}
|
||||
}
|
||||
if (attribute.key === PromptHeaderAttributes.tools) {
|
||||
if (attribute.value.type === 'array') {
|
||||
// if the position is inside the tools metadata, we provide tool name completions
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Range } from '../../../../../../editor/common/core/range.js';
|
||||
import { Hover, HoverContext, HoverProvider } from '../../../../../../editor/common/languages.js';
|
||||
import { ITextModel } from '../../../../../../editor/common/model.js';
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js';
|
||||
import { ILanguageModelsService } from '../../languageModels.js';
|
||||
import { ILanguageModelToolsService, isToolSet, IToolSet } from '../../tools/languageModelToolsService.js';
|
||||
import { IChatModeService, isBuiltinChatMode } from '../../chatModes.js';
|
||||
import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js';
|
||||
@@ -106,7 +106,7 @@ export class PromptHoverProvider implements HoverProvider {
|
||||
case PromptHeaderAttributes.argumentHint:
|
||||
return this.createHover(localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'), attribute.range);
|
||||
case PromptHeaderAttributes.model:
|
||||
return this.getModelHover(attribute, attribute.range, localize('promptHeader.agent.model', 'Specify the model that runs this custom agent.'), isGithubTarget(promptType, header.target));
|
||||
return this.getModelHover(attribute, position, localize('promptHeader.agent.model', 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.'), isGithubTarget(promptType, header.target));
|
||||
case PromptHeaderAttributes.tools:
|
||||
return this.getToolHover(attribute, position, localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.'));
|
||||
case PromptHeaderAttributes.handOffs:
|
||||
@@ -132,7 +132,7 @@ export class PromptHoverProvider implements HoverProvider {
|
||||
case PromptHeaderAttributes.argumentHint:
|
||||
return this.createHover(localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'), attribute.range);
|
||||
case PromptHeaderAttributes.model:
|
||||
return this.getModelHover(attribute, attribute.range, localize('promptHeader.prompt.model', 'The model to use in this prompt.'), false);
|
||||
return this.getModelHover(attribute, position, localize('promptHeader.prompt.model', 'The model to use in this prompt. Can also be a list of models. The first available model will be used.'), false);
|
||||
case PromptHeaderAttributes.tools:
|
||||
return this.getToolHover(attribute, position, localize('promptHeader.prompt.tools', 'The tools to use in this prompt.'));
|
||||
case PromptHeaderAttributes.agent:
|
||||
@@ -184,27 +184,41 @@ export class PromptHoverProvider implements HoverProvider {
|
||||
return this.createHover(lines.join('\n'), range);
|
||||
}
|
||||
|
||||
private getModelHover(node: IHeaderAttribute, range: Range, baseMessage: string, isGitHubTarget: boolean): Hover | undefined {
|
||||
private getModelHover(node: IHeaderAttribute, position: Position, baseMessage: string, isGitHubTarget: boolean): Hover | undefined {
|
||||
if (isGitHubTarget) {
|
||||
return this.createHover(baseMessage + '\n\n' + localize('promptHeader.agent.model.githubCopilot', 'Note: This attribute is not used when target is github-copilot.'), range);
|
||||
return this.createHover(baseMessage + '\n\n' + localize('promptHeader.agent.model.githubCopilot', 'Note: This attribute is not used when target is github-copilot.'), node.range);
|
||||
}
|
||||
const modelHoverContent = (modelName: string): Hover | undefined => {
|
||||
const meta = this.languageModelsService.lookupLanguageModelByQualifiedName(modelName);
|
||||
if (meta) {
|
||||
const lines: string[] = [];
|
||||
lines.push(baseMessage + '\n');
|
||||
lines.push(localize('modelName', '- Name: {0}', meta.name));
|
||||
lines.push(localize('modelFamily', '- Family: {0}', meta.family));
|
||||
lines.push(localize('modelVendor', '- Vendor: {0}', meta.vendor));
|
||||
if (meta.tooltip) {
|
||||
lines.push('', '', meta.tooltip);
|
||||
}
|
||||
return this.createHover(lines.join('\n'), node.range);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
if (node.value.type === 'string') {
|
||||
for (const id of this.languageModelsService.getLanguageModelIds()) {
|
||||
const meta = this.languageModelsService.lookupLanguageModel(id);
|
||||
if (meta && ILanguageModelChatMetadata.matchesQualifiedName(node.value.value, meta)) {
|
||||
const lines: string[] = [];
|
||||
lines.push(baseMessage + '\n');
|
||||
lines.push(localize('modelName', '- Name: {0}', meta.name));
|
||||
lines.push(localize('modelFamily', '- Family: {0}', meta.family));
|
||||
lines.push(localize('modelVendor', '- Vendor: {0}', meta.vendor));
|
||||
if (meta.tooltip) {
|
||||
lines.push('', '', meta.tooltip);
|
||||
const hover = modelHoverContent(node.value.value);
|
||||
if (hover) {
|
||||
return hover;
|
||||
}
|
||||
} else if (node.value.type === 'array') {
|
||||
for (const item of node.value.items) {
|
||||
if (item.type === 'string' && item.range.containsPosition(position)) {
|
||||
const hover = modelHoverContent(item.value);
|
||||
if (hover) {
|
||||
return hover;
|
||||
}
|
||||
return this.createHover(lines.join('\n'), range);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.createHover(baseMessage, range);
|
||||
return this.createHover(baseMessage, node.range);
|
||||
}
|
||||
|
||||
private getAgentHover(agentAttribute: IHeaderAttribute, position: Position): Hover | undefined {
|
||||
|
||||
@@ -282,36 +282,58 @@ export class PromptValidator {
|
||||
if (!attribute) {
|
||||
return;
|
||||
}
|
||||
if (attribute.value.type !== 'string') {
|
||||
report(toMarker(localize('promptValidator.modelMustBeString', "The 'model' attribute must be a string."), attribute.value.range, MarkerSeverity.Error));
|
||||
return;
|
||||
}
|
||||
const modelName = attribute.value.value.trim();
|
||||
if (modelName.length === 0) {
|
||||
report(toMarker(localize('promptValidator.modelMustBeNonEmpty', "The 'model' attribute must be a non-empty string."), attribute.value.range, MarkerSeverity.Error));
|
||||
if (attribute.value.type !== 'string' && attribute.value.type !== 'array') {
|
||||
report(toMarker(localize('promptValidator.modelMustBeStringOrArray', "The 'model' attribute must be a string or an array of strings."), attribute.value.range, MarkerSeverity.Error));
|
||||
return;
|
||||
}
|
||||
|
||||
const languageModes = this.languageModelsService.getLanguageModelIds();
|
||||
if (languageModes.length === 0) {
|
||||
const modelNames: [string, Range][] = [];
|
||||
if (attribute.value.type === 'string') {
|
||||
const modelName = attribute.value.value.trim();
|
||||
if (modelName.length === 0) {
|
||||
report(toMarker(localize('promptValidator.modelMustBeNonEmpty', "The 'model' attribute must be a non-empty string."), attribute.value.range, MarkerSeverity.Error));
|
||||
return;
|
||||
}
|
||||
modelNames.push([modelName, attribute.value.range]);
|
||||
} else if (attribute.value.type === 'array') {
|
||||
if (attribute.value.items.length === 0) {
|
||||
report(toMarker(localize('promptValidator.modelArrayMustNotBeEmpty', "The 'model' array must not be empty."), attribute.value.range, MarkerSeverity.Error));
|
||||
return;
|
||||
}
|
||||
for (const item of attribute.value.items) {
|
||||
if (item.type !== 'string') {
|
||||
report(toMarker(localize('promptValidator.modelArrayMustContainStrings', "The 'model' array must contain only strings."), item.range, MarkerSeverity.Error));
|
||||
return;
|
||||
}
|
||||
const modelName = item.value.trim();
|
||||
if (modelName.length === 0) {
|
||||
report(toMarker(localize('promptValidator.modelArrayItemMustBeNonEmpty', "Model names in the array must be non-empty strings."), item.range, MarkerSeverity.Error));
|
||||
return;
|
||||
}
|
||||
modelNames.push([modelName, item.range]);
|
||||
}
|
||||
}
|
||||
|
||||
const languageModels = this.languageModelsService.getLanguageModelIds();
|
||||
if (languageModels.length === 0) {
|
||||
// likely the service is not initialized yet
|
||||
return;
|
||||
}
|
||||
const modelMetadata = this.findModelByName(languageModes, modelName);
|
||||
if (!modelMetadata) {
|
||||
report(toMarker(localize('promptValidator.modelNotFound', "Unknown model '{0}'.", modelName), attribute.value.range, MarkerSeverity.Warning));
|
||||
|
||||
} else if (agentKind === ChatModeKind.Agent && !ILanguageModelChatMetadata.suitableForAgentMode(modelMetadata)) {
|
||||
report(toMarker(localize('promptValidator.modelNotSuited', "Model '{0}' is not suited for agent mode.", modelName), attribute.value.range, MarkerSeverity.Warning));
|
||||
for (const [modelName, range] of modelNames) {
|
||||
const modelMetadata = this.findModelByName(modelName);
|
||||
if (!modelMetadata) {
|
||||
report(toMarker(localize('promptValidator.modelNotFound', "Unknown model '{0}'.", modelName), range, MarkerSeverity.Warning));
|
||||
} else if (agentKind === ChatModeKind.Agent && !ILanguageModelChatMetadata.suitableForAgentMode(modelMetadata)) {
|
||||
report(toMarker(localize('promptValidator.modelNotSuited', "Model '{0}' is not suited for agent mode.", modelName), range, MarkerSeverity.Warning));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private findModelByName(languageModes: string[], modelName: string): ILanguageModelChatMetadata | undefined {
|
||||
for (const model of languageModes) {
|
||||
const metadata = this.languageModelsService.lookupLanguageModel(model);
|
||||
if (metadata && metadata.isUserSelectable !== false && ILanguageModelChatMetadata.matchesQualifiedName(modelName, metadata)) {
|
||||
return metadata;
|
||||
}
|
||||
private findModelByName(modelName: string): ILanguageModelChatMetadata | undefined {
|
||||
const metadata = this.languageModelsService.lookupLanguageModelByQualifiedName(modelName);
|
||||
if (metadata && metadata.isUserSelectable !== false) {
|
||||
return metadata;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -184,8 +184,8 @@ export class PromptHeader {
|
||||
return this.getStringAttribute(PromptHeaderAttributes.agent) ?? this.getStringAttribute(PromptHeaderAttributes.mode);
|
||||
}
|
||||
|
||||
public get model(): string | undefined {
|
||||
return this.getStringAttribute(PromptHeaderAttributes.model);
|
||||
public get model(): readonly string[] | undefined {
|
||||
return this.getStringOrStringArrayAttribute(PromptHeaderAttributes.model);
|
||||
}
|
||||
|
||||
public get applyTo(): string | undefined {
|
||||
@@ -294,6 +294,26 @@ export class PromptHeader {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getStringOrStringArrayAttribute(key: string): readonly string[] | undefined {
|
||||
const attribute = this._parsedHeader.attributes.find(attr => attr.key === key);
|
||||
if (!attribute) {
|
||||
return undefined;
|
||||
}
|
||||
if (attribute.value.type === 'string') {
|
||||
return [attribute.value.value];
|
||||
}
|
||||
if (attribute.value.type === 'array') {
|
||||
const result: string[] = [];
|
||||
for (const item of attribute.value.items) {
|
||||
if (item.type === 'string') {
|
||||
result.push(item.value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get agents(): string[] | undefined {
|
||||
return this.getStringArrayAttribute(PromptHeaderAttributes.agents);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export interface ICustomAgent {
|
||||
/**
|
||||
* Model metadata in the prompt header.
|
||||
*/
|
||||
readonly model?: string;
|
||||
readonly model?: readonly string[];
|
||||
|
||||
/**
|
||||
* Argument hint metadata in the prompt header that describes what inputs the agent expects or supports.
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ChatMode, IChatMode, IChatModeService } from '../../chatModes.js';
|
||||
import { IChatProgress, IChatService } from '../../chatService/chatService.js';
|
||||
import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js';
|
||||
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../constants.js';
|
||||
import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js';
|
||||
import { ILanguageModelsService } from '../../languageModels.js';
|
||||
import {
|
||||
CountTokensCallback,
|
||||
ILanguageModelToolsService,
|
||||
@@ -146,14 +146,13 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
|
||||
mode = this.chatModeService.findModeByName(args.agentName);
|
||||
if (mode) {
|
||||
// Use mode-specific model if available
|
||||
const modeModelQualifiedName = mode.model?.get();
|
||||
if (modeModelQualifiedName) {
|
||||
// Find the actual model identifier from the qualified name
|
||||
const modelIds = this.languageModelsService.getLanguageModelIds();
|
||||
for (const modelId of modelIds) {
|
||||
const metadata = this.languageModelsService.lookupLanguageModel(modelId);
|
||||
if (metadata && ILanguageModelChatMetadata.matchesQualifiedName(modeModelQualifiedName, metadata)) {
|
||||
modeModelId = modelId;
|
||||
const modeModelQualifiedNames = mode.model?.get();
|
||||
if (modeModelQualifiedNames) {
|
||||
// Find the actual model identifier from the qualified name(s)
|
||||
for (const qualifiedName of modeModelQualifiedNames) {
|
||||
const metadata = this.languageModelsService.lookupLanguageModelByQualifiedName(qualifiedName);
|
||||
if (metadata) {
|
||||
modeModelId = metadata.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,15 @@ class MockLanguageModelsService implements ILanguageModelsService {
|
||||
return this.models.get(identifier);
|
||||
}
|
||||
|
||||
lookupLanguageModelByQualifiedName(referenceName: string): ILanguageModelChatMetadata | undefined {
|
||||
for (const metadata of this.models.values()) {
|
||||
if (ILanguageModelChatMetadata.matchesQualifiedName(referenceName, metadata)) {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getLanguageModels(): ILanguageModelChatMetadataAndIdentifier[] {
|
||||
const result: ILanguageModelChatMetadataAndIdentifier[] = [];
|
||||
for (const [identifier, metadata] of this.models.entries()) {
|
||||
|
||||
@@ -178,6 +178,38 @@ suite('PromptHeaderAutocompletion', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('complete model names inside model array', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test"',
|
||||
'model: [|]',
|
||||
'---',
|
||||
].join('\n');
|
||||
|
||||
const actual = await getCompletions(content, PromptsType.agent);
|
||||
// GPT 4 is excluded because it has agentMode: false
|
||||
assert.deepStrictEqual(actual.sort(sortByLabel), [
|
||||
{ label: 'MAE 4 (olama)', result: `model: ['MAE 4 (olama)']` },
|
||||
{ label: 'MAE 4.1 (copilot)', result: `model: ['MAE 4.1 (copilot)']` },
|
||||
].sort(sortByLabel));
|
||||
});
|
||||
|
||||
test('complete model names inside model array with existing entries', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test"',
|
||||
`model: ['MAE 4 (olama)', |]`,
|
||||
'---',
|
||||
].join('\n');
|
||||
|
||||
const actual = await getCompletions(content, PromptsType.agent);
|
||||
// GPT 4 is excluded because it has agentMode: false
|
||||
assert.deepStrictEqual(actual.sort(sortByLabel), [
|
||||
{ label: 'MAE 4 (olama)', result: `model: ['MAE 4 (olama)', 'MAE 4 (olama)']` },
|
||||
{ label: 'MAE 4.1 (copilot)', result: `model: ['MAE 4 (olama)', 'MAE 4.1 (copilot)']` },
|
||||
].sort(sortByLabel));
|
||||
});
|
||||
|
||||
test('complete tool names inside tools array', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
|
||||
@@ -59,8 +59,13 @@ suite('PromptHoverProvider', () => {
|
||||
|
||||
instaService.stub(ILanguageModelsService, {
|
||||
getLanguageModelIds() { return testModels.map(m => m.id); },
|
||||
lookupLanguageModel(name: string) {
|
||||
return testModels.find(m => m.id === name);
|
||||
lookupLanguageModelByQualifiedName(qualifiedName: string) {
|
||||
for (const metadata of testModels) {
|
||||
if (ILanguageModelChatMetadata.matchesQualifiedName(qualifiedName, metadata)) {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,7 +126,7 @@ suite('PromptHoverProvider', () => {
|
||||
].join('\n');
|
||||
const hover = await getHover(content, 4, 1, PromptsType.agent);
|
||||
const expected = [
|
||||
'Specify the model that runs this custom agent.',
|
||||
'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.',
|
||||
'',
|
||||
'Note: This attribute is not used when target is github-copilot.'
|
||||
].join('\n');
|
||||
@@ -138,7 +143,7 @@ suite('PromptHoverProvider', () => {
|
||||
].join('\n');
|
||||
const hover = await getHover(content, 4, 1, PromptsType.agent);
|
||||
const expected = [
|
||||
'Specify the model that runs this custom agent.',
|
||||
'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.',
|
||||
'',
|
||||
'- Name: MAE 4',
|
||||
'- Family: mae',
|
||||
@@ -229,6 +234,44 @@ suite('PromptHoverProvider', () => {
|
||||
assert.strictEqual(hover, 'Test Tool 1');
|
||||
});
|
||||
|
||||
test('hover on model attribute with vscode target and model array', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test"',
|
||||
'target: vscode',
|
||||
`model: ['MAE 4 (olama)', 'MAE 4.1 (copilot)']`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const hover = await getHover(content, 4, 10, PromptsType.agent);
|
||||
const expected = [
|
||||
'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.',
|
||||
'',
|
||||
'- Name: MAE 4',
|
||||
'- Family: mae',
|
||||
'- Vendor: olama'
|
||||
].join('\n');
|
||||
assert.strictEqual(hover, expected);
|
||||
});
|
||||
|
||||
test('hover on second model in model array', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test"',
|
||||
'target: vscode',
|
||||
`model: ['MAE 4 (olama)', 'MAE 4.1 (copilot)']`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const hover = await getHover(content, 4, 30, PromptsType.agent);
|
||||
const expected = [
|
||||
'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.',
|
||||
'',
|
||||
'- Name: MAE 4.1',
|
||||
'- Family: mae',
|
||||
'- Vendor: copilot'
|
||||
].join('\n');
|
||||
assert.strictEqual(hover, expected);
|
||||
});
|
||||
|
||||
test('hover on description attribute', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
@@ -298,7 +341,7 @@ suite('PromptHoverProvider', () => {
|
||||
].join('\n');
|
||||
const hover = await getHover(content, 3, 1, PromptsType.prompt);
|
||||
const expected = [
|
||||
'The model to use in this prompt.',
|
||||
'The model to use in this prompt. Can also be a list of models. The first available model will be used.',
|
||||
'',
|
||||
'- Name: MAE 4',
|
||||
'- Family: mae',
|
||||
|
||||
@@ -119,8 +119,13 @@ suite('PromptValidator', () => {
|
||||
|
||||
instaService.stub(ILanguageModelsService, {
|
||||
getLanguageModelIds() { return testModels.map(m => m.id); },
|
||||
lookupLanguageModel(name: string) {
|
||||
return testModels.find(m => m.id === name);
|
||||
lookupLanguageModelByQualifiedName(qualifiedName: string) {
|
||||
for (const metadata of testModels) {
|
||||
if (ILanguageModelChatMetadata.matchesQualifiedName(qualifiedName, metadata)) {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -199,6 +204,95 @@ suite('PromptValidator', () => {
|
||||
assert.deepStrictEqual(markers.map(m => m.message), [`The 'tools' attribute must be an array.`]);
|
||||
});
|
||||
|
||||
test('model as string array - valid', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test with model array"',
|
||||
`model: ['MAE 4 (olama)', 'MAE 4.1']`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const markers = await validate(content, PromptsType.agent);
|
||||
assert.deepStrictEqual(markers, []);
|
||||
});
|
||||
|
||||
test('model as string array - unknown model', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test with model array"',
|
||||
`model: ['MAE 4 (olama)', 'Unknown Model']`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const markers = await validate(content, PromptsType.agent);
|
||||
assert.strictEqual(markers.length, 1);
|
||||
assert.strictEqual(markers[0].severity, MarkerSeverity.Warning);
|
||||
assert.strictEqual(markers[0].message, `Unknown model 'Unknown Model'.`);
|
||||
});
|
||||
|
||||
test('model as string array - unsuitable model', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test with model array"',
|
||||
`model: ['MAE 4 (olama)', 'MAE 3.5 Turbo']`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const markers = await validate(content, PromptsType.agent);
|
||||
assert.strictEqual(markers.length, 1);
|
||||
assert.strictEqual(markers[0].severity, MarkerSeverity.Warning);
|
||||
assert.strictEqual(markers[0].message, `Model 'MAE 3.5 Turbo' is not suited for agent mode.`);
|
||||
});
|
||||
|
||||
test('model as string array - empty array', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test with empty model array"',
|
||||
`model: []`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const markers = await validate(content, PromptsType.agent);
|
||||
assert.strictEqual(markers.length, 1);
|
||||
assert.strictEqual(markers[0].severity, MarkerSeverity.Error);
|
||||
assert.strictEqual(markers[0].message, `The 'model' array must not be empty.`);
|
||||
});
|
||||
|
||||
test('model as string array - non-string item', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test with invalid model array"',
|
||||
`model: ['MAE 4 (olama)', 123]`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const markers = await validate(content, PromptsType.agent);
|
||||
assert.strictEqual(markers.length, 1);
|
||||
assert.strictEqual(markers[0].severity, MarkerSeverity.Error);
|
||||
assert.strictEqual(markers[0].message, `The 'model' array must contain only strings.`);
|
||||
});
|
||||
|
||||
test('model as string array - empty string item', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test with empty string in model array"',
|
||||
`model: ['MAE 4 (olama)', '']`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const markers = await validate(content, PromptsType.agent);
|
||||
assert.strictEqual(markers.length, 1);
|
||||
assert.strictEqual(markers[0].severity, MarkerSeverity.Error);
|
||||
assert.strictEqual(markers[0].message, `Model names in the array must be non-empty strings.`);
|
||||
});
|
||||
|
||||
test('model as invalid type', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
'description: "Test with invalid model type"',
|
||||
`model: 123`,
|
||||
'---',
|
||||
].join('\n');
|
||||
const markers = await validate(content, PromptsType.agent);
|
||||
assert.strictEqual(markers.length, 1);
|
||||
assert.strictEqual(markers[0].severity, MarkerSeverity.Error);
|
||||
assert.strictEqual(markers[0].message, `The 'model' attribute must be a string or an array of strings.`);
|
||||
});
|
||||
|
||||
test('each tool must be string', async () => {
|
||||
const content = [
|
||||
'---',
|
||||
|
||||
@@ -195,7 +195,7 @@ suite('ChatModeService', () => {
|
||||
description: 'Initial description',
|
||||
tools: ['tool1'],
|
||||
agentInstructions: { content: 'Initial body', toolReferences: [] },
|
||||
model: 'gpt-4',
|
||||
model: ['gpt-4'],
|
||||
source: workspaceSource,
|
||||
};
|
||||
|
||||
@@ -212,7 +212,7 @@ suite('ChatModeService', () => {
|
||||
description: 'Updated description',
|
||||
tools: ['tool1', 'tool2'],
|
||||
agentInstructions: { content: 'Updated body', toolReferences: [] },
|
||||
model: 'Updated model'
|
||||
model: ['Updated model']
|
||||
};
|
||||
|
||||
promptsService.setCustomModes([updatedMode]);
|
||||
@@ -228,7 +228,7 @@ suite('ChatModeService', () => {
|
||||
assert.strictEqual(updatedCustomMode.description.get(), 'Updated description');
|
||||
assert.deepStrictEqual(updatedCustomMode.customTools?.get(), ['tool1', 'tool2']);
|
||||
assert.deepStrictEqual(updatedCustomMode.modeInstructions?.get(), { content: 'Updated body', toolReferences: [] });
|
||||
assert.strictEqual(updatedCustomMode.model?.get(), 'Updated model');
|
||||
assert.deepStrictEqual(updatedCustomMode.model?.get(), ['Updated model']);
|
||||
assert.deepStrictEqual(updatedCustomMode.source, workspaceSource);
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@ export class NullLanguageModelsService implements ILanguageModelsService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
lookupLanguageModelByQualifiedName(qualifiedName: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getLanguageModels(): ILanguageModelChatMetadataAndIdentifier[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ suite('NewPromptsParser', () => {
|
||||
{ range: new Range(7, 79, 7, 85), name: 'tool-2', offset: 170 }
|
||||
]);
|
||||
assert.deepEqual(result.header.description, 'Agent test');
|
||||
assert.deepEqual(result.header.model, 'GPT 4.1');
|
||||
assert.deepEqual(result.header.model, ['GPT 4.1']);
|
||||
assert.ok(result.header.tools);
|
||||
assert.deepEqual(result.header.tools, ['tool1', 'tool2']);
|
||||
});
|
||||
@@ -110,7 +110,7 @@ suite('NewPromptsParser', () => {
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(result.header.description, 'Agent test');
|
||||
assert.deepEqual(result.header.model, 'GPT 4.1');
|
||||
assert.deepEqual(result.header.model, ['GPT 4.1']);
|
||||
assert.ok(result.header.handOffs);
|
||||
assert.deepEqual(result.header.handOffs, [
|
||||
{ label: 'Implement', agent: 'Default', prompt: 'Implement the plan', send: false },
|
||||
@@ -234,7 +234,7 @@ suite('NewPromptsParser', () => {
|
||||
]);
|
||||
assert.deepEqual(result.header.description, 'General purpose coding assistant');
|
||||
assert.deepEqual(result.header.agent, 'agent');
|
||||
assert.deepEqual(result.header.model, 'GPT 4.1');
|
||||
assert.deepEqual(result.header.model, ['GPT 4.1']);
|
||||
assert.ok(result.header.tools);
|
||||
assert.deepEqual(result.header.tools, ['search', 'terminal']);
|
||||
});
|
||||
|
||||
@@ -994,7 +994,7 @@ suite('PromptsService', () => {
|
||||
name: 'vscode-agent',
|
||||
description: 'VS Code specialized agent.',
|
||||
target: 'vscode',
|
||||
model: 'gpt-4',
|
||||
model: ['gpt-4'],
|
||||
agentInstructions: {
|
||||
content: 'I am specialized for VS Code editor tasks.',
|
||||
toolReferences: [],
|
||||
|
||||
@@ -474,7 +474,7 @@ export class InlineChatController implements IEditorContribution {
|
||||
|
||||
// Check for default model setting
|
||||
const defaultModelSetting = this._configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);
|
||||
if (defaultModelSetting && !this._zone.value.widget.chatWidget.input.switchModelByQualifiedName(defaultModelSetting)) {
|
||||
if (defaultModelSetting && !this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting])) {
|
||||
this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user