feat(ui): serve a dark-mode variant of the LiteLLM logo (#37656)

The bundled logo is a JPEG, so it carries no alpha and its white
background renders as a bright slab against a dark sidebar. Making it
transparent alone would not be enough either: the wordmark is near-black
and would disappear on dark.

Adds logo_dark.png, derived from the light logo. The sky-blue disc and
train are kept as they are behind a circular alpha mask, and the
wordmark's antialiasing is un-flattened from white into straight alpha
and repainted in the dark theme's own foreground colour. Both files are
1000x257, so swapping between them cannot shift the sidebar header.

/get_image gains a theme query param. The default response is byte for
byte what it was, and a logo configured through UI_LOGO_PATH is served
unchanged in both themes, since custom logos have no dark variant yet.
This commit is contained in:
yuneng-jiang 2026-08-20 11:26:10 -07:00 committed by GitHub
parent 282bcdadcc
commit edbb3429a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 89 additions and 10 deletions

BIN
litellm/proxy/logo_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -15256,12 +15256,17 @@ def get_logo_url():
@app.get("/get_image", include_in_schema=False)
async def get_image():
async def get_image(theme: Literal["light", "dark"] | None = None):
"""Get logo to show on admin UI"""
# get current_dir
current_dir: Final = os.path.dirname(os.path.abspath(__file__))
default_site_logo: Final = os.path.join(current_dir, "logo.jpg")
bundled_light_logo: Final = os.path.join(current_dir, "logo.jpg")
bundled_dark_logo: Final = os.path.join(current_dir, "logo_dark.png")
default_site_logo: Final = (
bundled_dark_logo if theme == "dark" and os.path.isfile(bundled_dark_logo) else bundled_light_logo
)
default_logo_filename: Final = os.path.basename(default_site_logo)
is_non_root: Final = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
@ -15284,7 +15289,7 @@ async def get_image():
assets_dir = current_dir
# Determine default logo path
default_logo = os.path.join(assets_dir, "logo.jpg") if assets_dir != current_dir else default_site_logo
default_logo = os.path.join(assets_dir, default_logo_filename) if assets_dir != current_dir else default_site_logo
if assets_dir != current_dir and not os.path.exists(default_logo):
default_logo = default_site_logo
@ -15316,7 +15321,7 @@ async def get_image():
if safe_logo is not None:
safe_logo_path, media_type = safe_logo
return FileResponse(safe_logo_path, media_type=media_type)
return FileResponse(default_site_logo, media_type="image/jpeg")
return FileResponse(bundled_light_logo, media_type="image/jpeg")
@app.get("/get_favicon", include_in_schema=False)

View file

@ -187,6 +187,54 @@ def test_get_image_returns_default_logo(client, monkeypatch):
assert shape == {"status": 200, "media_type_image": True, "has_body": True}
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
PNG_IHDR_COLOUR_TYPE_OFFSET = 25
PNG_COLOUR_TYPE_RGBA = 6
def test_get_image_dark_theme_returns_logo_with_an_alpha_channel(client, monkeypatch):
"""?theme=dark serves the dark logo. It must be an RGBA PNG: the light logo is a
JPEG whose baked-in white background renders as a white slab on a dark sidebar."""
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
response = client.get("/get_image", params={"theme": "dark"})
body = response.content
shape = {
"status": response.status_code,
"media_type": response.headers.get("content-type", "").split(";")[0],
"is_png": body[:8] == PNG_SIGNATURE,
"colour_type": body[PNG_IHDR_COLOUR_TYPE_OFFSET],
}
assert shape == {
"status": 200,
"media_type": "image/png",
"is_png": True,
"colour_type": PNG_COLOUR_TYPE_RGBA,
}
def test_get_image_without_theme_still_serves_the_light_jpeg(client, monkeypatch):
"""The default response is unchanged, so light mode keeps the existing logo."""
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
response = client.get("/get_image")
shape = {
"status": response.status_code,
"media_type": response.headers.get("content-type", "").split(";")[0],
"is_jpeg": response.content[:3] == b"\xff\xd8\xff",
}
assert shape == {"status": 200, "media_type": "image/jpeg", "is_jpeg": True}
def test_get_image_dark_theme_keeps_serving_a_custom_ui_logo(client, monkeypatch, tmp_path):
"""A custom UI_LOGO_PATH has no dark variant yet, so dark mode must fall back to the
admin's own logo rather than replacing it with LiteLLM's."""
custom_logo = tmp_path / "custom.png"
custom_logo.write_bytes(PNG_SIGNATURE + b"custom-logo-marker")
monkeypatch.setenv("UI_LOGO_PATH", str(custom_logo))
response = client.get("/get_image", params={"theme": "dark"})
shape = {"status": response.status_code, "body": response.content}
assert shape == {"status": 200, "body": PNG_SIGNATURE + b"custom-logo-marker"}
def test_get_image_redirects_remote_url(client, monkeypatch):
"""Remote logo URLs are served via redirect — the proxy never fetches them server-side."""
monkeypatch.setenv("UI_LOGO_PATH", "https://example.invalid/logo.png")

View file

@ -105,6 +105,21 @@ describe("Sidebar (leftnav)", () => {
expect(screen.getByRole("link", { name: /litellm home/i })).toHaveAttribute("href", "/ui");
});
it("pairs the logo with a dark-mode variant that swaps on the dark class", () => {
renderWithProviders(<Sidebar {...defaultProps} />);
const [light, dark] = Array.from(screen.getByRole("link", { name: /litellm home/i }).querySelectorAll("img"));
const classesOf = (el: Element) => new Set(el.className.split(/\s+/));
const lightSrc = light.getAttribute("src") ?? "";
expect(light).toHaveAttribute("src", expect.stringMatching(/\/get_image$/));
expect(dark).toHaveAttribute("src", `${lightSrc}?theme=dark`);
expect(classesOf(light).has("dark:hidden")).toBe(true);
expect(classesOf(light).has("hidden")).toBe(false);
expect(classesOf(dark).has("hidden")).toBe(true);
expect(classesOf(dark).has("dark:block")).toBe(true);
});
it("renders all top-level (non-nested) tabs for admin", () => {
renderWithProviders(<Sidebar {...defaultProps} />);

View file

@ -81,6 +81,8 @@ import { MIGRATED_PAGES, migratedHref, legacyPageHref } from "@/utils/migratedPa
const ICON = { strokeWidth: 1.75 } as const;
const LOGO_CLASS_NAME = "h-7 w-auto max-w-[150px] object-contain group-data-[collapsed=true]/sidebar:w-7";
interface SidebarProps {
setPage: (page: string) => void;
defaultSelectedKey: string;
@ -603,6 +605,7 @@ const Sidebar_: React.FC<SidebarProps> = ({
};
const logoSrc = logoUrl || `${baseUrl}/get_image`;
const darkLogoSrc = logoUrl || `${baseUrl}/get_image?theme=dark`;
return (
<Sidebar collapsed={collapsed}>
@ -610,11 +613,8 @@ const Sidebar_: React.FC<SidebarProps> = ({
<div className="flex items-center justify-between gap-2 group-data-[collapsed=true]/sidebar:flex-col">
<div className="flex min-w-0 items-center gap-2">
<Link href={migratedHref("")} className="flex min-w-0 items-center" aria-label="LiteLLM home">
<img
src={logoSrc}
alt="LiteLLM"
className="h-7 w-auto max-w-[150px] object-contain group-data-[collapsed=true]/sidebar:w-7"
/>
<img src={logoSrc} alt="LiteLLM" className={cn(LOGO_CLASS_NAME, "dark:hidden")} />
<img src={darkLogoSrc} alt="" aria-hidden className={cn(LOGO_CLASS_NAME, "hidden dark:block")} />
</Link>
{version && (
<Badge

View file

@ -43633,7 +43633,9 @@ export interface operations {
};
get_image_get_image_get: {
parameters: {
query?: never;
query?: {
theme?: ("light" | "dark") | null;
};
header?: never;
path?: never;
cookie?: never;
@ -43649,6 +43651,15 @@ export interface operations {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_logo_url_get_logo_url_get: {