fix(ui): show Lighthouse provider and model names

This commit is contained in:
alejandrobailo
2026-06-30 11:00:24 +02:00
parent 3eb49f918e
commit aba1dd29ad
6 changed files with 153 additions and 32 deletions
@@ -49,14 +49,15 @@ describe("lighthouse-v2.adapter", () => {
it("should map supported provider and model payloads", () => {
// Given
const provider = {
id: "openai",
id: "openai_compatible",
type: "lighthouse-supported-providers",
attributes: { name: "OpenAI" },
attributes: { name: "OpenAI Compatible" },
};
const model = {
id: "gpt-5.5",
type: "lighthouse-supported-models",
attributes: {
model_name: "GPT 5.5",
max_input_tokens: 100000,
max_output_tokens: 8192,
supports_function_calling: true,
@@ -67,11 +68,12 @@ describe("lighthouse-v2.adapter", () => {
// When / Then
expect(mapLighthouseV2Provider(provider)).toEqual({
id: "openai",
name: "OpenAI",
id: "openai-compatible",
name: "OpenAI Compatible",
});
expect(mapLighthouseV2Model(model)).toEqual({
id: "gpt-5.5",
name: "GPT 5.5",
maxInputTokens: 100000,
maxOutputTokens: 8192,
supportsFunctionCalling: true,
@@ -30,7 +30,7 @@ export interface JsonApiDocument<TData> {
}
interface ConfigurationAttributes {
provider_type: LighthouseV2ProviderType;
provider_type: string;
base_url: string | null;
default_model?: string | null;
business_context?: string | null;
@@ -45,6 +45,8 @@ interface SupportedProviderAttributes {
}
interface SupportedModelAttributes {
model_name?: string | null;
name?: string | null;
max_input_tokens: number | null;
max_output_tokens: number | null;
supports_function_calling: boolean | null;
@@ -112,7 +114,9 @@ export function mapLighthouseV2Configuration(
): LighthouseV2Configuration {
return {
id: resource.id,
providerType: resource.attributes.provider_type,
providerType: normalizeLighthouseV2ProviderType(
resource.attributes.provider_type,
),
baseUrl: resource.attributes.base_url,
defaultModel: resource.attributes.default_model ?? null,
businessContext: resource.attributes.business_context ?? "",
@@ -127,7 +131,7 @@ export function mapLighthouseV2Provider(
resource: JsonApiResource<SupportedProviderAttributes>,
): LighthouseV2SupportedProvider {
return {
id: resource.id as LighthouseV2ProviderType,
id: normalizeLighthouseV2ProviderType(resource.id),
name: resource.attributes.name,
};
}
@@ -137,6 +141,8 @@ export function mapLighthouseV2Model(
): LighthouseV2SupportedModel {
return {
id: resource.id,
name:
resource.attributes.model_name ?? resource.attributes.name ?? resource.id,
maxInputTokens: resource.attributes.max_input_tokens,
maxOutputTokens: resource.attributes.max_output_tokens,
supportsFunctionCalling: resource.attributes.supports_function_calling,
@@ -344,3 +350,13 @@ function hasBedrockRegion(credentials: LighthouseV2Credentials): boolean {
"aws_region_name" in credentials && Boolean(credentials.aws_region_name)
);
}
function normalizeLighthouseV2ProviderType(
providerType: string,
): LighthouseV2ProviderType {
if (providerType === "openai_compatible") {
return LIGHTHOUSE_V2_PROVIDER_TYPE.OPENAI_COMPATIBLE;
}
return providerType as LighthouseV2ProviderType;
}
@@ -8,6 +8,7 @@ import type {
LighthouseV2Configuration,
LighthouseV2Message,
LighthouseV2SupportedModel,
LighthouseV2SupportedProvider,
} from "@/app/(prowler)/lighthouse/_types";
import { LighthouseV2ChatPage } from "./lighthouse-v2-chat-page";
@@ -117,6 +118,12 @@ const modelsByProvider = {
"openai-compatible": [model("llama-3.3")],
};
const supportedProviders: LighthouseV2SupportedProvider[] = [
{ id: "openai", name: "OpenAI" },
{ id: "bedrock", name: "AWS Bedrock" },
{ id: "openai-compatible", name: "OpenAI Compatible" },
];
describe("LighthouseV2ChatPage", () => {
beforeEach(() => {
vi.stubGlobal(
@@ -180,6 +187,84 @@ describe("LighthouseV2ChatPage", () => {
).toHaveAttribute("href", "/lighthouse/settings");
});
it("shows model names in the selector while keeping model ids for persistence", async () => {
// Given
const user = userEvent.setup();
renderPage({
configurations: [
{ ...configurations[0], defaultModel: "gpt-5.1" },
{
...configurations[1],
defaultModel: "us.anthropic.claude-sonnet-4-20250514-v1:0",
},
],
modelsByProvider: {
openai: [model("gpt-5.1", "GPT-5.1")],
bedrock: [
model(
"us.anthropic.claude-sonnet-4-20250514-v1:0",
"Claude Sonnet 4",
),
],
"openai-compatible": [],
},
});
// When
const modelSelector = screen.getByRole("combobox", { name: "Model" });
await user.click(modelSelector);
// Then
expect(modelSelector).toHaveTextContent("GPT-5.1");
expect(
await screen.findByRole("option", { name: "Claude Sonnet 4" }),
).toBeInTheDocument();
expect(
screen.queryByText("us.anthropic.claude-sonnet-4-20250514-v1:0"),
).not.toBeInTheDocument();
// When
await user.click(screen.getByRole("option", { name: "Claude Sonnet 4" }));
// Then
await waitFor(() =>
expect(updateConfigurationMock).toHaveBeenCalledWith("config-bedrock", {
defaultModel: "us.anthropic.claude-sonnet-4-20250514-v1:0",
}),
);
expect(modelSelector).toHaveTextContent("Claude Sonnet 4");
});
it("uses supported provider names as model selector section headings", async () => {
// Given
const user = userEvent.setup();
renderPage({
configurations: [
...configurations,
{
id: "config-openai-compatible",
providerType: "openai-compatible",
baseUrl: "https://example.com/v1",
defaultModel: "llama-3.3",
businessContext: "Production account",
connected: true,
connectionLastCheckedAt: "2026-06-22T10:00:00Z",
insertedAt: "2026-06-22T09:00:00Z",
updatedAt: "2026-06-22T10:00:00Z",
},
],
supportedProviders,
});
// When
await user.click(screen.getByRole("combobox", { name: "Model" }));
// Then
expect(await screen.findByText("AWS Bedrock")).toBeInTheDocument();
expect(screen.getByText("OpenAI Compatible")).toBeInTheDocument();
expect(screen.queryByText("Amazon Bedrock")).not.toBeInTheDocument();
});
it("uses the tuned scrollbar and bottom fade without a composer separator", () => {
// Given / When
const { container } = renderPage({
@@ -476,6 +561,7 @@ function renderPage(props?: RenderPageProps) {
const componentProps = {
configurations: props?.configurations ?? configurations,
modelsByProvider: props?.modelsByProvider ?? modelsByProvider,
supportedProviders: props?.supportedProviders ?? supportedProviders,
initialSessionId: props?.initialSessionId,
initialMessages: props?.initialMessages ?? [],
initialPrompt: props?.initialPrompt,
@@ -486,9 +572,10 @@ function renderPage(props?: RenderPageProps) {
return render(<LighthouseV2ChatPage {...componentProps} />);
}
function model(id: string): LighthouseV2SupportedModel {
function model(id: string, name = id): LighthouseV2SupportedModel {
return {
id,
name,
maxInputTokens: null,
maxOutputTokens: null,
supportsFunctionCalling: null,
@@ -42,6 +42,7 @@ import {
type LighthouseV2ProviderType,
type LighthouseV2SSEEvent,
type LighthouseV2SupportedModel,
type LighthouseV2SupportedProvider,
} from "@/app/(prowler)/lighthouse/_types";
import { Card } from "@/components/shadcn";
import {
@@ -61,6 +62,7 @@ interface LighthouseV2ChatPageProps {
LighthouseV2ProviderType,
LighthouseV2SupportedModel[]
>;
supportedProviders: LighthouseV2SupportedProvider[];
initialSessionId?: string;
initialMessages: LighthouseV2Message[];
initialActiveTaskId?: string | null;
@@ -71,6 +73,7 @@ interface LighthouseV2ChatPageProps {
export function LighthouseV2ChatPage({
configurations,
modelsByProvider,
supportedProviders,
initialSessionId,
initialMessages,
initialActiveTaskId,
@@ -112,6 +115,7 @@ export function LighthouseV2ChatPage({
const modelSelectorGroups = buildModelSelectorGroups(
connectedConfigurations,
modelsByProvider,
supportedProviders,
);
const selectedModelValue = selectedModelSelection
? buildLighthouseV2ModelSelectionValue(
@@ -526,29 +530,33 @@ function buildModelSelectorGroups(
LighthouseV2ProviderType,
LighthouseV2SupportedModel[]
>,
supportedProviders: LighthouseV2SupportedProvider[],
): ComboboxGroup[] {
return connectedConfigurations
.map((configuration) => ({
heading: getLighthouseV2ProviderLabel(configuration.providerType),
options: (modelsByProvider[configuration.providerType] ?? []).map(
(model) => ({
value: buildLighthouseV2ModelSelectionValue(
configuration.providerType,
model.id,
),
label: model.id,
}),
),
}))
.filter((group) => group.options.length > 0);
}
const groups: ComboboxGroup[] = [];
function getLighthouseV2ProviderLabel(providerType: LighthouseV2ProviderType) {
return LIGHTHOUSE_V2_PROVIDER_LABELS[providerType] ?? providerType;
}
for (const provider of supportedProviders) {
const configuration = connectedConfigurations.find(
(item) => item.providerType === provider.id,
);
if (!configuration) continue;
const LIGHTHOUSE_V2_PROVIDER_LABELS = {
openai: "OpenAI",
bedrock: "Amazon Bedrock",
"openai-compatible": "OpenAI Compatible",
} as const satisfies Record<LighthouseV2ProviderType, string>;
const options = (modelsByProvider[configuration.providerType] ?? []).map(
(model) => ({
value: buildLighthouseV2ModelSelectionValue(
configuration.providerType,
model.id,
),
label: model.name,
}),
);
if (options.length === 0) continue;
groups.push({
heading: provider.name,
options,
});
}
return groups;
}
@@ -64,6 +64,7 @@ export interface LighthouseV2SupportedProvider {
export interface LighthouseV2SupportedModel {
id: string;
name: string;
maxInputTokens: number | null;
maxOutputTokens: number | null;
supportsFunctionCalling: boolean | null;
+8 -1
View File
@@ -9,6 +9,7 @@ import {
getLighthouseV2Messages,
getLighthouseV2Session,
getLighthouseV2SupportedModels,
getLighthouseV2SupportedProviders,
} from "@/app/(prowler)/lighthouse/_actions";
import { LighthouseV2ChatPage } from "@/app/(prowler)/lighthouse/_components/chat";
import { LighthouseV2NavigationModeSync } from "@/app/(prowler)/lighthouse/_components/navigation";
@@ -36,9 +37,14 @@ export default async function AIChatbot({
typeof params.session === "string" ? params.session : undefined;
if (isCloud()) {
const configurationsResult = await getLighthouseV2Configurations();
const [configurationsResult, supportedProvidersResult] = await Promise.all([
getLighthouseV2Configurations(),
getLighthouseV2SupportedProviders(),
]);
const configurations =
"data" in configurationsResult ? configurationsResult.data : [];
const supportedProviders =
"data" in supportedProvidersResult ? supportedProvidersResult.data : [];
const connectedConfigurations = configurations.filter(
(configuration) => configuration.connected === true,
);
@@ -89,6 +95,7 @@ export default async function AIChatbot({
key={chatRouteKey}
configurations={configurations}
modelsByProvider={modelsByProvider}
supportedProviders={supportedProviders}
initialSessionId={activeSessionId}
initialMessages={
"data" in initialMessages ? initialMessages.data : []