diff --git a/cookbook/misc/test_responses_api.py b/cookbook/misc/test_responses_api.py index 0011db4664d..68da5fb6cd0 100644 --- a/cookbook/misc/test_responses_api.py +++ b/cookbook/misc/test_responses_api.py @@ -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}", }, ], } diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index d4c07d56d90..eca12855afa 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -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 \ diff --git a/litellm-rust/crates/gateway-ui/Cargo.toml b/litellm-rust/crates/gateway-ui/Cargo.toml index 0e0d76fbdbf..94546ef9118 100644 --- a/litellm-rust/crates/gateway-ui/Cargo.toml +++ b/litellm-rust/crates/gateway-ui/Cargo.toml @@ -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 diff --git a/litellm-rust/crates/gateway-ui/src/dashboard.rs b/litellm-rust/crates/gateway-ui/src/dashboard.rs index 96588db9883..2642506fde6 100644 --- a/litellm-rust/crates/gateway-ui/src/dashboard.rs +++ b/litellm-rust/crates/gateway-ui/src/dashboard.rs @@ -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, + variant: Option, +} + +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) -> 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) -> Router { }) }), ) - .route_service( + .route( "/get_image", - ServeFile::new(directory.join("assets/logos/litellm_logo.jpg")), + get(move |Query(query): Query, request: Request| { + ServeFile::new(logos.join(logo_file(&query))).oneshot(request) + }), ) .route_service( "/get_favicon", diff --git a/litellm-rust/crates/gateway-ui/tests/assets.rs b/litellm-rust/crates/gateway-ui/tests/assets.rs index d77763319b6..ba741fbf2ec 100644 --- a/litellm-rust/crates/gateway-ui/tests/assets.rs +++ b/litellm-rust/crates/gateway-ui/tests/assets.rs @@ -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); +} diff --git a/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py b/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py index 87b6d36529a..b51da626a60 100644 --- a/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py @@ -506,7 +506,7 @@ def _build_authorize_html(
- LiteLLM + LiteLLM →
diff --git a/litellm/proxy/_experimental/out/assets/logos/litellm_logo.png b/litellm/proxy/_experimental/out/assets/logos/litellm_logo.png new file mode 100644 index 00000000000..4e47364ce69 Binary files /dev/null and b/litellm/proxy/_experimental/out/assets/logos/litellm_logo.png differ diff --git a/litellm/proxy/_experimental/out/assets/logos/litellm_logo_dark.png b/litellm/proxy/_experimental/out/assets/logos/litellm_logo_dark.png new file mode 100644 index 00000000000..c7f45c18f19 Binary files /dev/null and b/litellm/proxy/_experimental/out/assets/logos/litellm_logo_dark.png differ diff --git a/litellm/proxy/_experimental/out/assets/logos/litellm_monogram.svg b/litellm/proxy/_experimental/out/assets/logos/litellm_monogram.svg new file mode 100644 index 00000000000..82cbe3eeb03 --- /dev/null +++ b/litellm/proxy/_experimental/out/assets/logos/litellm_monogram.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/assets/logos/litellm_monogram_dark.svg b/litellm/proxy/_experimental/out/assets/logos/litellm_monogram_dark.svg new file mode 100644 index 00000000000..bc3771b7330 --- /dev/null +++ b/litellm/proxy/_experimental/out/assets/logos/litellm_monogram_dark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/favicon.ico b/litellm/proxy/_experimental/out/favicon.ico index 7c45601d5c3..657ee1e24e8 100644 Binary files a/litellm/proxy/_experimental/out/favicon.ico and b/litellm/proxy/_experimental/out/favicon.ico differ diff --git a/litellm/proxy/logo.jpg b/litellm/proxy/logo.jpg deleted file mode 100644 index a10a1d24969..00000000000 Binary files a/litellm/proxy/logo.jpg and /dev/null differ diff --git a/litellm/proxy/logo.png b/litellm/proxy/logo.png new file mode 100644 index 00000000000..4e47364ce69 Binary files /dev/null and b/litellm/proxy/logo.png differ diff --git a/litellm/proxy/logo_dark.png b/litellm/proxy/logo_dark.png index f92fbefdd22..c7f45c18f19 100644 Binary files a/litellm/proxy/logo_dark.png and b/litellm/proxy/logo_dark.png differ diff --git a/litellm/proxy/logo_monogram.png b/litellm/proxy/logo_monogram.png new file mode 100644 index 00000000000..5a2816197ab Binary files /dev/null and b/litellm/proxy/logo_monogram.png differ diff --git a/litellm/proxy/logo_monogram_dark.png b/litellm/proxy/logo_monogram_dark.png new file mode 100644 index 00000000000..6e44c798a63 Binary files /dev/null and b/litellm/proxy/logo_monogram_dark.png differ diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 57a94dd7387..f89dde04e06 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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) diff --git a/litellm/proxy/swagger/favicon.ico b/litellm/proxy/swagger/favicon.ico index 7c45601d5c3..657ee1e24e8 100644 Binary files a/litellm/proxy/swagger/favicon.ico and b/litellm/proxy/swagger/favicon.ico differ diff --git a/litellm/proxy/swagger/favicon.png b/litellm/proxy/swagger/favicon.png index 261b7504da8..c7c16fbf709 100644 Binary files a/litellm/proxy/swagger/favicon.png and b/litellm/proxy/swagger/favicon.png differ diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py index 6d2ea2ff301..d7205a3095e 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_byok_oauth_endpoints.py @@ -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' + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/litellm-dashboard/public/assets/logos/litellm_monogram_dark.svg b/ui/litellm-dashboard/public/assets/logos/litellm_monogram_dark.svg new file mode 100644 index 00000000000..bc3771b7330 --- /dev/null +++ b/ui/litellm-dashboard/public/assets/logos/litellm_monogram_dark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_garden_data.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_garden_data.test.ts index eb5d47d7891..a27dd344c95 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_garden_data.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_garden_data.test.ts @@ -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"); } }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx index 476bcd3a8ae..8df7dfb1403 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx @@ -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"; diff --git a/ui/litellm-dashboard/src/app/favicon.ico b/ui/litellm-dashboard/src/app/favicon.ico index 7c45601d5c3..657ee1e24e8 100644 Binary files a/ui/litellm-dashboard/src/app/favicon.ico and b/ui/litellm-dashboard/src/app/favicon.ico differ diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx index 5fa6e9728cf..df94772400e 100644 --- a/ui/litellm-dashboard/src/components/leftnav.test.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx @@ -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(); + + 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(); + + 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(), diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 1af6095413a..2fab3d9b111 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -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 = ({ ); }; - 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 ( diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index c9b17d6fc0c..e9ecd3492f7 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -55907,6 +55907,7 @@ export interface operations { parameters: { query?: { theme?: ("light" | "dark") | null; + variant?: "full" | "monogram"; }; header?: never; path?: never; diff --git a/ui/litellm-dashboard/src/lib/logoTreatments.test.ts b/ui/litellm-dashboard/src/lib/logoTreatments.test.ts index 08ea283a7a3..828ea72a57f 100644 --- a/ui/litellm-dashboard/src/lib/logoTreatments.test.ts +++ b/ui/litellm-dashboard/src/lib/logoTreatments.test.ts @@ -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"); }); diff --git a/ui/litellm-dashboard/src/lib/logoTreatments.ts b/ui/litellm-dashboard/src/lib/logoTreatments.ts index 1448dfa686d..fc19c5c364d 100644 --- a/ui/litellm-dashboard/src/lib/logoTreatments.ts +++ b/ui/litellm-dashboard/src/lib/logoTreatments.ts @@ -15,6 +15,7 @@ const TREATMENT_BY_ASSET: Readonly> = { "lago.svg": "invert", "lambda.svg": "invert", "langflow.svg": "invert", + "litellm_monogram.svg": "invert", "lmstudio.svg": "invert", "moonshot.svg": "invert", "nebius.svg": "invert",