diff --git a/ui/litellm-dashboard/src/app/globals.css b/ui/litellm-dashboard/src/app/globals.css
index f389fa5df3d..87959ca0139 100644
--- a/ui/litellm-dashboard/src/app/globals.css
+++ b/ui/litellm-dashboard/src/app/globals.css
@@ -140,6 +140,7 @@
--sidebar-border: oklch(0.928 0.006 264.531);
--sidebar-ring: oklch(0.707 0.022 261.325);
--neutral-border: #dcddeb;
+ --logo-surface: oklch(1 0 0);
}
.dark {
@@ -227,6 +228,7 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
+ --color-logo-surface: var(--logo-surface);
}
@layer base {
diff --git a/ui/litellm-dashboard/src/components/molecules/logo/Logo.test.tsx b/ui/litellm-dashboard/src/components/molecules/logo/Logo.test.tsx
index 5e4da208f4e..62ce8e9ee12 100644
--- a/ui/litellm-dashboard/src/components/molecules/logo/Logo.test.tsx
+++ b/ui/litellm-dashboard/src/components/molecules/logo/Logo.test.tsx
@@ -52,6 +52,40 @@ describe("Logo", () => {
warnSpy.mockRestore();
});
+ it("leaves the caller's class list untouched for an asset that reads on dark", () => {
+ render();
+ expect(screen.getByRole("img", { name: "Slack logo" })).toHaveClass("w-5 h-5 shrink-0", { exact: true });
+ });
+
+ it("passes an untreated logo's classes through verbatim rather than normalizing them", () => {
+ render();
+ expect(screen.getByRole("img", { name: "Slack logo" })).toHaveClass("w-4 w-5 h-5", { exact: true });
+ });
+
+ it("forces a monochrome mark to white on dark without disturbing the caller's classes", () => {
+ render();
+ const img = screen.getByRole("img", { name: "GitHub logo" });
+ expect(img).toHaveClass("w-5", "h-5", "dark:[filter:brightness(0)_invert(1)]");
+ expect(img).not.toHaveClass("dark:bg-logo-surface");
+ });
+
+ it("plates a multicolor dark mark rather than inverting it", () => {
+ render();
+ const img = screen.getByRole("img", { name: "Fireworks logo" });
+ expect(img).toHaveClass("dark:bg-logo-surface", "dark:object-contain", "dark:p-0.5");
+ expect(img).not.toHaveClass("dark:[filter:brightness(0)_invert(1)]");
+ });
+
+ it("does not treat an external logo URL that collides with a bundled filename", () => {
+ render();
+ expect(screen.getByRole("img", { name: "Ext logo" })).toHaveClass("w-5 h-5", { exact: true });
+ });
+
+ it("applies the treatment to a provider logo resolved through the bundler", () => {
+ render();
+ expect(screen.getByRole("img", { name: "openrouter logo" })).toHaveClass("dark:[filter:brightness(0)_invert(1)]");
+ });
+
it("retries with a new src after a previous src errored", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { rerender } = render();
diff --git a/ui/litellm-dashboard/src/components/molecules/logo/Logo.tsx b/ui/litellm-dashboard/src/components/molecules/logo/Logo.tsx
index d388e3dc07d..66c10f1e222 100644
--- a/ui/litellm-dashboard/src/components/molecules/logo/Logo.tsx
+++ b/ui/litellm-dashboard/src/components/molecules/logo/Logo.tsx
@@ -1,12 +1,19 @@
import React, { useState } from "react";
import { getProviderLogoAndName } from "@/components/provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
+import { cn } from "@/lib/cva.config";
+import { logoTreatmentFor, type LogoTreatment } from "@/lib/logoTreatments";
type LogoProps = { className?: string } & (
| { provider: string; src?: never; label?: string }
| { provider?: never; src: string | null | undefined; label: string }
);
+const DARK_TREATMENT_CLASS: Readonly> = {
+ invert: "dark:[filter:brightness(0)_invert(1)]",
+ plate: "dark:bg-logo-surface dark:object-contain dark:p-0.5",
+};
+
export const Logo: React.FC = ({ provider, src, label, className = "w-4 h-4" }) => {
const [erroredSrc, setErroredSrc] = useState(null);
const resolvedSrc = provider !== undefined ? getProviderLogoAndName(provider).logo : resolveLogoSrc(src) ?? "";
@@ -20,11 +27,13 @@ export const Logo: React.FC = ({ provider, src, label, className = "w
);
}
+ const treatment = logoTreatmentFor(resolvedSrc);
+
return (
{
console.warn(`Logo failed to load: ${resolvedSrc}`);
setErroredSrc(resolvedSrc);
diff --git a/ui/litellm-dashboard/src/lib/logoTreatments.test.ts b/ui/litellm-dashboard/src/lib/logoTreatments.test.ts
new file mode 100644
index 00000000000..b0a2073be8d
--- /dev/null
+++ b/ui/litellm-dashboard/src/lib/logoTreatments.test.ts
@@ -0,0 +1,56 @@
+import { describe, expect, it } from "vitest";
+import { logoTreatmentFor } from "./logoTreatments";
+
+describe("logoTreatmentFor", () => {
+ it("marks a monochrome transparent mark for inversion", () => {
+ expect(logoTreatmentFor("/ui/assets/logos/github.svg")).toBe("invert");
+ });
+
+ it("marks a multicolor dark mark for a plate instead of inversion", () => {
+ expect(logoTreatmentFor("/ui/assets/logos/fireworks.svg")).toBe("plate");
+ });
+
+ it("plates a dark mark with a light knockout, which inversion would flatten away", () => {
+ expect(logoTreatmentFor("/ui/assets/logos/repelloai.png")).toBe("plate");
+ expect(logoTreatmentFor("/ui/assets/logos/aiml_api.svg")).toBe("plate");
+ });
+
+ it("leaves an asset that already reads on dark untreated", () => {
+ expect(logoTreatmentFor("/ui/assets/logos/slack.svg")).toBeUndefined();
+ });
+
+ it("resolves through a bundler fingerprint in the filename", () => {
+ expect(logoTreatmentFor("/litellm-asset-prefix/_next/static/media/openrouter.1xk7748-_jixf.svg")).toBe("invert");
+ });
+
+ it("resolves a bundled asset served under a proxy root path", () => {
+ expect(logoTreatmentFor("/litellm/ui/assets/logos/notion.svg")).toBe("invert");
+ });
+
+ it("ignores a query string and fragment on the asset URL", () => {
+ expect(logoTreatmentFor("/ui/assets/logos/vercel.svg?v=2#icon")).toBe("invert");
+ });
+
+ it("does not treat an external URL whose filename collides with a bundled asset", () => {
+ expect(logoTreatmentFor("https://cdn.example.com/github.svg")).toBeUndefined();
+ });
+
+ it("does not treat a non-logo path whose filename collides with a bundled asset", () => {
+ expect(logoTreatmentFor("/uploads/user/github.svg")).toBeUndefined();
+ });
+
+ it("leaves an opaque dark box untreated, since a plate behind it cannot show through", () => {
+ expect(logoTreatmentFor("/ui/assets/logos/lakeraai.jpeg")).toBeUndefined();
+ });
+
+ it("returns undefined for empty and nullish input", () => {
+ expect(logoTreatmentFor(null)).toBeUndefined();
+ expect(logoTreatmentFor(undefined)).toBeUndefined();
+ expect(logoTreatmentFor("")).toBeUndefined();
+ });
+
+ it("distinguishes assets that share a stem but differ by extension", () => {
+ expect(logoTreatmentFor("/ui/assets/logos/runway.png")).toBe("invert");
+ expect(logoTreatmentFor("/ui/assets/logos/runway.svg")).toBeUndefined();
+ });
+});
diff --git a/ui/litellm-dashboard/src/lib/logoTreatments.ts b/ui/litellm-dashboard/src/lib/logoTreatments.ts
new file mode 100644
index 00000000000..f8378800046
--- /dev/null
+++ b/ui/litellm-dashboard/src/lib/logoTreatments.ts
@@ -0,0 +1,56 @@
+export type LogoTreatment = "invert" | "plate";
+
+const BUNDLED_LOGO_PATH = /(?:\/assets\/logos\/|\/_next\/static\/media\/)/;
+
+const TREATMENT_BY_ASSET: Readonly> = {
+ "baseten.svg": "invert",
+ "cursor.svg": "invert",
+ "enkrypt_ai.avif": "invert",
+ "friendli.svg": "invert",
+ "github.svg": "invert",
+ "github_copilot.svg": "invert",
+ "lago.svg": "invert",
+ "lambda.svg": "invert",
+ "langflow.svg": "invert",
+ "lmstudio.svg": "invert",
+ "moonshot.svg": "invert",
+ "nebius.svg": "invert",
+ "notion.svg": "invert",
+ "ollama.svg": "invert",
+ "openrouter.svg": "invert",
+ "promptguard.svg": "invert",
+ "recraft.svg": "invert",
+ "replicate.svg": "invert",
+ "runway.png": "invert",
+ "scx_ai.svg": "invert",
+ "secret_detect.png": "invert",
+ "topaz.svg": "invert",
+ "v0.svg": "invert",
+ "vercel.svg": "invert",
+ "watsonx.svg": "invert",
+ "aiml_api.svg": "plate",
+ "akto.svg": "plate",
+ "aws.svg": "plate",
+ "deepkeep.svg": "plate",
+ "fireworks.svg": "plate",
+ "llm_guard.png": "plate",
+ "pangea.png": "plate",
+ "repelloai.png": "plate",
+ "sambanova.svg": "plate",
+ "sentry.svg": "plate",
+ "valkey.svg": "plate",
+};
+
+const basenameOf = (src: string): string | undefined => src.split(/[?#]/)[0].split("/").pop() || undefined;
+
+const withoutBundlerHash = (basename: string): string | undefined => {
+ const parts = basename.split(".");
+ return parts.length < 2 ? undefined : `${parts[0]}.${parts[parts.length - 1]}`;
+};
+
+export const logoTreatmentFor = (src: string | null | undefined): LogoTreatment | undefined => {
+ if (!src || !BUNDLED_LOGO_PATH.test(src)) return undefined;
+ const basename = basenameOf(src);
+ const key = basename === undefined ? undefined : withoutBundlerHash(basename);
+ return key === undefined ? undefined : TREATMENT_BY_ASSET[key];
+};