Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
85 changes: 85 additions & 0 deletions packages/web/src/ee/features/mcp/askCodebase.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof selectConfiguredLanguageModel>[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'.",
},
});
});
});
79 changes: 71 additions & 8 deletions packages/web/src/ee/features/mcp/askCodebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<ReturnType<typeof getConfiguredLanguageModels>>[number];

export type AskCodebaseParams = {
query: string;
Expand All @@ -36,6 +37,64 @@ export type AskCodebaseResult = {
languageModel: LanguageModelInfo;
};

const formatLanguageModelName = (model: Pick<LanguageModelInfo, 'provider' | 'model'>) =>
`${model.provider}/${model.model}`;

const formatConfiguredLanguageModelLabel = (model: Pick<LanguageModelInfo, 'provider' | 'model' | 'displayName'>) =>
model.displayName ? `'${model.displayName}'` : formatLanguageModelName(model);

export const selectConfiguredLanguageModel = (
configuredModels: ConfiguredLanguageModel[],
requestedLanguageModel: Pick<LanguageModelInfo, 'provider' | 'model' | 'displayName'>
): {
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 <T extends UIMessage<unknown, UIDataTypes, UITools>>(
stream: ReadableStream<InferUIMessageChunk<T>>
) => {
Expand Down Expand Up @@ -71,17 +130,21 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul

let languageModelConfig = configuredModels[0];
if (requestedLanguageModel) {
const matchingModel = configuredModels.find(
(m) => 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);
Expand Down