From a8026154ab43624bdad8f9789a8e4af01c24904f Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 19 Feb 2026 18:32:48 -0800 Subject: [PATCH] [Fix] /get_image returns stale cached logo instead of custom UI_LOGO_PATH The /get_image endpoint checked for cached_logo.jpg before reading the UI_LOGO_PATH env var, so a pre-existing cache (e.g. baked into the base Docker image) would always be served, ignoring the user's custom logo. Move the UI_LOGO_PATH read before the cache check and serve local file paths directly, bypassing the cache. The cache optimization is preserved for HTTP URLs and the default logo where it is actually needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- litellm/proxy/proxy_server.py | 12 ++-- tests/test_litellm/proxy/test_proxy_server.py | 70 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0e702abfc6c..45d1af06525 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -10694,13 +10694,17 @@ async def get_image(): cache_dir = assets_dir if os.access(assets_dir, os.W_OK) else current_dir cache_path = os.path.join(cache_dir, "cached_logo.jpg") - # [OPTIMIZATION] Check if the cached image exists first - if os.path.exists(cache_path): - return FileResponse(cache_path, media_type="image/jpeg") - logo_path = os.getenv("UI_LOGO_PATH", default_logo) verbose_proxy_logger.debug("Reading logo from path: %s", logo_path) + # If UI_LOGO_PATH points to a local file, serve it directly (skip cache) + if logo_path != default_logo and not logo_path.startswith(("http://", "https://")): + return FileResponse(logo_path, media_type="image/jpeg") + + # [OPTIMIZATION] For HTTP URLs and default logo, check if the cached image exists + if os.path.exists(cache_path): + return FileResponse(cache_path, media_type="image/jpeg") + # Check if the logo path is an HTTP/HTTPS URL if logo_path.startswith(("http://", "https://")): try: diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 79b5e34022f..532e11e70f1 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -3154,6 +3154,76 @@ async def test_get_image_root_case_uses_current_dir(monkeypatch): assert mock_file_response.called, "FileResponse should be called" +@pytest.mark.asyncio +async def test_get_image_custom_local_logo_bypasses_cache(monkeypatch): + """ + Test that when UI_LOGO_PATH is set to a local file, get_image serves it + directly and does not return a stale cached_logo.jpg. + + Regression test: previously the cache check ran before reading UI_LOGO_PATH, + so a pre-existing cached_logo.jpg (e.g. from the base Docker image) would + always be returned, ignoring the user's custom logo. + """ + from unittest.mock import patch + + from litellm.proxy.proxy_server import get_image + + monkeypatch.setenv("UI_LOGO_PATH", "/app/custom_logo.jpg") + monkeypatch.delenv("LITELLM_NON_ROOT", raising=False) + monkeypatch.delenv("LITELLM_ASSETS_PATH", raising=False) + + calls_to_file_response = [] + + def fake_file_response(path, **kwargs): + calls_to_file_response.append(path) + return MagicMock() + + with patch("litellm.proxy.proxy_server.os.path.exists", return_value=True), \ + patch("litellm.proxy.proxy_server.os.access", return_value=True), \ + patch("litellm.proxy.proxy_server.FileResponse", side_effect=fake_file_response): + + await get_image() + + assert len(calls_to_file_response) == 1, "FileResponse should be called exactly once" + assert calls_to_file_response[0] == "/app/custom_logo.jpg", ( + f"Expected custom logo path, got {calls_to_file_response[0]}. " + "A stale cached_logo.jpg may have been returned instead." + ) + + +@pytest.mark.asyncio +async def test_get_image_default_logo_still_uses_cache(monkeypatch): + """ + Test that when UI_LOGO_PATH is NOT set (default logo), the cache + optimization still works — cached_logo.jpg is returned if it exists. + """ + from unittest.mock import patch + + from litellm.proxy.proxy_server import get_image + + monkeypatch.delenv("UI_LOGO_PATH", raising=False) + monkeypatch.delenv("LITELLM_NON_ROOT", raising=False) + monkeypatch.delenv("LITELLM_ASSETS_PATH", raising=False) + + calls_to_file_response = [] + + def fake_file_response(path, **kwargs): + calls_to_file_response.append(path) + return MagicMock() + + with patch("litellm.proxy.proxy_server.os.path.exists", return_value=True), \ + patch("litellm.proxy.proxy_server.os.access", return_value=True), \ + patch("litellm.proxy.proxy_server.FileResponse", side_effect=fake_file_response): + + await get_image() + + assert len(calls_to_file_response) == 1, "FileResponse should be called exactly once" + served_path = calls_to_file_response[0] + assert served_path.endswith("cached_logo.jpg"), ( + f"Expected cached_logo.jpg for default logo, got {served_path}" + ) + + def test_get_config_normalizes_string_callbacks(monkeypatch): """ Test that /get/config/callbacks normalizes string callbacks to lists.