From 26120bb5c840f7e0ae94d750f857e0c554cfba0b Mon Sep 17 00:00:00 2001 From: Harsh Kashyap Date: Thu, 2 Jul 2026 11:39:24 +0530 Subject: [PATCH 1/3] fix(web): resolve MCP language model lookups without displayName --- .../web/src/ee/features/mcp/askCodebase.ts | 79 +++++++++++++++++-- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/packages/web/src/ee/features/mcp/askCodebase.ts b/packages/web/src/ee/features/mcp/askCodebase.ts index 35337d29f..84dd07a97 100644 --- a/packages/web/src/ee/features/mcp/askCodebase.ts +++ b/packages/web/src/ee/features/mcp/askCodebase.ts @@ -4,7 +4,7 @@ import { generateChatNameFromMessage } from "@/ee/features/chat/llm.server"; import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server"; import { resolveContextWindow } from "@/features/chat/modelContextWindow.server"; import { LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; -import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; +import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage } from "@/features/chat/utils"; import { resolveModelCapabilities } from "@/features/chat/modelCapabilities.server"; import { ErrorCode } from "@/lib/errorCodes"; import { ServiceError, ServiceErrorException } from "@/lib/serviceError"; @@ -20,6 +20,7 @@ import { createMessageStream } from "@/ee/features/chat/agent"; import { getPromptCacheStrategy } from "@/ee/features/chat/promptCaching"; const logger = createLogger('ask-codebase-api'); +type ConfiguredLanguageModel = Awaited>[number]; export type AskCodebaseParams = { query: string; @@ -36,6 +37,64 @@ export type AskCodebaseResult = { languageModel: LanguageModelInfo; }; +const formatLanguageModelName = (model: Pick) => + `${model.provider}/${model.model}`; + +const formatConfiguredLanguageModelLabel = (model: Pick) => + model.displayName ? `'${model.displayName}'` : formatLanguageModelName(model); + +export const selectConfiguredLanguageModel = ( + configuredModels: ConfiguredLanguageModel[], + requestedLanguageModel: Pick +): { + languageModelConfig?: ConfiguredLanguageModel; + error?: ServiceError; +} => { + const candidateModels = configuredModels.filter( + (model) => model.provider === requestedLanguageModel.provider && model.model === requestedLanguageModel.model + ); + if (candidateModels.length === 0) { + return { + error: { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: `Language model '${formatLanguageModelName(requestedLanguageModel)}' is not configured.`, + } satisfies ServiceError, + }; + } + + if (requestedLanguageModel.displayName) { + const matchingModel = candidateModels.find( + (model) => model.displayName === requestedLanguageModel.displayName + ); + if (!matchingModel) { + const availableDisplayNames = candidateModels.map(formatConfiguredLanguageModelLabel).join(', '); + return { + error: { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: `Language model '${formatLanguageModelName(requestedLanguageModel)}' is configured, but not with displayName '${requestedLanguageModel.displayName}'. Available matches: ${availableDisplayNames}.`, + } satisfies ServiceError, + }; + } + + return { languageModelConfig: matchingModel }; + } + + if (candidateModels.length > 1) { + const availableDisplayNames = candidateModels.map(formatConfiguredLanguageModelLabel).join(', '); + return { + error: { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: `Language model '${formatLanguageModelName(requestedLanguageModel)}' matches multiple configured models. Pass displayName to disambiguate. Available matches: ${availableDisplayNames}.`, + } satisfies ServiceError, + }; + } + + return { languageModelConfig: candidateModels[0] }; +}; + const blockStreamUntilFinish = async >( stream: ReadableStream> ) => { @@ -71,17 +130,21 @@ export const askCodebase = (params: AskCodebaseParams): Promise getLanguageModelKey(m) === getLanguageModelKey(requestedLanguageModel) + const { languageModelConfig: selectedLanguageModel, error } = selectConfiguredLanguageModel( + configuredModels, + requestedLanguageModel ); - if (!matchingModel) { + if (error) { + return error; + } + if (!selectedLanguageModel) { return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.INVALID_REQUEST_BODY, - message: `Language model '${requestedLanguageModel.provider}/${requestedLanguageModel.model}' is not configured.`, + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + errorCode: ErrorCode.UNEXPECTED_ERROR, + message: "Failed to resolve the requested language model.", } satisfies ServiceError; } - languageModelConfig = matchingModel; + languageModelConfig = selectedLanguageModel; } const { model, providerOptions, temperature } = await getAISDKLanguageModelAndOptions(languageModelConfig); From ff5c1c55b986dd5a0f8af8905397970da7abb678 Mon Sep 17 00:00:00 2001 From: Harsh Kashyap Date: Thu, 2 Jul 2026 11:39:29 +0530 Subject: [PATCH 2/3] test(web): cover MCP language model disambiguation --- .../src/ee/features/mcp/askCodebase.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 packages/web/src/ee/features/mcp/askCodebase.test.ts diff --git a/packages/web/src/ee/features/mcp/askCodebase.test.ts b/packages/web/src/ee/features/mcp/askCodebase.test.ts new file mode 100644 index 000000000..37c53c4f6 --- /dev/null +++ b/packages/web/src/ee/features/mcp/askCodebase.test.ts @@ -0,0 +1,85 @@ +import { ErrorCode } from "@/lib/errorCodes"; +import { StatusCodes } from "http-status-codes"; +import { describe, expect, it } from "vitest"; +import { selectConfiguredLanguageModel } from "./askCodebase"; + +type ConfiguredLanguageModel = Parameters[0][number]; + +const createConfiguredModel = (overrides: Partial<{ + provider: string; + model: string; + displayName?: string; +}> = {}) => ({ + provider: 'anthropic', + model: 'claude-opus-4-7', + displayName: 'Claude Opus 4.7', + ...overrides, +}) as ConfiguredLanguageModel; + +describe("selectConfiguredLanguageModel", () => { + it("matches a configured model by provider and model when displayName is omitted", () => { + const configuredModel = createConfiguredModel(); + + const result = selectConfiguredLanguageModel( + [configuredModel], + { provider: configuredModel.provider, model: configuredModel.model } + ); + + expect(result).toEqual({ languageModelConfig: configuredModel }); + }); + + it("uses displayName to disambiguate duplicate provider/model entries", () => { + const firstModel = createConfiguredModel({ displayName: 'Claude Opus 4.7 (slow)' }); + const secondModel = createConfiguredModel({ displayName: 'Claude Opus 4.7 (fast)' }); + + const result = selectConfiguredLanguageModel( + [firstModel, secondModel], + { + provider: firstModel.provider, + model: firstModel.model, + displayName: secondModel.displayName, + } + ); + + expect(result).toEqual({ languageModelConfig: secondModel }); + }); + + it("returns a disambiguation error when multiple configured models match", () => { + const firstModel = createConfiguredModel({ displayName: 'Claude Opus 4.7 (slow)' }); + const secondModel = createConfiguredModel({ displayName: 'Claude Opus 4.7 (fast)' }); + + const result = selectConfiguredLanguageModel( + [firstModel, secondModel], + { provider: firstModel.provider, model: firstModel.model } + ); + + expect(result).toEqual({ + error: { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: "Language model 'anthropic/claude-opus-4-7' matches multiple configured models. Pass displayName to disambiguate. Available matches: 'Claude Opus 4.7 (slow)', 'Claude Opus 4.7 (fast)'.", + }, + }); + }); + + it("returns an error when displayName does not match any configured model", () => { + const configuredModel = createConfiguredModel(); + + const result = selectConfiguredLanguageModel( + [configuredModel], + { + provider: configuredModel.provider, + model: configuredModel.model, + displayName: 'Claude Sonnet 4.6', + } + ); + + expect(result).toEqual({ + error: { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: "Language model 'anthropic/claude-opus-4-7' is configured, but not with displayName 'Claude Sonnet 4.6'. Available matches: 'Claude Opus 4.7'.", + }, + }); + }); +}); From 72b615de238dc28f73582e00129f913c00912fa1 Mon Sep 17 00:00:00 2001 From: Harsh Kashyap Date: Thu, 2 Jul 2026 11:41:56 +0530 Subject: [PATCH 3/3] chore: add changelog entry for MCP language model selection fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea80d6c5e..cf51010ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Maintained the sidebar scroll position when navigating between chats instead of resetting to the top. [#1411](https://github.com/sourcebot-dev/sourcebot/pull/1411) - Upgraded `nodemailer` to `^9.0.1`. [#1356](https://github.com/sourcebot-dev/sourcebot/pull/1356) - Upgraded `@opentelemetry/core` to `^2.8.0`. [#1413](https://github.com/sourcebot-dev/sourcebot/pull/1413) +- [EE] Fixed `ask_codebase` language model selection to match configured models by `provider` and `model`, only requiring `displayName` when multiple configurations share the same pair. [#1414](https://github.com/sourcebot-dev/sourcebot/pull/1414) ## [5.0.4] - 2026-06-18