diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index b0e7743f..34d16045 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -56,27 +56,61 @@ function isAlias(id: string): boolean { 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[], +): Model | 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. * Returns the matched model or undefined if no match found. */ function tryMatchModel(modelPattern: string, availableModels: Model[]): Model | undefined { - // Check for provider/modelId format (provider is everything before the first /) - 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()); + const exactMatch = findExactModelReferenceMatch(modelPattern, availableModels); if (exactMatch) { return exactMatch; } diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index db6bc6c9..e1ceeaf1 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -212,7 +212,11 @@ export class ModelSelectorComponent extends Container implements Focusable { private filterModels(query: string): void { 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.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1)); this.updateList(); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 8b3377ef..c5981f15 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -56,7 +56,7 @@ import type { import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.js"; import { type AppAction, KeybindingsManager } from "../../core/keybindings.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 SessionContext, SessionManager } from "../../core/session-manager.js"; import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js"; @@ -3244,30 +3244,8 @@ export class InteractiveMode { } private async findExactModelMatch(searchTerm: string): Promise | 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 exactMatches = models.filter((item) => { - const idMatch = item.id.toLowerCase() === targetModelId; - const providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider; - return idMatch && providerMatch; - }); - - return exactMatches.length === 1 ? exactMatches[0] : undefined; + return findExactModelReferenceMatch(searchTerm, models); } private async getModelCandidates(): Promise[]> {