From aba1dd29adf7da1a1476cd0148eb460fa8c3329c Mon Sep 17 00:00:00 2001 From: alejandrobailo Date: Tue, 30 Jun 2026 11:00:24 +0200 Subject: [PATCH] fix(ui): show Lighthouse provider and model names --- .../_actions/lighthouse-v2.adapter.test.ts | 10 ++- .../_actions/lighthouse-v2.adapter.ts | 22 ++++- .../chat/lighthouse-v2-chat-page.test.tsx | 89 ++++++++++++++++++- .../chat/lighthouse-v2-chat-page.tsx | 54 ++++++----- ui/app/(prowler)/lighthouse/_types/config.ts | 1 + ui/app/(prowler)/lighthouse/page.tsx | 9 +- 6 files changed, 153 insertions(+), 32 deletions(-) diff --git a/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.adapter.test.ts b/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.adapter.test.ts index 07c8b7c2c8..09006354e5 100644 --- a/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.adapter.test.ts +++ b/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.adapter.test.ts @@ -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, diff --git a/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.adapter.ts b/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.adapter.ts index be7538011a..448057e83e 100644 --- a/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.adapter.ts +++ b/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.adapter.ts @@ -30,7 +30,7 @@ export interface JsonApiDocument { } 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, ): 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; +} diff --git a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.test.tsx b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.test.tsx index 9a33af0fb2..aa376c97e2 100644 --- a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.test.tsx +++ b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.test.tsx @@ -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(); } -function model(id: string): LighthouseV2SupportedModel { +function model(id: string, name = id): LighthouseV2SupportedModel { return { id, + name, maxInputTokens: null, maxOutputTokens: null, supportsFunctionCalling: null, diff --git a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.tsx b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.tsx index 703145f2ca..4f59c963d9 100644 --- a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.tsx +++ b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.tsx @@ -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; + 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; +} diff --git a/ui/app/(prowler)/lighthouse/_types/config.ts b/ui/app/(prowler)/lighthouse/_types/config.ts index 66e9acd27d..785f3ee92f 100644 --- a/ui/app/(prowler)/lighthouse/_types/config.ts +++ b/ui/app/(prowler)/lighthouse/_types/config.ts @@ -64,6 +64,7 @@ export interface LighthouseV2SupportedProvider { export interface LighthouseV2SupportedModel { id: string; + name: string; maxInputTokens: number | null; maxOutputTokens: number | null; supportsFunctionCalling: boolean | null; diff --git a/ui/app/(prowler)/lighthouse/page.tsx b/ui/app/(prowler)/lighthouse/page.tsx index 070f7ee7fc..ddf3f5a763 100644 --- a/ui/app/(prowler)/lighthouse/page.tsx +++ b/ui/app/(prowler)/lighthouse/page.tsx @@ -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 : []