mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
fix(ui): load Lighthouse models only for connected providers
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type {
|
||||
LighthouseV2Configuration,
|
||||
LighthouseV2ProviderType,
|
||||
LighthouseV2SupportedModel,
|
||||
} from "@/app/(prowler)/lighthouse/_types";
|
||||
|
||||
import { loadLighthouseV2ConnectedModels } from "./model-loading";
|
||||
|
||||
describe("loadLighthouseV2ConnectedModels", () => {
|
||||
it("loads models only for connected configurations and initializes the rest as empty", async () => {
|
||||
// Given
|
||||
const openAIModel = model("gpt-5.5");
|
||||
const loadModels = vi.fn(async (providerType: LighthouseV2ProviderType) => {
|
||||
if (providerType === "openai-compatible") {
|
||||
return { error: "Connection failed", status: 400 };
|
||||
}
|
||||
|
||||
return { data: [openAIModel] };
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await loadLighthouseV2ConnectedModels(
|
||||
[
|
||||
configuration("openai", true),
|
||||
configuration("bedrock", false),
|
||||
configuration("openai-compatible", false),
|
||||
],
|
||||
loadModels,
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(loadModels).toHaveBeenCalledTimes(1);
|
||||
expect(loadModels).toHaveBeenCalledWith("openai");
|
||||
expect(result.modelsByProvider).toEqual({
|
||||
openai: [openAIModel],
|
||||
bedrock: [],
|
||||
"openai-compatible": [],
|
||||
});
|
||||
expect(result.failedModelProviders).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports model-loading failures only for connected configurations", async () => {
|
||||
// Given
|
||||
const loadModels = vi.fn(async (providerType: LighthouseV2ProviderType) => {
|
||||
if (providerType === "bedrock") {
|
||||
return { error: "Bedrock models unavailable", status: 503 };
|
||||
}
|
||||
|
||||
throw new Error(`Disconnected provider should not load: ${providerType}`);
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await loadLighthouseV2ConnectedModels(
|
||||
[
|
||||
configuration("openai", false),
|
||||
configuration("bedrock", true),
|
||||
configuration("openai-compatible", false),
|
||||
],
|
||||
loadModels,
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(loadModels).toHaveBeenCalledTimes(1);
|
||||
expect(loadModels).toHaveBeenCalledWith("bedrock");
|
||||
expect(result.modelsByProvider).toEqual({
|
||||
openai: [],
|
||||
bedrock: [],
|
||||
"openai-compatible": [],
|
||||
});
|
||||
expect(result.failedModelProviders).toEqual(["bedrock"]);
|
||||
});
|
||||
|
||||
it("dedupes provider types and treats null connection state as disconnected", async () => {
|
||||
// Given
|
||||
const openAIModel = model("gpt-5.5");
|
||||
const loadModels = vi.fn(async () => ({ data: [openAIModel] }));
|
||||
|
||||
// When
|
||||
const result = await loadLighthouseV2ConnectedModels(
|
||||
[
|
||||
configuration("openai", true),
|
||||
{ ...configuration("openai", true), id: "config-openai-2" },
|
||||
{ ...configuration("bedrock", false), connected: null },
|
||||
],
|
||||
loadModels,
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(loadModels).toHaveBeenCalledTimes(1);
|
||||
expect(loadModels).toHaveBeenCalledWith("openai");
|
||||
expect(result.modelsByProvider.openai).toEqual([openAIModel]);
|
||||
expect(result.modelsByProvider.bedrock).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function configuration(
|
||||
providerType: LighthouseV2ProviderType,
|
||||
connected: boolean,
|
||||
): LighthouseV2Configuration {
|
||||
return {
|
||||
id: `config-${providerType}`,
|
||||
providerType,
|
||||
baseUrl:
|
||||
providerType === "openai-compatible" ? "https://example.com" : null,
|
||||
defaultModel: null,
|
||||
businessContext: "Production account",
|
||||
connected,
|
||||
connectionLastCheckedAt: null,
|
||||
insertedAt: "2026-06-24T09:00:00Z",
|
||||
updatedAt: "2026-06-24T10:00:00Z",
|
||||
};
|
||||
}
|
||||
|
||||
function model(id: string): LighthouseV2SupportedModel {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
maxInputTokens: null,
|
||||
maxOutputTokens: null,
|
||||
supportsFunctionCalling: true,
|
||||
supportsVision: false,
|
||||
supportsReasoning: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
LIGHTHOUSE_V2_PROVIDER_TYPE,
|
||||
type LighthouseV2Configuration,
|
||||
type LighthouseV2ProviderType,
|
||||
type LighthouseV2SupportedModel,
|
||||
} from "@/app/(prowler)/lighthouse/_types";
|
||||
|
||||
interface LighthouseV2SupportedModelsSuccess {
|
||||
data: LighthouseV2SupportedModel[];
|
||||
}
|
||||
|
||||
interface LighthouseV2SupportedModelsFailure {
|
||||
error: string;
|
||||
errors?: unknown[];
|
||||
status?: number;
|
||||
}
|
||||
|
||||
type LighthouseV2SupportedModelsResult =
|
||||
| LighthouseV2SupportedModelsSuccess
|
||||
| LighthouseV2SupportedModelsFailure;
|
||||
|
||||
type LoadLighthouseV2SupportedModels = (
|
||||
providerType: LighthouseV2ProviderType,
|
||||
) => Promise<LighthouseV2SupportedModelsResult>;
|
||||
|
||||
interface LighthouseV2ConnectedModelsResult {
|
||||
modelsByProvider: Record<
|
||||
LighthouseV2ProviderType,
|
||||
LighthouseV2SupportedModel[]
|
||||
>;
|
||||
failedModelProviders: LighthouseV2ProviderType[];
|
||||
}
|
||||
|
||||
export function createEmptyLighthouseV2ModelsByProvider(): Record<
|
||||
LighthouseV2ProviderType,
|
||||
LighthouseV2SupportedModel[]
|
||||
> {
|
||||
return {
|
||||
[LIGHTHOUSE_V2_PROVIDER_TYPE.OPENAI]: [],
|
||||
[LIGHTHOUSE_V2_PROVIDER_TYPE.BEDROCK]: [],
|
||||
[LIGHTHOUSE_V2_PROVIDER_TYPE.OPENAI_COMPATIBLE]: [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadLighthouseV2ConnectedModels(
|
||||
configurations: LighthouseV2Configuration[],
|
||||
loadModels: LoadLighthouseV2SupportedModels,
|
||||
): Promise<LighthouseV2ConnectedModelsResult> {
|
||||
const modelsByProvider = createEmptyLighthouseV2ModelsByProvider();
|
||||
// Disconnected providers keep the [] pre-seed and never hit the models
|
||||
// endpoint, so they can't surface spurious load failures either.
|
||||
const connectedProviderTypes = Array.from(
|
||||
new Set(
|
||||
configurations
|
||||
.filter((configuration) => configuration.connected === true)
|
||||
.map((configuration) => configuration.providerType),
|
||||
),
|
||||
);
|
||||
|
||||
const modelsEntries = await Promise.all(
|
||||
connectedProviderTypes.map(async (providerType) => {
|
||||
const result = await loadModels(providerType);
|
||||
return [providerType, result] as const;
|
||||
}),
|
||||
);
|
||||
|
||||
const failedModelProviders: LighthouseV2ProviderType[] = [];
|
||||
|
||||
modelsEntries.forEach(([providerType, result]) => {
|
||||
if ("data" in result) {
|
||||
modelsByProvider[providerType] = result.data;
|
||||
return;
|
||||
}
|
||||
|
||||
failedModelProviders.push(providerType);
|
||||
});
|
||||
|
||||
return { modelsByProvider, failedModelProviders };
|
||||
}
|
||||
@@ -13,11 +13,8 @@ import {
|
||||
} from "@/app/(prowler)/lighthouse/_actions";
|
||||
import { LighthouseV2ChatPage } from "@/app/(prowler)/lighthouse/_components/chat";
|
||||
import { LighthouseV2NavigationModeSync } from "@/app/(prowler)/lighthouse/_components/navigation";
|
||||
import { loadLighthouseV2ConnectedModels } from "@/app/(prowler)/lighthouse/_lib/model-loading";
|
||||
import { buildLighthouseV2StreamUrl } from "@/app/(prowler)/lighthouse/_lib/stream-url";
|
||||
import type {
|
||||
LighthouseV2ProviderType,
|
||||
LighthouseV2SupportedModel,
|
||||
} from "@/app/(prowler)/lighthouse/_types";
|
||||
import { LighthouseIcon } from "@/components/icons/Icons";
|
||||
import { Chat } from "@/components/lighthouse-v1";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
@@ -53,25 +50,14 @@ export default async function AIChatbot({
|
||||
return redirect("/lighthouse/settings");
|
||||
}
|
||||
|
||||
const modelsEntries = await Promise.all(
|
||||
configurations.map(async (configuration) => {
|
||||
const result = await getLighthouseV2SupportedModels(
|
||||
configuration.providerType,
|
||||
);
|
||||
return [configuration.providerType, result] as const;
|
||||
}),
|
||||
);
|
||||
const modelsByProvider = Object.fromEntries(
|
||||
modelsEntries.map(([providerType, result]) => [
|
||||
providerType,
|
||||
"data" in result ? result.data : [],
|
||||
]),
|
||||
) as Record<LighthouseV2ProviderType, LighthouseV2SupportedModel[]>;
|
||||
// Surface (rather than silently swallow to []) providers whose models
|
||||
// failed to load, so an empty model list reads as a real backend failure.
|
||||
const failedModelProviders = modelsEntries
|
||||
.filter(([, result]) => !("data" in result))
|
||||
.map(([providerType]) => providerType);
|
||||
const { modelsByProvider, failedModelProviders } =
|
||||
await loadLighthouseV2ConnectedModels(
|
||||
configurations,
|
||||
getLighthouseV2SupportedModels,
|
||||
);
|
||||
// Surface (rather than silently swallow to []) connected providers whose
|
||||
// models failed to load, so their empty list reads as a real backend
|
||||
// failure. Disconnected providers are never fetched (see model-loading.ts).
|
||||
const modelsError =
|
||||
failedModelProviders.length > 0
|
||||
? `Could not load available models for: ${failedModelProviders.join(", ")}. Try again shortly.`
|
||||
|
||||
Reference in New Issue
Block a user