fix(coding-agent): handle slash-delimited /model refs closes #2174
This commit is contained in:
@@ -56,27 +56,61 @@ function isAlias(id: string): boolean {
|
|||||||
return !datePattern.test(id);
|
return !datePattern.test(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an exact model reference match.
|
||||||
|
* Supports either a bare model id or a canonical provider/modelId reference.
|
||||||
|
* When matching by bare id, ambiguous matches across providers are rejected.
|
||||||
|
*/
|
||||||
|
export function findExactModelReferenceMatch(
|
||||||
|
modelReference: string,
|
||||||
|
availableModels: Model<Api>[],
|
||||||
|
): Model<Api> | undefined {
|
||||||
|
const trimmedReference = modelReference.trim();
|
||||||
|
if (!trimmedReference) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedReference = trimmedReference.toLowerCase();
|
||||||
|
|
||||||
|
const canonicalMatches = availableModels.filter(
|
||||||
|
(model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,
|
||||||
|
);
|
||||||
|
if (canonicalMatches.length === 1) {
|
||||||
|
return canonicalMatches[0];
|
||||||
|
}
|
||||||
|
if (canonicalMatches.length > 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashIndex = trimmedReference.indexOf("/");
|
||||||
|
if (slashIndex !== -1) {
|
||||||
|
const provider = trimmedReference.substring(0, slashIndex).trim();
|
||||||
|
const modelId = trimmedReference.substring(slashIndex + 1).trim();
|
||||||
|
if (provider && modelId) {
|
||||||
|
const providerMatches = availableModels.filter(
|
||||||
|
(model) =>
|
||||||
|
model.provider.toLowerCase() === provider.toLowerCase() &&
|
||||||
|
model.id.toLowerCase() === modelId.toLowerCase(),
|
||||||
|
);
|
||||||
|
if (providerMatches.length === 1) {
|
||||||
|
return providerMatches[0];
|
||||||
|
}
|
||||||
|
if (providerMatches.length > 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idMatches = availableModels.filter((model) => model.id.toLowerCase() === normalizedReference);
|
||||||
|
return idMatches.length === 1 ? idMatches[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to match a pattern to a model from the available models list.
|
* Try to match a pattern to a model from the available models list.
|
||||||
* Returns the matched model or undefined if no match found.
|
* Returns the matched model or undefined if no match found.
|
||||||
*/
|
*/
|
||||||
function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {
|
function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {
|
||||||
// Check for provider/modelId format (provider is everything before the first /)
|
const exactMatch = findExactModelReferenceMatch(modelPattern, availableModels);
|
||||||
const slashIndex = modelPattern.indexOf("/");
|
|
||||||
if (slashIndex !== -1) {
|
|
||||||
const provider = modelPattern.substring(0, slashIndex);
|
|
||||||
const modelId = modelPattern.substring(slashIndex + 1);
|
|
||||||
const providerMatch = availableModels.find(
|
|
||||||
(m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),
|
|
||||||
);
|
|
||||||
if (providerMatch) {
|
|
||||||
return providerMatch;
|
|
||||||
}
|
|
||||||
// No exact provider/model match - fall through to other matching
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for exact ID match (case-insensitive)
|
|
||||||
const exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());
|
|
||||||
if (exactMatch) {
|
if (exactMatch) {
|
||||||
return exactMatch;
|
return exactMatch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,11 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||||||
|
|
||||||
private filterModels(query: string): void {
|
private filterModels(query: string): void {
|
||||||
this.filteredModels = query
|
this.filteredModels = query
|
||||||
? fuzzyFilter(this.activeModels, query, ({ id, provider }) => `${id} ${provider}`)
|
? fuzzyFilter(
|
||||||
|
this.activeModels,
|
||||||
|
query,
|
||||||
|
({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`,
|
||||||
|
)
|
||||||
: this.activeModels;
|
: this.activeModels;
|
||||||
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
||||||
this.updateList();
|
this.updateList();
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import type {
|
|||||||
import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.js";
|
import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.js";
|
||||||
import { type AppAction, KeybindingsManager } from "../../core/keybindings.js";
|
import { type AppAction, KeybindingsManager } from "../../core/keybindings.js";
|
||||||
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
||||||
import { resolveModelScope } from "../../core/model-resolver.js";
|
import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
||||||
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
||||||
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
||||||
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
||||||
@@ -3244,30 +3244,8 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async findExactModelMatch(searchTerm: string): Promise<Model<any> | undefined> {
|
private async findExactModelMatch(searchTerm: string): Promise<Model<any> | undefined> {
|
||||||
const term = searchTerm.trim();
|
|
||||||
if (!term) return undefined;
|
|
||||||
|
|
||||||
let targetProvider: string | undefined;
|
|
||||||
let targetModelId = "";
|
|
||||||
|
|
||||||
if (term.includes("/")) {
|
|
||||||
const parts = term.split("/", 2);
|
|
||||||
targetProvider = parts[0]?.trim().toLowerCase();
|
|
||||||
targetModelId = parts[1]?.trim().toLowerCase() ?? "";
|
|
||||||
} else {
|
|
||||||
targetModelId = term.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetModelId) return undefined;
|
|
||||||
|
|
||||||
const models = await this.getModelCandidates();
|
const models = await this.getModelCandidates();
|
||||||
const exactMatches = models.filter((item) => {
|
return findExactModelReferenceMatch(searchTerm, models);
|
||||||
const idMatch = item.id.toLowerCase() === targetModelId;
|
|
||||||
const providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider;
|
|
||||||
return idMatch && providerMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
return exactMatches.length === 1 ? exactMatches[0] : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getModelCandidates(): Promise<Model<any>[]> {
|
private async getModelCandidates(): Promise<Model<any>[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user