fix(ui): harden provider logo map typing and bundled asset guard (#34163)

Follow-up to the static logo import PR. Types providerLogoMap as
Partial<Record<Providers, string>> so raw string keys and lookups are
compile errors, tightens the resolveLogoSrc passthrough from /_next/ to
/_next/static/ so lookalike backend paths still get root-prefixed, adds
an enum coverage test that locks the exact set of logoless providers,
and makes Logo props a discriminated union so provider and src modes
cannot be mixed and src mode requires a label.
This commit is contained in:
ryan-crabbe-berri 2026-07-21 16:11:38 -07:00 • committed by GitHub
parent 42f269ccf2
commit 02746eb122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 52 additions and 24 deletions

View file

@ -20,8 +20,8 @@ describe("Logo", () => {
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
it("renders a dash avatar when neither provider nor src is given", () => {
render(<Logo />);
it("renders a dash avatar when src is empty and the label has no characters", () => {
render(<Logo src={null} label="" />);
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});

View file

@ -2,12 +2,10 @@ import React, { useState } from "react";
import { getProviderLogoAndName } from "@/components/provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
interface LogoProps {
provider?: string;
src?: string | null;
label?: string;
className?: string;
}
type LogoProps = { className?: string } & (
| { provider: string; src?: never; label?: string }
| { provider?: never; src: string | null | undefined; label: string }
);
export const Logo: React.FC<LogoProps> = ({ provider, src, label, className = "w-4 h-4" }) => {
const [erroredSrc, setErroredSrc] = useState<string | null>(null);

View file

@ -114,13 +114,35 @@ describe("provider_info_helpers", () => {
});
describe("provider logo bundled assets", () => {
it("should expose every provider logo as a truthy bundled URL, never a raw /ui/assets path", () => {
const logos = Object.values(providerLogoMap);
expect(logos.length).toBeGreaterThan(0);
logos.forEach((logo) => {
expect(typeof logo).toBe("string");
expect(logo.length).toBeGreaterThan(0);
expect(logo.startsWith("/ui/assets/")).toBe(false);
it("should map every provider to a bundled logo except the known logoless set, never a raw /ui/assets path", () => {
const knownLogolessProviders = [
Providers.AUTO_ROUTER,
Providers.BYTEZ,
Providers.CLARIFAI,
Providers.COMPACTIFAI,
Providers.DATAROBOT,
Providers.DOCKER_MODEL_RUNNER,
Providers.DOTPROMPT,
Providers.EMPOWER,
Providers.GALADRIEL,
Providers.GradientAI,
Providers.HEROKU,
Providers.LEMONADE,
Providers.LLAMAFILE,
Providers.MARITALK,
Providers.NLP_CLOUD,
Providers.NSCALE,
Providers.OVHCLOUD,
Providers.PETALS,
Providers.PG_VECTOR,
Providers.PREDIBASE,
Providers.WANDB,
Providers.ZAI,
];
const logolessProviders = Object.values(Providers).filter((provider) => !providerLogoMap[provider]);
expect([...logolessProviders].sort()).toEqual([...knownLogolessProviders].sort());
Object.values(providerLogoMap).forEach((logo) => {
expect(logo?.startsWith("/ui/assets/")).toBe(false);
});
});

View file

@ -283,7 +283,7 @@ export const provider_map: Record<string, string> = {
const standaloneSubproviderSlugs = new Set<string>(["bedrock_mantle"]);
export const providerLogoMap: Record<string, string> = {
export const providerLogoMap: Partial<Record<Providers, string>> = {
[Providers.A2A_Agent]: a2aAgentLogo.src,
[Providers.AI21]: ai21Logo.src,
[Providers.AI21_CHAT]: ai21Logo.src,
@ -395,7 +395,7 @@ export const getProviderLogoAndName = (providerValue: string): { logo: string; d
// Get the display name from Providers enum and logo from map
const displayName = Providers[enumKey as keyof typeof Providers];
const logo = resolveLogoSrc(providerLogoMap[displayName as keyof typeof providerLogoMap]) ?? "";
const logo = resolveLogoSrc(providerLogoMap[displayName]) ?? "";
return { logo, displayName };
};

View file

@ -26,12 +26,12 @@ export const vectorStoreProviderMap: Record<string, string> = {
};
export const vectorStoreProviderLogoMap: Record<string, string> = {
[VectorStoreProviders.Bedrock]: providerLogoMap[Providers.Bedrock],
[VectorStoreProviders.Bedrock]: providerLogoMap[Providers.Bedrock] ?? "",
[VectorStoreProviders.PgVector]: postgresqlLogo.src,
[VectorStoreProviders.VertexRagEngine]: providerLogoMap[Providers.Vertex_AI],
[VectorStoreProviders.VertexAiSearch]: providerLogoMap[Providers.Vertex_AI],
[VectorStoreProviders.OpenAI]: providerLogoMap[Providers.OpenAI],
[VectorStoreProviders.Azure]: providerLogoMap[Providers.Azure],
[VectorStoreProviders.VertexRagEngine]: providerLogoMap[Providers.Vertex_AI] ?? "",
[VectorStoreProviders.VertexAiSearch]: providerLogoMap[Providers.Vertex_AI] ?? "",
[VectorStoreProviders.OpenAI]: providerLogoMap[Providers.OpenAI] ?? "",
[VectorStoreProviders.Azure]: providerLogoMap[Providers.Azure] ?? "",
[VectorStoreProviders.Milvus]: milvusLogo.src,
[VectorStoreProviders.S3Vectors]: s3VectorLogo.src,
};

View file

@ -48,7 +48,7 @@ describe("resolveLogoSrc", () => {
expect(resolveLogoSrc("//cdn.example.com/x.svg")).toBe("//cdn.example.com/x.svg");
});
it("passes bundled /_next/ asset URLs through untouched even under a sub-path mount", async () => {
it("passes bundled /_next/static/ asset URLs through untouched even under a sub-path mount", async () => {
const { resolveLogoSrc } = await importWithRoot("/litellm");
expect(resolveLogoSrc("/_next/static/media/openai_small.abc123.svg")).toBe(
"/_next/static/media/openai_small.abc123.svg",
@ -56,6 +56,14 @@ describe("resolveLogoSrc", () => {
expect(resolveLogoSrc("/litellm-asset-prefix/_next/static/media/openai_small.abc123.svg")).toBe(
"/litellm-asset-prefix/_next/static/media/openai_small.abc123.svg",
);
expect(resolveLogoSrc("/litellm/_next/static/media/openai_small.abc123.svg")).toBe(
"/litellm/_next/static/media/openai_small.abc123.svg",
);
});
it("still prefixes a backend path that merely contains a /_next/ lookalike segment", async () => {
const { resolveLogoSrc } = await importWithRoot("/litellm");
expect(resolveLogoSrc("/ui/assets/logos/_next/logo.svg")).toBe("/litellm/ui/assets/logos/_next/logo.svg");
});
it("roots a local asset path using the live server root path", async () => {

View file

@ -23,7 +23,7 @@ export const withServerRoot = (path: string, root: string): string => {
export const resolveLogoSrc = (value: string | null | undefined, root: string = serverRootPath): string | undefined => {
if (!value) return undefined;
if (EXTERNAL_SRC.test(value)) return value;
if (value.includes("/_next/")) return value;
if (value.includes("/_next/static/")) return value;
const prefix = normalizeRootPath(root);
if (prefix && (value === prefix || value.startsWith(`${prefix}/`))) return value;
return withServerRoot(value, root);