mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
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:
parent
42f269ccf2
commit
02746eb122
7 changed files with 52 additions and 24 deletions
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue