mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-11 17:05:43 +00:00
Merge pull request #17989 from Jetemple/non-root-ui-path-fix
fix: update UI path handling for non-root Docker and restructure HTML…
This commit is contained in:
@@ -5,6 +5,7 @@ import io
|
||||
import os
|
||||
import random
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -939,31 +940,68 @@ origins = ["*"]
|
||||
# get current directory
|
||||
try:
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
ui_path = os.path.join(current_dir, "_experimental", "out")
|
||||
packaged_ui_path = os.path.join(current_dir, "_experimental", "out")
|
||||
ui_path = packaged_ui_path
|
||||
litellm_asset_prefix = "/litellm-asset-prefix"
|
||||
|
||||
# For non-root Docker, use the pre-built UI from /tmp/litellm_ui
|
||||
# Support both "true" and "True" for case-insensitive comparison
|
||||
if os.getenv("LITELLM_NON_ROOT", "").lower() == "true":
|
||||
non_root_ui_path = "/tmp/litellm_ui"
|
||||
def _dir_has_content(path: str) -> bool:
|
||||
try:
|
||||
return os.path.isdir(path) and any(os.scandir(path))
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
# Check if the UI was built and exists at the expected location
|
||||
if os.path.exists(non_root_ui_path) and os.listdir(non_root_ui_path):
|
||||
# Use a writable runtime UI directory whenever possible.
|
||||
# This prevents mutating the packaged UI directory (e.g. site-packages or the repo checkout)
|
||||
# and ensures extensionless routes like /ui/login work via <route>/index.html.
|
||||
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
|
||||
runtime_ui_path = "/tmp/litellm_ui"
|
||||
|
||||
if _dir_has_content(runtime_ui_path):
|
||||
if is_non_root:
|
||||
verbose_proxy_logger.info(
|
||||
f"Using pre-built UI for non-root Docker: {non_root_ui_path}"
|
||||
f"Using pre-built UI for non-root Docker: {runtime_ui_path}"
|
||||
)
|
||||
verbose_proxy_logger.info(
|
||||
f"UI files found: {len(os.listdir(non_root_ui_path))} items"
|
||||
)
|
||||
ui_path = non_root_ui_path
|
||||
else:
|
||||
verbose_proxy_logger.info(
|
||||
f"Using cached runtime UI directory: {runtime_ui_path}"
|
||||
)
|
||||
ui_path = runtime_ui_path
|
||||
else:
|
||||
if is_non_root:
|
||||
verbose_proxy_logger.error(
|
||||
f"UI not found at {non_root_ui_path}. UI will not be available."
|
||||
f"UI not found at {runtime_ui_path}. Attempting to populate it from packaged UI."
|
||||
)
|
||||
verbose_proxy_logger.error(
|
||||
f"Path exists: {os.path.exists(non_root_ui_path)}, Has content: {os.path.exists(non_root_ui_path) and bool(os.listdir(non_root_ui_path))}"
|
||||
f"Path exists: {os.path.exists(runtime_ui_path)}, Has content: {_dir_has_content(runtime_ui_path)}"
|
||||
)
|
||||
|
||||
try:
|
||||
os.makedirs(runtime_ui_path, exist_ok=True)
|
||||
if not _dir_has_content(runtime_ui_path) and _dir_has_content(
|
||||
packaged_ui_path
|
||||
):
|
||||
shutil.copytree(
|
||||
packaged_ui_path,
|
||||
runtime_ui_path,
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
except Exception as e:
|
||||
if is_non_root:
|
||||
verbose_proxy_logger.exception(
|
||||
f"Failed to populate runtime UI directory {runtime_ui_path} from {packaged_ui_path}: {e}"
|
||||
)
|
||||
else:
|
||||
if _dir_has_content(runtime_ui_path):
|
||||
if is_non_root:
|
||||
verbose_proxy_logger.info(
|
||||
f"Using populated UI for non-root Docker: {runtime_ui_path}"
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.info(
|
||||
f"Using populated runtime UI directory: {runtime_ui_path}"
|
||||
)
|
||||
ui_path = runtime_ui_path
|
||||
|
||||
# Only modify files if a custom server root path is set
|
||||
if server_root_path and server_root_path != "/":
|
||||
# Iterate through files in the UI directory
|
||||
@@ -1042,16 +1080,25 @@ try:
|
||||
target_path = os.path.join(target_dir, "index.html")
|
||||
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
os.replace(file_path, target_path)
|
||||
try:
|
||||
os.replace(file_path, target_path)
|
||||
except FileNotFoundError:
|
||||
# Another process may have already moved this file.
|
||||
continue
|
||||
|
||||
# Handle HTML file restructuring
|
||||
# Skip this for non-root Docker since it's done at build time
|
||||
# Support both "true" and "True" for case-insensitive comparison
|
||||
if os.getenv("LITELLM_NON_ROOT", "").lower() != "true":
|
||||
_restructure_ui_html_files(ui_path)
|
||||
# Always restructure the directory we actually serve, but avoid mutating the packaged UI.
|
||||
# This is critical for extensionless routes like /ui/login (expects login/index.html).
|
||||
if ui_path != packaged_ui_path:
|
||||
try:
|
||||
_restructure_ui_html_files(ui_path)
|
||||
except PermissionError as e:
|
||||
verbose_proxy_logger.exception(
|
||||
f"Permission error while restructuring UI directory {ui_path}: {e}"
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.info(
|
||||
"Skipping runtime HTML restructuring for non-root Docker (already done at build time)"
|
||||
f"Skipping runtime HTML restructuring for packaged UI directory: {ui_path}"
|
||||
)
|
||||
|
||||
except Exception:
|
||||
|
||||
@@ -15,6 +15,7 @@ import httpx
|
||||
import pytest
|
||||
import yaml
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
sys.path.insert(
|
||||
@@ -196,6 +197,32 @@ def test_restructure_ui_html_files_handles_nested_routes(tmp_path):
|
||||
)
|
||||
|
||||
|
||||
def test_ui_extensionless_route_requires_restructure(tmp_path):
|
||||
"""Regression for non-root fallback: /ui/login expects login/index.html."""
|
||||
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
ui_root = tmp_path / "ui"
|
||||
ui_root.mkdir()
|
||||
(ui_root / "index.html").write_text("index")
|
||||
(ui_root / "login.html").write_text("login")
|
||||
|
||||
fastapi_app = FastAPI()
|
||||
fastapi_app.mount(
|
||||
"/ui", StaticFiles(directory=str(ui_root), html=True), name="ui"
|
||||
)
|
||||
client = TestClient(fastapi_app)
|
||||
|
||||
assert client.get("/ui/login.html").status_code == 200
|
||||
assert client.get("/ui/login").status_code == 404
|
||||
|
||||
proxy_server._restructure_ui_html_files(str(ui_root))
|
||||
|
||||
response = client.get("/ui/login")
|
||||
assert response.status_code == 200
|
||||
assert "login" in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_scheduled_jobs_credentials(monkeypatch):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user