feat(ui): adopt the new LiteLLM logo and monogram (#43913)

* feat(ui): adopt the new LiteLLM logo and monogram

Swap the bundled admin UI logos for the new brand assets: the primary
logo in blue for light mode and white for dark mode, and the monogram for
the collapsed sidebar, favicons, and the built-in guardrail cards.

/get_image gains a variant=monogram query parameter so the collapsed
sidebar can request the monogram while admin-configured UI_LOGO_PATH /
UI_LOGO_PATH_DARK logos still take precedence. The bundled light logo
moves from JPEG to a transparent PNG.

* test(ui): query collapsed sidebar logos by role to stay within the lint budget

* fix: point remaining logo consumers at the new bundled assets

The Rust gateway UI served /get_image from the removed litellm_logo.jpg,
the non-root get_image tests pinned logo.jpg, and a cookbook script read
litellm/proxy/logo.jpg. Point them at the monogram and logo.png.

* fix(mcp): serve the BYOK OAuth page logo from /get_image

The page pointed at /ui/assets/logos/litellm_logo.jpg, which the rebrand
removes from the dashboard sources, so the next UI build would drop it.
/get_image?variant=monogram is always served by the proxy and follows any
admin-configured logo.

* fix(ui): invert the LiteLLM monogram on dark guardrail cards

The blue monogram has a transparent train cut-out, so on a dark card it
read as a muddy blue block. Inverting it yields the brand's white mark,
which the logo guidelines prescribe for dark backgrounds.

* fix(gateway-ui): serve theme and variant aware logos from the dashboard export

The Rust gateway served one monogram for every /get_image request, and the
committed export lacked it, so /get_image returned 404 until the next UI
release build. Pick the full or monogram logo in light or dark from the
query, ship those assets in the dashboard's public dir and the committed
export, and drop two comments that restated asserted paths.
This commit is contained in:
yuneng-jiang 2026-09-30 14:51:06 -07:00 • committed by GitHub
parent cbe69723b1
commit a6f6c64b6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 273 additions and 40 deletions

View file

@ -12,7 +12,7 @@ def encode_image(image_path):
# Path to your image
image_path = "litellm/proxy/logo.jpg"
image_path = "litellm/proxy/logo.png"
# Getting the Base64 string
base64_image = encode_image(image_path)
@ -27,7 +27,7 @@ response = client.responses.create(
{"type": "input_text", "text": "what color is the image"},
{
"type": "input_image",
"image_url": f"data:image/jpeg;base64,{base64_image}",
"image_url": f"data:image/png;base64,{base64_image}",
},
],
}

View file

@ -103,7 +103,7 @@ ENV LITELLM_NON_ROOT=true
RUN mkdir -p /var/lib/litellm/ui /var/lib/litellm/assets && \
cp -r /app/litellm/proxy/_experimental/out/. /var/lib/litellm/ui/ && \
cp /app/litellm/proxy/logo.jpg /var/lib/litellm/assets/logo.jpg && \
cp /app/litellm/proxy/logo.png /var/lib/litellm/assets/logo.png && \
touch /var/lib/litellm/ui/.litellm_ui_ready
RUN --mount=type=cache,target=/app/.cache/uv,id=litellm-uv-cache \

View file

@ -6,7 +6,7 @@ license.workspace = true
repository.workspace = true
[dependencies]
axum = { workspace = true, features = ["json", "original-uri"] }
axum = { workspace = true, features = ["json", "original-uri", "query"] }
axum-login.workspace = true
base64.workspace = true
governor = { version = "0.10.4", default-features = false, features = ["std"] }
@ -16,6 +16,7 @@ rand.workspace = true
serde.workspace = true
thiserror.workspace = true
time.workspace = true
tower = { version = "0.5", features = ["util"] }
tower-cookies = "0.11.0"
tower-http = { version = "0.6.11", features = ["fs", "set-header"] }
tower-sessions.workspace = true

View file

