Skip to content

fix(mcp): match language model by provider+model, fall back when displayName absent#1153

Open
dmitrievav wants to merge 2 commits intosourcebot-dev:mainfrom
dmitrievav:fix/ask-codebase-language-model-match
Open

fix(mcp): match language model by provider+model, fall back when displayName absent#1153
dmitrievav wants to merge 2 commits intosourcebot-dev:mainfrom
dmitrievav:fix/ask-codebase-language-model-match

Conversation

@dmitrievav
Copy link
Copy Markdown

@dmitrievav dmitrievav commented Apr 26, 2026

Problem

ask_codebase rejects any explicit languageModel with a 400, even when list_language_models returns the exact model. Reported in #1137.

Root cause: askCodebase.ts uses getLanguageModelKey to match the requested model against configured ones. That key includes displayName:

// chat/utils.ts
export const getLanguageModelKey = (model: LanguageModelInfo) =>
    `${model.provider}-${model.model}-${model.displayName}`;

But the MCP package (@sourcebot/mcp) explicitly omits displayName from the ask_codebase request schema:

// @sourcebot/mcp dist/schemas.js
export const askCodebaseRequestSchema = z.object({
    languageModel: languageModelInfoSchema.omit({ displayName: true })

So an LLM agent can only pass {provider, model}displayName is always undefined on the caller's side. Match always fails:

  • configured key: anthropic-claude-opus-4-7-Claude Opus 4.7 (slow, highest quality)
  • requested key: anthropic-claude-opus-4-7-undefined

Fix

Replace the getLanguageModelKey-based match with a two-step approach:

  1. Filter configured models by provider + modelcandidates.
  2. displayName !== undefined (strict, not truthiness — so "" is treated as an explicit value).
  3. If displayName provided → exact match within candidates. Preserves disambiguation between same-model entries differing in displayName (e.g. two opus-4-7 with different reasoningEffort).
  4. If displayName absent and exactly one candidate → use it.
  5. If displayName absent and multiple candidates → 400 with one of two distinct messages:
    • some candidates have displayName → list them and ask caller to disambiguate
    • no candidate has displayName → tell the operator to add displayName to the config

Three distinct 400 messages:

Scenario Message
provider+model not found Language model 'X/Y' is not configured.
provider+model found, wrong displayName … is configured but displayName 'Z' was not found. Available: "A", "B".
Multiple candidates, caller sent no displayName, some have displayName Multiple configurations found for 'X/Y'. Provide a displayName to disambiguate. Available: "A", "B".
Multiple candidates, none have displayName Multiple configurations found for 'X/Y' but none define a displayName. Add a unique displayName to each configuration entry to enable disambiguation.

Also removes the now-unused getLanguageModelKey import.

Test plan

  • {provider, model}, one matching config, no displayName → succeeds
  • {provider, model, displayName: ""} → treated as explicit empty-string match
  • {provider, model, displayName} → exact match on all three fields
  • {provider, model, displayName} wrong name → 400 listing available displayNames
  • Two configs same provider+model, different displayName, no displayName sent → 400 with hint listing available names
  • Two configs same provider+model, different displayName, correct displayName sent → correct model selected
  • Two configs same provider+model, neither has displayName → 400 telling operator to add displayName to config
  • provider+model not configured at all → plain "not configured" message
  • No languageModel param → falls back to first configured model (unchanged)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

askCodebase now selects models by matching {provider, model} against configuredModels and applies displayName only for disambiguation. If selection fails, it returns distinct BAD_REQUEST errors for zero candidates, displayName mismatches (listing available names), or multiple candidates requiring a displayName.

Changes

Cohort / File(s) Summary
Model Matching Logic
packages/web/src/features/mcp/askCodebase.ts
Replaces getLanguageModelKey-based resolution with direct {provider, model} candidate derivation from configuredModels. Applies displayName exact-match when provided; otherwise requires a single candidate. Expands BAD_REQUEST messages into three specific cases and removes reliance on getLanguageModelKey.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing language model matching logic to work with provider+model and fallback behavior when displayName is absent.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/web/src/features/mcp/askCodebase.ts (1)

60-72: Matching semantics now diverge from other call sites; consider consolidating.

The new predicate fixes the MCP case correctly, but packages/web/src/app/api/(server)/chat/route.ts and packages/web/src/features/chat/useSelectedLanguageModel.ts still match via getLanguageModelKey (strict provider+model+displayName). With this change, the same {provider, model} payload that resolves successfully in MCP could be rejected (or reset) on those paths, and vice‑versa when two configured models share provider+model but differ in displayName.

Two small follow-ups worth considering:

  1. Extract a shared matcher (e.g. matchesLanguageModel(configured, requested)) in @/features/chat/utils and reuse it from both the chat route and useSelectedLanguageModel, so MCP and the web client agree on what "same model" means.
  2. The error message on line 70 omits displayName even when the caller supplied one, which makes "configured but displayName mismatch" hard to diagnose — including it when present would help.
♻️ Suggested error-message tweak
-                if (!matchingModel) {
-                    return {
-                        statusCode: StatusCodes.BAD_REQUEST,
-                        errorCode: ErrorCode.INVALID_REQUEST_BODY,
-                        message: `Language model '${requestedLanguageModel.provider}/${requestedLanguageModel.model}' is not configured.`,
-                    } satisfies ServiceError;
-                }
+                if (!matchingModel) {
+                    const suffix = requestedLanguageModel.displayName
+                        ? ` (displayName: '${requestedLanguageModel.displayName}')`
+                        : '';
+                    return {
+                        statusCode: StatusCodes.BAD_REQUEST,
+                        errorCode: ErrorCode.INVALID_REQUEST_BODY,
+                        message: `Language model '${requestedLanguageModel.provider}/${requestedLanguageModel.model}'${suffix} is not configured.`,
+                    } satisfies ServiceError;
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/mcp/askCodebase.ts` around lines 60 - 72, The
current matching predicate used when finding matchingModel (checking
provider+model and optional displayName) diverges from other call sites that use
getLanguageModelKey and can cause inconsistent behavior; replace the inline
predicate by extracting and reusing a single matcher (e.g.
matchesLanguageModel(configured, requested)) from a shared utilities module
(suggested name: matchesLanguageModel in features/chat/utils) and update
configuredModels.find to use it, then update the chat route and
useSelectedLanguageModel to call the same matcher so all paths agree on equality
semantics; also enhance the error message constructed for missing matchingModel
to include requestedLanguageModel.displayName when present so displayName
mismatches are clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/web/src/features/mcp/askCodebase.ts`:
- Around line 60-72: The current matching predicate used when finding
matchingModel (checking provider+model and optional displayName) diverges from
other call sites that use getLanguageModelKey and can cause inconsistent
behavior; replace the inline predicate by extracting and reusing a single
matcher (e.g. matchesLanguageModel(configured, requested)) from a shared
utilities module (suggested name: matchesLanguageModel in features/chat/utils)
and update configuredModels.find to use it, then update the chat route and
useSelectedLanguageModel to call the same matcher so all paths agree on equality
semantics; also enhance the error message constructed for missing matchingModel
to include requestedLanguageModel.displayName when present so displayName
mismatches are clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1891842-b341-4835-aa56-856add137e4b

📥 Commits

Reviewing files that changed from the base of the PR and between 105ddf4 and 502b4ac.

📒 Files selected for processing (1)
  • packages/web/src/features/mcp/askCodebase.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/web/src/features/mcp/askCodebase.ts (1)

73-87: Minor: extract the duplicated available construction.

The available displayName list is built identically in both the displayNameProvided and the multi-candidate branches. A small helper would remove the duplication and keep the branches focused on message wording.

♻️ Suggested refactor
-                if (!matchingModel) {
-                    let message: string;
-                    if (candidates.length === 0) {
-                        message = `Language model '${requestedLanguageModel.provider}/${requestedLanguageModel.model}' is not configured.`;
-                    } else if (displayNameProvided) {
-                        const available = candidates
-                            .map((m) => m.displayName)
-                            .filter((n): n is string => n !== undefined)
-                            .map((n) => `"${n}"`)
-                            .join(', ');
-                        message = `Language model '${requestedLanguageModel.provider}/${requestedLanguageModel.model}' is configured but displayName '${requestedLanguageModel.displayName}' was not found.`
-                            + (available ? ` Available: ${available}.` : '');
-                    } else {
-                        const available = candidates
-                            .map((m) => m.displayName)
-                            .filter((n): n is string => n !== undefined)
-                            .map((n) => `"${n}"`)
-                            .join(', ');
-                        message = `Multiple configurations found for '${requestedLanguageModel.provider}/${requestedLanguageModel.model}'. Provide a displayName to disambiguate.`
-                            + (available ? ` Available: ${available}.` : '');
-                    }
+                if (!matchingModel) {
+                    const modelId = `${requestedLanguageModel.provider}/${requestedLanguageModel.model}`;
+                    const available = candidates
+                        .map((m) => m.displayName)
+                        .filter((n): n is string => n !== undefined)
+                        .map((n) => `"${n}"`)
+                        .join(', ');
+                    const availableSuffix = available ? ` Available: ${available}.` : '';
+                    let message: string;
+                    if (candidates.length === 0) {
+                        message = `Language model '${modelId}' is not configured.`;
+                    } else if (displayNameProvided) {
+                        message = `Language model '${modelId}' is configured but displayName '${requestedLanguageModel.displayName}' was not found.${availableSuffix}`;
+                    } else {
+                        message = `Multiple configurations found for '${modelId}'. Provide a displayName to disambiguate.${availableSuffix}`;
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/mcp/askCodebase.ts` around lines 73 - 87, Extract
the duplicated logic that builds the available displayName list into a small
helper function (e.g., buildAvailableList or formatCandidateDisplayNames) and
call it from both branches instead of repeating the map/filter/map/join chain;
update the code paths that currently compute `available` (the blocks that
reference `candidates` and produce the quoted names used in the message for both
the `displayNameProvided` and multi-candidate branches) to call that helper and
keep the message construction lines unchanged except for using the helper's
result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/features/mcp/askCodebase.ts`:
- Around line 80-87: The message shown when multiple configs match (constructed
around message, candidates, available, and requestedLanguageModel) is misleading
if all candidates have undefined displayName; update the logic that builds
available and message so that when available is empty you set a specific hint
like “Multiple configurations found for '<provider>/<model>' but none define a
displayName; add a unique displayName to each config to disambiguate” instead of
asking the caller to supply a displayName, otherwise keep the existing hint that
lists available displayNames when present.

---

Nitpick comments:
In `@packages/web/src/features/mcp/askCodebase.ts`:
- Around line 73-87: Extract the duplicated logic that builds the available
displayName list into a small helper function (e.g., buildAvailableList or
formatCandidateDisplayNames) and call it from both branches instead of repeating
the map/filter/map/join chain; update the code paths that currently compute
`available` (the blocks that reference `candidates` and produce the quoted names
used in the message for both the `displayNameProvided` and multi-candidate
branches) to call that helper and keep the message construction lines unchanged
except for using the helper's result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f391926-a0db-4522-91df-b0f6fa2218c2

📥 Commits

Reviewing files that changed from the base of the PR and between 54ed3cb and 804183f.

📒 Files selected for processing (1)
  • packages/web/src/features/mcp/askCodebase.ts

Comment thread packages/web/src/features/mcp/askCodebase.ts Outdated
…essages

The MCP package (@sourcebot/mcp) omits displayName from the ask_codebase
request schema (.omit({ displayName: true })), so LLM agents can only
pass {provider, model}. The previous getLanguageModelKey-based match
included displayName in the key, causing a permanent 400 for any explicit
model selection.

New matching logic:
- filter configured models by provider+model to collect candidates
- displayName check uses !== undefined (not truthiness) so "" is treated
  as an explicit value, not absent
- if displayName provided: exact match within candidates — preserves
  disambiguation between same-model entries with different displayName
  (e.g. two opus-4-7 with different reasoningEffort)
- if displayName absent and exactly one candidate: use it
- if displayName absent and multiple candidates: return 400 with hint

Three distinct 400 messages:
- provider+model not found: "Language model 'X/Y' is not configured."
- displayName mismatch: "… is configured but displayName 'Z' was not
  found. Available: "A", "B"."
- ambiguous: "Multiple configurations found for 'X/Y'. Provide a
  displayName to disambiguate. Available: "A", "B"."

Available displayNames are quoted and filtered so undefined values never
appear in error messages.

Also removes the now-unused getLanguageModelKey import.

Fixes sourcebot-dev#1137
@dmitrievav dmitrievav force-pushed the fix/ask-codebase-language-model-match branch from 8f3ff73 to 37d8793 Compare April 26, 2026 13:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/web/src/features/mcp/askCodebase.ts (1)

80-83: ⚠️ Potential issue | 🟡 Minor

Multi-candidate hint is still unactionable when no candidate has a displayName.

When two configs share provider+model and neither defines a displayName, available is empty and the message becomes "Multiple configurations found for '…'. Provide a displayName to disambiguate." — but the caller cannot, since none exist server-side. This branch should detect the empty-available sub-case and instruct the operator to add unique displayNames in config.

🛠️ Suggested tweak
                     } else {
-                        message = `Multiple configurations found for '${requestedLanguageModel.provider}/${requestedLanguageModel.model}'. Provide a displayName to disambiguate.`
-                            + (available ? ` Available: ${available}.` : '');
+                        message = available
+                            ? `Multiple configurations found for '${requestedLanguageModel.provider}/${requestedLanguageModel.model}'. Provide a displayName to disambiguate. Available: ${available}.`
+                            : `Multiple configurations found for '${requestedLanguageModel.provider}/${requestedLanguageModel.model}' but none define a displayName; add a unique displayName to each config to disambiguate.`;
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/mcp/askCodebase.ts` around lines 80 - 83, The
message for multiple matching configs (built where the code sets message for
requestedLanguageModel.provider/model) must handle the case when available is
empty; update the else branch that currently composes message = `Multiple
configurations found for
'${requestedLanguageModel.provider}/${requestedLanguageModel.model}'...` so that
if available is falsy/empty it produces a clear operator-actionable string like
"Multiple configurations found for '<provider>/<model>' but none expose a
displayName — add unique displayName fields to your server-side configs to
disambiguate." Otherwise keep the existing message that lists available
displayNames; adjust the condition around the available variable used there (in
askCodebase.ts) to choose the appropriate message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/features/mcp/askCodebase.ts`:
- Around line 60-67: The chat route currently compares models using
getLanguageModelKey(languageModel) which fails when displayName is undefined;
update the matching logic in the chat route (where configuredModels,
languageModel, and getLanguageModelKey are used) to mirror the MCP fix: filter
configuredModels by provider and model to produce candidates, check if
displayName is provided on languageModel, and if so find the candidate with the
same displayName, otherwise only accept a match when candidates.length === 1;
replace the current getLanguageModelKey equality check with this
displayName-aware candidate selection to avoid `"...-undefined"` mismatches.

---

Duplicate comments:
In `@packages/web/src/features/mcp/askCodebase.ts`:
- Around line 80-83: The message for multiple matching configs (built where the
code sets message for requestedLanguageModel.provider/model) must handle the
case when available is empty; update the else branch that currently composes
message = `Multiple configurations found for
'${requestedLanguageModel.provider}/${requestedLanguageModel.model}'...` so that
if available is falsy/empty it produces a clear operator-actionable string like
"Multiple configurations found for '<provider>/<model>' but none expose a
displayName — add unique displayName fields to your server-side configs to
disambiguate." Otherwise keep the existing message that lists available
displayNames; adjust the condition around the available variable used there (in
askCodebase.ts) to choose the appropriate message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26680aa1-457a-4d7f-8436-ef1970515593

📥 Commits

Reviewing files that changed from the base of the PR and between 8f3ff73 and 37d8793.

📒 Files selected for processing (1)
  • packages/web/src/features/mcp/askCodebase.ts

Comment on lines +60 to +67
const candidates = configuredModels.filter(
(m) => m.provider === requestedLanguageModel.provider &&
m.model === requestedLanguageModel.model
);
const displayNameProvided = requestedLanguageModel.displayName !== undefined;
const matchingModel = displayNameProvided
? candidates.find((m) => m.displayName === requestedLanguageModel.displayName)
: candidates.length === 1 ? candidates[0] : undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the chat route and its request schema to see if the same key-based
# match has the same displayName-undefined failure mode.
fd -t f 'route.ts' packages/web/src/app/api | xargs rg -nP -C5 'getLanguageModelKey'
rg -nP -C3 'languageModel\s*[:?]' packages/web/src/app/api/\(server\)/chat
rg -nP -C3 'displayName' packages/web/src/app/api/\(server\)/chat

Repository: sourcebot-dev/sourcebot

Length of output: 4415


🏁 Script executed:

# First, let's understand what getLanguageModelKey does
rg -nP -A10 'function getLanguageModelKey|export.*getLanguageModelKey|const getLanguageModelKey' packages/web/src/features/chat/utils.ts

Repository: sourcebot-dev/sourcebot

Length of output: 629


🏁 Script executed:

# Check the full schema definition for languageModel in the chat request
rg -nP -A20 'additionalChatRequestParamsSchema' packages/web/src/features/chat/types.ts

Repository: sourcebot-dev/sourcebot

Length of output: 342


🏁 Script executed:

# Look at the languageModelInfoSchema to understand displayName handling
rg -nP -B5 -A15 'languageModelInfoSchema' packages/web/src/features/chat/types.ts

Repository: sourcebot-dev/sourcebot

Length of output: 1251


🏁 Script executed:

# Compare the MCP request schema for askCodebase
rg -nP -B5 -A15 'languageModel' packages/web/src/features/mcp/types.ts | head -60

Repository: sourcebot-dev/sourcebot

Length of output: 49


🏁 Script executed:

# Look at the actual MCP askCodebase.ts around lines 60-67 to see the fix
cat -n packages/web/src/features/mcp/askCodebase.ts | sed -n '50,80p'

Repository: sourcebot-dev/sourcebot

Length of output: 2263


🏁 Script executed:

# Check if there are other routes/API handlers using getLanguageModelKey with similar pattern
rg -nP 'getLanguageModelKey' packages/web/src --type ts --type tsx -B3 -A3

Repository: sourcebot-dev/sourcebot

Length of output: 95


🏁 Script executed:

# Look for any tests or comments explaining the bug that was fixed
rg -nP 'displayName|getLanguageModelKey' packages/web/src/features/chat/utils.ts -B5 -A5 | head -40

Repository: sourcebot-dev/sourcebot

Length of output: 1070


Chat route has the same bug and needs the same fix.

The chat route at packages/web/src/app/api/(server)/chat/route.ts:70 uses getLanguageModelKey(model) === getLanguageModelKey(languageModel) which breaks when displayName is undefined and multiple configured models share the same provider/model. Since getLanguageModelKey includes displayName in the key (${provider}-${model}-${displayName}), an undefined value creates a key like "provider-model-undefined" that never matches any configured model, causing a 400 error.

The MCP fix at lines 64-67 explicitly handles this by checking if displayName is provided and only accepting a single candidate when it's not. The chat route should be updated to use the same logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/mcp/askCodebase.ts` around lines 60 - 67, The chat
route currently compares models using getLanguageModelKey(languageModel) which
fails when displayName is undefined; update the matching logic in the chat route
(where configuredModels, languageModel, and getLanguageModelKey are used) to
mirror the MCP fix: filter configuredModels by provider and model to produce
candidates, check if displayName is provided on languageModel, and if so find
the candidate with the same displayName, otherwise only accept a match when
candidates.length === 1; replace the current getLanguageModelKey equality check
with this displayName-aware candidate selection to avoid `"...-undefined"`
mismatches.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant