From aa4fc0140a189f0ccfe97cd6b6bc23328c62ed33 Mon Sep 17 00:00:00 2001 From: Ritwij Aryan Parmar Date: Tue, 26 May 2026 01:10:19 -0400 Subject: [PATCH] fix(web): match MCP language models by identity --- packages/web/src/features/chat/utils.test.ts | 27 +++++++++++++++++++- packages/web/src/features/chat/utils.ts | 11 ++++++++ packages/web/src/features/mcp/askCodebase.ts | 4 +-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/web/src/features/chat/utils.test.ts b/packages/web/src/features/chat/utils.test.ts index 26359d2a9..5663de75a 100644 --- a/packages/web/src/features/chat/utils.test.ts +++ b/packages/web/src/features/chat/utils.test.ts @@ -1,5 +1,5 @@ import { expect, test, vi } from 'vitest' -import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences } from './utils' +import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, isSameLanguageModelIdentity, repairReferences } from './utils' import { FILE_REFERENCE_REGEX, ANSWER_TAG } from './constants'; import { SBChatMessage, SBChatMessagePart } from './types'; @@ -44,6 +44,31 @@ test('fileReferenceToString matches FILE_REFERENCE_REGEX', () => { }))).toBe(true); }); +test('isSameLanguageModelIdentity ignores displayName', () => { + expect(isSameLanguageModelIdentity( + { + provider: 'anthropic', + model: 'claude-opus-4-7', + displayName: 'Claude Opus 4.7 (slow, highest quality)', + }, + { + provider: 'anthropic', + model: 'claude-opus-4-7', + }, + )).toBe(true); + + expect(isSameLanguageModelIdentity( + { + provider: 'anthropic', + model: 'claude-opus-4-7', + }, + { + provider: 'anthropic', + model: 'claude-sonnet-4-6', + }, + )).toBe(false); +}); + test('groupMessageIntoSteps returns an empty array when there are no parts', () => { const parts: SBChatMessagePart[] = [] diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index 38dd784fd..026443f1e 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -371,6 +371,17 @@ export const getLanguageModelKey = (model: LanguageModelInfo) => { return `${model.provider}-${model.model}-${model.displayName}`; } +/** + * Checks whether two language model infos refer to the same configured model. + * + * Unlike getLanguageModelKey, this intentionally ignores displayName because + * MCP clients can select models using only the externally visible provider and + * model fields. + */ +export const isSameLanguageModelIdentity = (a: LanguageModelInfo, b: LanguageModelInfo) => { + return a.provider === b.provider && a.model === b.model; +} + /** * Given a file reference and a list of file sources, attempts to resolve the file source that the reference points to. */ diff --git a/packages/web/src/features/mcp/askCodebase.ts b/packages/web/src/features/mcp/askCodebase.ts index 810010bcc..2678e00bf 100644 --- a/packages/web/src/features/mcp/askCodebase.ts +++ b/packages/web/src/features/mcp/askCodebase.ts @@ -1,7 +1,7 @@ import { sew } from "@/middleware/sew"; import { getConfiguredLanguageModels, getAISDKLanguageModelAndOptions, generateChatNameFromMessage, updateChatMessages } from "@/features/chat/utils.server"; import { LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; -import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; +import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, isSameLanguageModelIdentity } from "@/features/chat/utils"; import { ErrorCode } from "@/lib/errorCodes"; import { ServiceError, ServiceErrorException } from "@/lib/serviceError"; import { withOptionalAuth } from "@/middleware/withAuth"; @@ -58,7 +58,7 @@ export const askCodebase = (params: AskCodebaseParams): Promise getLanguageModelKey(m) === getLanguageModelKey(requestedLanguageModel) + (m) => isSameLanguageModelIdentity(m, requestedLanguageModel) ); if (!matchingModel) { return {