@ -1,7 +1,12 @@
use std::path::Path;
use axum::{Router, routing::get};
use serde::Serialize;
use axum::{
Router,
extract::{Query, Request},
routing::get,
};
use serde::{Deserialize, Serialize};
use tower::ServiceExt;
use tower_http::services::{ServeDir, ServeFile};
#[derive(Serialize)]
@ -9,9 +14,39 @@ struct Logo {
logo_url: &'static str,
}
#[derive(Clone, Copy, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Theme {
Light,
Dark,
}
#[derive(Clone, Copy, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Variant {
Full,
Monogram,
}
#[derive(Deserialize)]
struct LogoQuery {
theme: Option<Theme>,
variant: Option<Variant>,
}
fn logo_file(query: &LogoQuery) -> &'static str {
match (query.variant, query.theme) {
(Some(Variant::Monogram), Some(Theme::Dark)) => "assets/logos/litellm_monogram_dark.svg",
(Some(Variant::Monogram), _) => "assets/logos/litellm_monogram.svg",
(_, Some(Theme::Dark)) => "assets/logos/litellm_logo_dark.png",
_ => "assets/logos/litellm_logo.png",
}
}
pub fn dashboard_assets(directory: impl AsRef<Path>) -> Router {
let directory = directory.as_ref();
let assets = ServeDir::new(directory.join("_next")).append_index_html_on_directories(false);
let logos = directory.to_path_buf();
crate::static_assets(directory)
.route(
@ -22,9 +57,11 @@ pub fn dashboard_assets(directory: impl AsRef<Path>) -> Router {
})
}),
)
.route_service(
.route(
"/get_image",
ServeFile::new(directory.join("assets/logos/litellm_logo.jpg")),
get(move |Query(query): Query<LogoQuery>, request: Request| {
ServeFile::new(logos.join(logo_file(&query))).oneshot(request)
}),
)
.route_service(
"/get_favicon",

View file

@ -40,7 +40,14 @@ fn dashboard(directory: TempDir) -> App {
let export = directory.path().join("public");
std::fs::create_dir_all(export.join("_next/static")).unwrap();
std::fs::create_dir_all(export.join("assets/logos")).unwrap();
std::fs::write(export.join("assets/logos/litellm_logo.jpg"), "logo bytes").unwrap();
for (file, bytes) in [
("litellm_logo.png", "logo bytes"),
("litellm_logo_dark.png", "dark logo bytes"),
("litellm_monogram.svg", "monogram bytes"),
("litellm_monogram_dark.svg", "dark monogram bytes"),
] {
std::fs::write(export.join("assets/logos").join(file), bytes).unwrap();
}
std::fs::write(export.join("favicon.ico"), "icon bytes").unwrap();
std::fs::write(export.join("_next/static/app.js"), "window.app = true;").unwrap();
App {
@ -130,7 +137,15 @@ async fn missing_paths_never_fall_back_to_dashboard(app: App, #[case] path: &str
)]
#[case::root_assets("/_next/static/app.js", "window.app = true;", "text/javascript")]
#[case::nested_assets("/ui/_next/static/app.js", "window.app = true;", "text/javascript")]
#[case::logo("/get_image", "logo bytes", "image/jpeg")]
#[case::logo("/get_image", "logo bytes", "image/png")]
#[case::logo_light("/get_image?theme=light", "logo bytes", "image/png")]
#[case::logo_dark("/get_image?theme=dark", "dark logo bytes", "image/png")]
#[case::monogram("/get_image?variant=monogram", "monogram bytes", "image/svg+xml")]
#[case::monogram_dark(
"/get_image?theme=dark&variant=monogram",
"dark monogram bytes",
"image/svg+xml"
)]
#[case::favicon("/get_favicon", "icon bytes", "image/x-icon")]
#[tokio::test]
async fn dashboard_adapter_preserves_existing_urls(
@ -184,3 +199,19 @@ async fn logo_discovery_points_to_served_image(dashboard: App) {
"logo bytes"
);
}
#[rstest]
#[case::logo("/get_image")]
#[case::logo_dark("/get_image?theme=dark")]
#[case::monogram("/get_image?variant=monogram")]
#[case::monogram_dark("/get_image?theme=dark&variant=monogram")]
#[tokio::test]
async fn committed_dashboard_export_serves_every_logo(#[case] path: &str) {
let export = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../../litellm/proxy/_experimental/out");
let response = litellm_gateway_ui::dashboard_assets(export)
.oneshot(Request::get(path).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}

View file

@ -506,7 +506,7 @@ def _build_authorize_html(
<button class="close-btn" type="button" onclick="doCancel()" title="Close">&times;</button>
<div class="logos">
<img src="/ui/assets/logos/litellm_logo.jpg" class="logo-img" alt="LiteLLM">
<img src="/get_image?variant=monogram" class="logo-img" alt="LiteLLM">
<span class="logo-arrow">&#8594;</span>
<div class="logo logo-s">{server_initial}</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 372.14 371.53">
<defs>
<style>
.cls-1 {
fill: #0117be;
}
</style>
</defs>
<path class="cls-1" d="M296.11,254.16c7.79-1.07,7.43-12.43.39-13.28-6.58-.79-14.5.86-21.17-.23l-50.86.37c-7.25,2.25-6.45,12.27,1.1,13.28l70.54-.14Z"/>
<path class="cls-1" d="M309.42,196.83c-35.23-15.22-72.84-26.08-109.96-35.87-33.05-8.71-69.92-16.92-103.8-21.32-31.89-4.14-63.43-3.41-95.51-3.23v6.54c36.23-.59,71.45.42,107.34,5.91,48.38,7.41,99.06,19.56,145.78,33.98,27.15,8.38,53.59,18.79,79.2,31.01l.1,67.98c12.46-2,31.2-8.81,39.28-20.3l-.04-24.63c-10.86-17.17-45.21-32.66-62.4-40.09Z"/>
<path class="cls-1" d="M14.79,115.72c-.07.12-.14.23-.21.35,23.36.36,46.79,1.59,69.97,4.75,36.98,5.04,75.62,13.61,111.4,24.06,17.99,5.25,35.13,13.22,54.25,12.22,4.13-.22,8.25-1.11,12.36-1.52l-.56-.73c-22.47-16.17-52.91-34.43-91.33-48.5C101.55,81.06,39.38,81.29.08,85.13l.05,31.74c3.47-.28,6.94-.56,10.39-.99,1.4-.02,2.84-.06,4.28-.16Z"/>
<path class="cls-1" d="M333.75-.37L37.67.08C16.84.11-.03,17.02,0,37.85l.06,39.13c27.48-4.04,66.25-6.26,112.14,2.78,1.52.3,6.38,1.29,12.73,2.77,54.99,12.83,104.51,41.22,151.11,71.93,22.76,14.99,46.61,30.85,68.19,47.42,8.92,6.85,19.82,15.04,27.57,24.61l-.29-189.2c-.03-20.83-16.94-37.69-37.77-37.66Z"/>
<path class="cls-1" d="M92.09,245.59h6.39s0,.03,0,.03l57.07-.09-48.14-66.6c-.53-.87-3.85-2.78-4.61-2.78h-4.75s0,.18,0,.18h-6.06s0-.09,0-.09l-91.76.14.11,69.62,91.76-.14v-.28Z"/>
<path class="cls-1" d="M.4,298.26l.05,35.68c.03,20.83,16.94,37.69,37.77,37.66l296.08-.45c20.83-.03,37.69-16.94,37.66-37.77l-.09-59.31c-17.35,19.08-52.22,22.16-75.65,23.75l-295.82.45Z"/>
<path class="cls-1" d="M159.06,253.3c-20.19.1-40.38.04-60.58.04v.26s-42.72.07-42.72.07c-.83.12-1.69.18-2.17,0l-53.25.08v9.76s166.32-.25,166.32-.25c-1.47-1.7-5.76-9.61-7.6-9.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 372.14 371.53">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<path class="cls-1" d="M296.11,254.16c7.79-1.07,7.43-12.43.39-13.28-6.58-.79-14.5.86-21.17-.23l-50.86.37c-7.25,2.25-6.45,12.27,1.1,13.28l70.54-.14Z"/>
<path class="cls-1" d="M309.42,196.83c-35.23-15.22-72.84-26.08-109.96-35.87-33.05-8.71-69.92-16.92-103.8-21.32-31.89-4.14-63.43-3.41-95.51-3.23v6.54c36.23-.59,71.45.42,107.34,5.91,48.38,7.41,99.06,19.56,145.78,33.98,27.15,8.38,53.59,18.79,79.2,31.01l.1,67.98c12.46-2,31.2-8.81,39.28-20.3l-.04-24.63c-10.86-17.17-45.21-32.66-62.4-40.09Z"/>
<path class="cls-1" d="M14.79,115.72c-.07.12-.14.23-.21.35,23.36.36,46.79,1.59,69.97,4.75,36.98,5.04,75.62,13.61,111.4,24.06,17.99,5.25,35.13,13.22,54.25,12.22,4.13-.22,8.25-1.11,12.36-1.52l-.56-.73c-22.47-16.17-52.91-34.43-91.33-48.5C101.55,81.06,39.38,81.29.08,85.13l.05,31.74c3.47-.28,6.94-.56,10.39-.99,1.4-.02,2.84-.06,4.28-.16Z"/>
<path class="cls-1" d="M333.75-.37L37.67.08C16.84.11-.03,17.02,0,37.85l.06,39.13c27.48-4.04,66.25-6.26,112.14,2.78,1.52.3,6.38,1.29,12.73,2.77,54.99,12.83,104.51,41.22,151.11,71.93,22.76,14.99,46.61,30.85,68.19,47.42,8.92,6.85,19.82,15.04,27.57,24.61l-.29-189.2c-.03-20.83-16.94-37.69-37.77-37.66Z"/>
<path class="cls-1" d="M92.09,245.59h6.39s0,.03,0,.03l57.07-.09-48.14-66.6c-.53-.87-3.85-2.78-4.61-2.78h-4.75s0,.18,0,.18h-6.06s0-.09,0-.09l-91.76.14.11,69.62,91.76-.14v-.28Z"/>
<path class="cls-1" d="M.4,298.26l.05,35.68c.03,20.83,16.94,37.69,37.77,37.66l296.08-.45c20.83-.03,37.69-16.94,37.66-37.77l-.09-59.31c-17.35,19.08-52.22,22.16-75.65,23.75l-295.82.45Z"/>
<path class="cls-1" d="M159.06,253.3c-20.19.1-40.38.04-60.58.04v.26s-42.72.07-42.72.07c-.83.12-1.69.18-2.17,0l-53.25.08v9.76s166.32-.25,166.32-.25c-1.47-1.7-5.76-9.61-7.6-9.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

BIN
litellm/proxy/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -17540,13 +17540,17 @@ def _serve_custom_ui_logo(candidate: str) -> Response | None:
@app.get("/get_image", include_in_schema=False)
async def get_image(theme: Literal["light", "dark"] | None = None):
async def get_image(
theme: Literal["light", "dark"] | None = None,
variant: Literal["full", "monogram"] = "full",
):
"""Get logo to show on admin UI"""
# get current_dir
current_dir: Final = os.path.dirname(os.path.abspath(__file__))
bundled_light_logo: Final = os.path.join(current_dir, "logo.jpg")
bundled_dark_logo: Final = os.path.join(current_dir, "logo_dark.png")
bundled_logo_stem: Final = "logo_monogram" if variant == "monogram" else "logo"
bundled_light_logo: Final = os.path.join(current_dir, f"{bundled_logo_stem}.png")
bundled_dark_logo: Final = os.path.join(current_dir, f"{bundled_logo_stem}_dark.png")
default_site_logo: Final = (
bundled_dark_logo if theme == "dark" and os.path.isfile(bundled_dark_logo) else bundled_light_logo
)
@ -17607,7 +17611,7 @@ async def get_image(theme: Literal["light", "dark"] | None = None):
if safe_logo is not None:
safe_logo_path, media_type = safe_logo
return FileResponse(safe_logo_path, media_type=media_type)
return FileResponse(bundled_light_logo, media_type="image/jpeg")
return FileResponse(bundled_light_logo, media_type="image/png")
@app.get("/get_favicon", include_in_schema=False)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View file

@ -13,6 +13,7 @@ Covers:
import base64
import hashlib
import json
import re
import time
import uuid
from typing import Any, Optional
@ -218,6 +219,29 @@ def test_authorize_get_returns_html(client):
assert "abc123" in resp.text
def test_authorize_page_logo_is_served_by_the_proxy(client):
from litellm.proxy.proxy_server import app
page = client.get(
"/v1/mcp/oauth/authorize",
params={
"client_id": "test-client",
"redirect_uri": "http://127.0.0.1:3000/callback",
"response_type": "code",
"code_challenge": "abc123",
"code_challenge_method": "S256",
"state": "xyz",
"server_id": "my-server",
},
follow_redirects=False,
)
logo_src = re.search(r'<img src="([^"]+)" class="logo-img"', page.text).group(1)
logo = TestClient(app).get(logo_src, follow_redirects=False)
assert (logo.status_code, logo.headers["content-type"].split(";")[0].startswith("image/")) == (200, True)
def test_authorize_get_missing_redirect_uri(client):
resp = client.get(
"/v1/mcp/oauth/authorize",

View file

@ -11,6 +11,7 @@ Routes covered:
from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
@ -192,11 +193,19 @@ 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."""
@pytest.mark.parametrize(
"params",
[
{},
{"theme": "dark"},
{"variant": "monogram"},
{"theme": "dark", "variant": "monogram"},
],
)
def test_get_image_bundled_logos_have_an_alpha_channel(client, monkeypatch, params):
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
response = client.get("/get_image", params={"theme": "dark"})
monkeypatch.delenv("UI_LOGO_PATH_DARK", raising=False)
response = client.get("/get_image", params=params)
body = response.content
shape = {
"status": response.status_code,
@ -212,16 +221,33 @@ def test_get_image_dark_theme_returns_logo_with_an_alpha_channel(client, monkeyp
}
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."""
@pytest.mark.parametrize(
("params", "bundled_file"),
[
({}, "logo.png"),
({"theme": "light"}, "logo.png"),
({"theme": "dark"}, "logo_dark.png"),
({"variant": "monogram"}, "logo_monogram.png"),
({"theme": "dark", "variant": "monogram"}, "logo_monogram_dark.png"),
],
)
def test_get_image_serves_the_bundled_logo_for_each_theme_and_variant(client, monkeypatch, params, bundled_file):
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}
monkeypatch.delenv("UI_LOGO_PATH_DARK", raising=False)
from litellm.proxy import proxy_server
expected = (Path(proxy_server.__file__).parent / bundled_file).read_bytes()
response = client.get("/get_image", params=params)
assert (response.status_code, response.content) == (200, expected)
def test_get_image_monogram_variant_keeps_serving_a_custom_ui_logo(client, monkeypatch, tmp_path):
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", "variant": "monogram"})
shape = {"status": response.status_code, "body": response.content}
assert shape == {"status": 200, "body": PNG_SIGNATURE + b"custom-logo-marker"}
def test_get_image_dark_theme_keeps_serving_a_custom_ui_logo(client, monkeypatch, tmp_path):
@ -290,7 +316,7 @@ def test_get_image_dark_logo_alone_still_serves_the_bundled_light_logo_in_light_
"status": response.status_code,
"media_type": response.headers.get("content-type", "").split(";")[0],
}
assert shape == {"status": 200, "media_type": "image/jpeg"}
assert shape == {"status": 200, "media_type": "image/png"}
def test_get_image_redirects_remote_url(client, monkeypatch):

View file

@ -6852,7 +6852,6 @@ async def test_get_image_non_root_fallback_to_default_logo(monkeypatch):
monkeypatch.setenv("LITELLM_NON_ROOT", "true")
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
# Track path.exists calls to verify it checks /var/lib/litellm/assets/logo.jpg
exists_calls = []
def exists_side_effect(path):
@ -6887,8 +6886,7 @@ async def test_get_image_non_root_fallback_to_default_logo(monkeypatch):
# Verify makedirs was called with /var/lib/litellm/assets
mock_makedirs.assert_called_once_with("/var/lib/litellm/assets", exist_ok=True)
# Verify that exists was called to check /var/lib/litellm/assets/logo.jpg
assets_logo_path = "/var/lib/litellm/assets/logo.jpg"
assets_logo_path = "/var/lib/litellm/assets/logo.png"
assert any(assets_logo_path in str(call) for call in exists_calls), f"Should check if {assets_logo_path} exists"
# Verify FileResponse was called (with fallback logo)
@ -7002,7 +7000,7 @@ async def test_get_image_default_logo_ignores_stale_cache(monkeypatch, tmp_path)
assert len(calls_to_file_response) == 1, "FileResponse should be called exactly once"
served_path = calls_to_file_response[0]
assert served_path != str(cache_path.resolve())
assert served_path.endswith("logo.jpg")
assert served_path.endswith("/logo.png")
@pytest.mark.asyncio
@ -7034,7 +7032,7 @@ async def test_get_image_custom_logo_missing_falls_through_to_default(monkeypatc
assert len(calls_to_file_response) == 1, "FileResponse should be called exactly once"
served_path = calls_to_file_response[0]
assert served_path != str(custom_logo_path), "Should not attempt to serve a non-existent custom logo"
assert served_path.endswith("logo.jpg")
assert served_path.endswith("/logo.png")
@pytest.mark.asyncio
@ -7067,7 +7065,7 @@ async def test_get_image_custom_logo_missing_no_cache_serves_default(monkeypatch
assert len(calls_to_file_response) == 1, "FileResponse should be called exactly once"
served_path = calls_to_file_response[0]
assert served_path != str(custom_logo_path), "Should not attempt to serve a non-existent custom logo"
assert served_path.endswith("logo.jpg"), f"Expected fallback to default logo.jpg, got {served_path}"
assert served_path.endswith("/logo.png"), f"Expected fallback to default logo.png, got {served_path}"
def test_get_config_normalizes_string_callbacks(monkeypatch):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 372.14 371.53">
<defs>
<style>
.cls-1 {
fill: #0117be;
}
</style>
</defs>
<path class="cls-1" d="M296.11,254.16c7.79-1.07,7.43-12.43.39-13.28-6.58-.79-14.5.86-21.17-.23l-50.86.37c-7.25,2.25-6.45,12.27,1.1,13.28l70.54-.14Z"/>
<path class="cls-1" d="M309.42,196.83c-35.23-15.22-72.84-26.08-109.96-35.87-33.05-8.71-69.92-16.92-103.8-21.32-31.89-4.14-63.43-3.41-95.51-3.23v6.54c36.23-.59,71.45.42,107.34,5.91,48.38,7.41,99.06,19.56,145.78,33.98,27.15,8.38,53.59,18.79,79.2,31.01l.1,67.98c12.46-2,31.2-8.81,39.28-20.3l-.04-24.63c-10.86-17.17-45.21-32.66-62.4-40.09Z"/>
<path class="cls-1" d="M14.79,115.72c-.07.12-.14.23-.21.35,23.36.36,46.79,1.59,69.97,4.75,36.98,5.04,75.62,13.61,111.4,24.06,17.99,5.25,35.13,13.22,54.25,12.22,4.13-.22,8.25-1.11,12.36-1.52l-.56-.73c-22.47-16.17-52.91-34.43-91.33-48.5C101.55,81.06,39.38,81.29.08,85.13l.05,31.74c3.47-.28,6.94-.56,10.39-.99,1.4-.02,2.84-.06,4.28-.16Z"/>
<path class="cls-1" d="M333.75-.37L37.67.08C16.84.11-.03,17.02,0,37.85l.06,39.13c27.48-4.04,66.25-6.26,112.14,2.78,1.52.3,6.38,1.29,12.73,2.77,54.99,12.83,104.51,41.22,151.11,71.93,22.76,14.99,46.61,30.85,68.19,47.42,8.92,6.85,19.82,15.04,27.57,24.61l-.29-189.2c-.03-20.83-16.94-37.69-37.77-37.66Z"/>
<path class="cls-1" d="M92.09,245.59h6.39s0,.03,0,.03l57.07-.09-48.14-66.6c-.53-.87-3.85-2.78-4.61-2.78h-4.75s0,.18,0,.18h-6.06s0-.09,0-.09l-91.76.14.11,69.62,91.76-.14v-.28Z"/>
<path class="cls-1" d="M.4,298.26l.05,35.68c.03,20.83,16.94,37.69,37.77,37.66l296.08-.45c20.83-.03,37.69-16.94,37.66-37.77l-.09-59.31c-17.35,19.08-52.22,22.16-75.65,23.75l-295.82.45Z"/>
<path class="cls-1" d="M159.06,253.3c-20.19.1-40.38.04-60.58.04v.26s-42.72.07-42.72.07c-.83.12-1.69.18-2.17,0l-53.25.08v9.76s166.32-.25,166.32-.25c-1.47-1.7-5.76-9.61-7.6-9.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 372.14 371.53">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<path class="cls-1" d="M296.11,254.16c7.79-1.07,7.43-12.43.39-13.28-6.58-.79-14.5.86-21.17-.23l-50.86.37c-7.25,2.25-6.45,12.27,1.1,13.28l70.54-.14Z"/>
<path class="cls-1" d="M309.42,196.83c-35.23-15.22-72.84-26.08-109.96-35.87-33.05-8.71-69.92-16.92-103.8-21.32-31.89-4.14-63.43-3.41-95.51-3.23v6.54c36.23-.59,71.45.42,107.34,5.91,48.38,7.41,99.06,19.56,145.78,33.98,27.15,8.38,53.59,18.79,79.2,31.01l.1,67.98c12.46-2,31.2-8.81,39.28-20.3l-.04-24.63c-10.86-17.17-45.21-32.66-62.4-40.09Z"/>
<path class="cls-1" d="M14.79,115.72c-.07.12-.14.23-.21.35,23.36.36,46.79,1.59,69.97,4.75,36.98,5.04,75.62,13.61,111.4,24.06,17.99,5.25,35.13,13.22,54.25,12.22,4.13-.22,8.25-1.11,12.36-1.52l-.56-.73c-22.47-16.17-52.91-34.43-91.33-48.5C101.55,81.06,39.38,81.29.08,85.13l.05,31.74c3.47-.28,6.94-.56,10.39-.99,1.4-.02,2.84-.06,4.28-.16Z"/>
<path class="cls-1" d="M333.75-.37L37.67.08C16.84.11-.03,17.02,0,37.85l.06,39.13c27.48-4.04,66.25-6.26,112.14,2.78,1.52.3,6.38,1.29,12.73,2.77,54.99,12.83,104.51,41.22,151.11,71.93,22.76,14.99,46.61,30.85,68.19,47.42,8.92,6.85,19.82,15.04,27.57,24.61l-.29-189.2c-.03-20.83-16.94-37.69-37.77-37.66Z"/>
<path class="cls-1" d="M92.09,245.59h6.39s0,.03,0,.03l57.07-.09-48.14-66.6c-.53-.87-3.85-2.78-4.61-2.78h-4.75s0,.18,0,.18h-6.06s0-.09,0-.09l-91.76.14.11,69.62,91.76-.14v-.28Z"/>
<path class="cls-1" d="M.4,298.26l.05,35.68c.03,20.83,16.94,37.69,37.77,37.66l296.08-.45c20.83-.03,37.69-16.94,37.66-37.77l-.09-59.31c-17.35,19.08-52.22,22.16-75.65,23.75l-295.82.45Z"/>
<path class="cls-1" d="M159.06,253.3c-20.19.1-40.38.04-60.58.04v.26s-42.72.07-42.72.07c-.83.12-1.69.18-2.17,0l-53.25.08v9.76s166.32-.25,166.32-.25c-1.47-1.7-5.76-9.61-7.6-9.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -44,7 +44,7 @@ describe("guardrail_garden_data logos", () => {
it("uses the LiteLLM logo for every content filter card", () => {
for (const card of LITELLM_CONTENT_FILTER_CARDS) {
expect(card.logo, `card ${card.id}`).toContain("litellm_logo.jpg");
expect(card.logo, `card ${card.id}`).toContain("litellm_monogram.svg");
}
});

View file

@ -13,7 +13,7 @@ import guardrailsAiLogo from "../../../../../public/assets/logos/guardrails_ai.j
import javelinLogo from "../../../../../public/assets/logos/javelin.png";
import lakeraAiLogo from "../../../../../public/assets/logos/lakeraai.jpeg";
import lassoLogo from "../../../../../public/assets/logos/lasso.png";
import litellmLogo from "../../../../../public/assets/logos/litellm_logo.jpg";
import litellmLogo from "../../../../../public/assets/logos/litellm_monogram.svg";
import microsoftAzureLogo from "../../../../../public/assets/logos/microsoft_azure.svg";
import nomaSecurityLogo from "../../../../../public/assets/logos/noma_security.png";
import openaiSmallLogo from "../../../../../public/assets/logos/openai_small.svg";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -1,4 +1,4 @@
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, screen, waitFor, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../tests/test-utils";
import Sidebar, { menuGroups, getBreadcrumb } from "./leftnav";
@ -136,6 +136,37 @@ describe("Sidebar (leftnav)", () => {
expect(classesOf(dark).has("dark:block")).toBe(true);
});
const collapsedLogos = () => {
const home = within(screen.getByRole("link", { name: /litellm home/i }));
return {
light: home.getByRole("img", { name: "LiteLLM" }),
dark: home.getByRole("presentation", { hidden: true }),
};
};
it("requests the bundled monogram for both themes when collapsed", () => {
renderWithProviders(<Sidebar collapsed />);
const { light, dark } = collapsedLogos();
expect(light).toHaveAttribute("src", expect.stringMatching(/\/get_image\?variant=monogram$/));
expect(dark).toHaveAttribute("src", expect.stringMatching(/\/get_image\?theme=dark&variant=monogram$/));
});
it("keeps a configured custom logo when collapsed instead of the LiteLLM monogram", () => {
mockUseThemeImpl = () => ({
...unbrandedTheme(),
logoUrl: "https://cdn.example.com/logo.png",
logoUrlDark: "https://cdn.example.com/logo-dark.png",
});
renderWithProviders(<Sidebar collapsed />);
const { light, dark } = collapsedLogos();
expect(light).toHaveAttribute("src", "https://cdn.example.com/logo.png");
expect(dark).toHaveAttribute("src", "https://cdn.example.com/logo-dark.png");
});
it("prefers a configured dark logo over the light one in dark mode", () => {
mockUseThemeImpl = () => ({
...unbrandedTheme(),

View file

@ -84,6 +84,11 @@ 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";
function bundledLogoSrc(baseUrl: string, { dark, monogram }: { dark: boolean; monogram: boolean }): string {
const query = [dark && "theme=dark", monogram && "variant=monogram"].filter(Boolean).join("&");
return `${baseUrl}/get_image${query ? `?${query}` : ""}`;
}
interface SidebarProps {
collapsed?: boolean;
onToggleCollapsed?: () => void;
@ -617,9 +622,9 @@ const Sidebar_: React.FC<SidebarProps> = ({
);
};
const logoSrc = logoUrl || `${baseUrl}/get_image`;
const logoSrc = logoUrl || bundledLogoSrc(baseUrl, { dark: false, monogram: collapsed });
const reachableDarkLogo = logoUrlDark === erroredDarkLogo ? null : logoUrlDark;
const darkLogoSrc = reachableDarkLogo || logoUrl || `${baseUrl}/get_image?theme=dark`;
const darkLogoSrc = reachableDarkLogo || logoUrl || bundledLogoSrc(baseUrl, { dark: true, monogram: collapsed });
return (
<Sidebar collapsed={collapsed}>

View file

@ -55907,6 +55907,7 @@ export interface operations {
parameters: {
query?: {
theme?: ("light" | "dark") | null;
variant?: "full" | "monogram";
};
header?: never;
path?: never;

View file

@ -6,6 +6,12 @@ describe("logoTreatmentFor", () => {
expect(logoTreatmentFor("/ui/assets/logos/github.svg")).toBe("invert");
});
it("inverts the bundled LiteLLM monogram into the white brand mark on dark", () => {
expect(logoTreatmentFor("/litellm-asset-prefix/_next/static/media/litellm_monogram.16r2_rdj4dq8r.svg")).toBe(
"invert",
);
});
it("marks a multicolor dark mark for a plate instead of inversion", () => {
expect(logoTreatmentFor("/ui/assets/logos/fireworks.svg")).toBe("plate");
});

View file

@ -15,6 +15,7 @@ const TREATMENT_BY_ASSET: Readonly<Record<string, LogoTreatment>> = {
"lago.svg": "invert",
"lambda.svg": "invert",
"langflow.svg": "invert",
"litellm_monogram.svg": "invert",
"lmstudio.svg": "invert",
"moonshot.svg": "invert",
"nebius.svg": "invert",