fix(proxy): make admin UI root-path rewrite idempotent

This commit is contained in:
Rashmi 2026-10-05 01:23:01 +05:30 • committed by Rashmi
parent e1d16f51d1
commit 99c3105ef6
3 changed files with 192 additions and 18 deletions

View file

@ -844,6 +844,7 @@ from litellm.proxy.utils import (
migrate_passwords_to_scrypt_async,
model_dump_with_preserved_fields,
prefetch_config_params,
rewrite_ui_content,
update_spend,
)
from litellm.proxy.video_endpoints.endpoints import router as video_router
@ -2352,20 +2353,15 @@ try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Replace the asset prefix with the server root path
modified_content = content.replace(
f"{litellm_asset_prefix}",
f"{server_root_path}",
modified_content = rewrite_ui_content(
content=content,
server_root_path=server_root_path,
litellm_asset_prefix=litellm_asset_prefix,
)
# Replace the /.well-known/litellm-ui-config with the server root path
modified_content = modified_content.replace(
"/litellm/.well-known/litellm-ui-config",
f"{server_root_path}/.well-known/litellm-ui-config",
)
with open(file_path, "w", encoding="utf-8") as f:
f.write(modified_content)
if modified_content != content:
with open(file_path, "w", encoding="utf-8") as f:
f.write(modified_content)
except (UnicodeDecodeError, PermissionError, OSError):
# Skip binary files or files we can't write to
continue

View file

@ -6,6 +6,7 @@ import inspect
import json
import math
import os
import re
import smtplib
import ssl
import sys
@ -8412,6 +8413,41 @@ def normalize_route_for_root_path(route: str) -> str | None:
return route
UI_CONFIG_ENDPOINT_PATTERN: Final = re.compile(
r"(?<![a-zA-Z0-9_/-])/(?:[a-zA-Z0-9_.-]+/)*\.well-known/litellm-ui-config"
)
UI_ASSET_PREFIX_PATTERN: Final = re.compile(r"(?<![a-zA-Z0-9_/-])/(?:[a-zA-Z0-9_.-]+/)*_next/")
def rewrite_ui_content(
content: str,
server_root_path: str,
litellm_asset_prefix: str = "/litellm-asset-prefix",
) -> str:
"""
Rewrite packaged admin UI asset references and configuration endpoint paths
for a configured SERVER_ROOT_PATH.
This transformation is idempotent: repeatedly applying it for the same
server_root_path (e.g. across container or proxy restarts) preserves the
configured path instead of nesting it (e.g. /foo/foo/litellm/...).
"""
if not server_root_path or server_root_path == "/":
return content
clean_root_path: Final = "/" + server_root_path.strip("/")
content_with_assets: Final = UI_ASSET_PREFIX_PATTERN.sub(f"{clean_root_path}/_next/", content)
content_with_prefix: Final = (
re.sub(rf"(?<![a-zA-Z0-9_/-]){re.escape(litellm_asset_prefix)}(?!/)", clean_root_path, content_with_assets)
if litellm_asset_prefix and litellm_asset_prefix != clean_root_path
else content_with_assets
)
target_endpoint: Final = f"{clean_root_path}/.well-known/litellm-ui-config"
return UI_CONFIG_ENDPOINT_PATTERN.sub(target_endpoint, content_with_prefix)
def get_prisma_client_or_throw(message: str):
from litellm.proxy.proxy_server import prisma_client

View file

@ -1,5 +1,6 @@
import os
from unittest import mock
from litellm.proxy import utils
@ -14,9 +15,7 @@ def test_get_server_root_path_unset():
del os.environ["SERVER_ROOT_PATH"]
root_path = utils.get_server_root_path()
assert (
root_path == ""
), "Should return empty string when unset to allow X-Forwarded-Prefix"
assert root_path == "", "Should return empty string when unset to allow X-Forwarded-Prefix"
def test_get_server_root_path_set():
@ -34,9 +33,7 @@ def test_get_server_root_path_empty_string():
"""
with mock.patch.dict(os.environ, {"SERVER_ROOT_PATH": ""}, clear=True):
root_path = utils.get_server_root_path()
assert (
root_path == ""
), "Should return empty string when explicitly set to empty"
assert root_path == "", "Should return empty string when explicitly set to empty"
# Integration test simulation for FastAPI app initialization
@ -62,3 +59,148 @@ def test_fastapi_app_initialization_mock():
server_root_path = utils.get_server_root_path()
app = FastAPI(root_path=server_root_path)
assert app.root_path == "/custom-root"
SAMPLE_PRISTINE_UI_CONTENT = (
"let t=h?`${h}/litellm/.well-known/litellm-ui-config`:"
'"/litellm/.well-known/litellm-ui-config";'
'<link rel="preload" href="/litellm-asset-prefix/_next/static/css/app.css" />'
)
def test_rewrite_ui_content_first_rewrite():
"""Test A: First rewrite from pristine path with /foo/litellm."""
result = utils.rewrite_ui_content(
content=SAMPLE_PRISTINE_UI_CONTENT,
server_root_path="/foo/litellm",
)
assert "`${h}/foo/litellm/.well-known/litellm-ui-config`" in result
assert ':"/foo/litellm/.well-known/litellm-ui-config"' in result
assert 'href="/foo/litellm/_next/static/css/app.css"' in result
def test_rewrite_ui_content_repeated_rewrite():
"""
Test B: Repeated rewrite must be idempotent.
Applying rewrite twice with /foo/litellm must NOT produce /foo/foo/litellm.
"""
first = utils.rewrite_ui_content(
content=SAMPLE_PRISTINE_UI_CONTENT,
server_root_path="/foo/litellm",
)
second = utils.rewrite_ui_content(
content=first,
server_root_path="/foo/litellm",
)
assert second == first
assert "/foo/foo/litellm" not in second
assert "`${h}/foo/litellm/.well-known/litellm-ui-config`" in second
assert ':"/foo/litellm/.well-known/litellm-ui-config"' in second
def test_rewrite_ui_content_default_path():
"""Test C: SERVER_ROOT_PATH=/litellm behavior must remain unchanged."""
first = utils.rewrite_ui_content(
content=SAMPLE_PRISTINE_UI_CONTENT,
server_root_path="/litellm",
)
second = utils.rewrite_ui_content(
content=first,
server_root_path="/litellm",
)
assert "`${h}/litellm/.well-known/litellm-ui-config`" in first
assert ':"/litellm/.well-known/litellm-ui-config"' in first
assert second == first
def test_rewrite_ui_content_nested_and_edge_paths():
"""Test other supported root paths: /api/litellm, nested, custom, and slash normalization."""
for root_path in [
"/api/litellm",
"/app/gateway/litellm",
"/custom-proxy",
"/nested/litellm/",
"prefix/litellm",
]:
first = utils.rewrite_ui_content(
content=SAMPLE_PRISTINE_UI_CONTENT,
server_root_path=root_path,
)
second = utils.rewrite_ui_content(
content=first,
server_root_path=root_path,
)
assert second == first, f"Failed idempotency for {root_path}"
clean_path = "/" + root_path.strip("/")
expected_endpoint = f"{clean_path}/.well-known/litellm-ui-config"
assert f':"{expected_endpoint}"' in first, f"Expected endpoint {expected_endpoint} not in output"
def test_rewrite_ui_content_healing_and_reconfiguration():
"""
Test healing previously corrupted paths (e.g. from buggy restarts)
and smoothly switching between different configured root paths.
"""
corrupted = (
'let t=h?`${h}/foo/foo/litellm/.well-known/litellm-ui-config`:"/foo/foo/litellm/.well-known/litellm-ui-config";'
)
healed = utils.rewrite_ui_content(
content=corrupted,
server_root_path="/foo/litellm",
)
assert "`${h}/foo/litellm/.well-known/litellm-ui-config`" in healed
assert ':"/foo/litellm/.well-known/litellm-ui-config"' in healed
reconfigured = utils.rewrite_ui_content(
content=healed,
server_root_path="/bar/litellm",
)
assert "`${h}/bar/litellm/.well-known/litellm-ui-config`" in reconfigured
assert ':"/bar/litellm/.well-known/litellm-ui-config"' in reconfigured
assert utils.rewrite_ui_content(reconfigured, "/bar/litellm") == reconfigured
def test_rewrite_ui_content_actual_packaged_ui_chunk():
"""Verify idempotency on the actual packaged UI chunk file if available."""
from pathlib import Path
current_dir = Path(__file__).resolve().parent
repo_root = current_dir.parents[2]
packaged_ui_path = repo_root / "litellm" / "proxy" / "_experimental" / "out"
if not packaged_ui_path.exists():
return
for chunk in packaged_ui_path.rglob("*.js"):
content = chunk.read_text(encoding="utf-8", errors="ignore")
if ".well-known/litellm-ui-config" in content:
once = utils.rewrite_ui_content(content, "/foo/litellm")
twice = utils.rewrite_ui_content(once, "/foo/litellm")
assert once == twice
assert "/foo/foo/litellm" not in twice
assert "/foo/litellm/.well-known/litellm-ui-config" in twice
def test_rewrite_ui_content_asset_reconfiguration_and_prefix_edge_cases():
"""Verify asset URL rewriting handles root path changes and root paths containing asset prefix."""
sample_html = '<link rel="stylesheet" href="/litellm-asset-prefix/_next/static/css/app.css" />'
first = utils.rewrite_ui_content(sample_html, "/foo/litellm")
assert 'href="/foo/litellm/_next/static/css/app.css"' in first
second = utils.rewrite_ui_content(first, "/foo/litellm")
assert second == first
reconfigured = utils.rewrite_ui_content(first, "/bar/litellm")
assert 'href="/bar/litellm/_next/static/css/app.css"' in reconfigured
assert utils.rewrite_ui_content(reconfigured, "/bar/litellm") == reconfigured
root_with_prefix = "/litellm-asset-prefix/custom"
with_prefix = utils.rewrite_ui_content(sample_html, root_with_prefix)
assert f'href="{root_with_prefix}/_next/static/css/app.css"' in with_prefix
assert utils.rewrite_ui_content(with_prefix, root_with_prefix) == with_prefix
def test_rewrite_ui_content_empty_or_root_path():
assert utils.rewrite_ui_content(SAMPLE_PRISTINE_UI_CONTENT, "") == SAMPLE_PRISTINE_UI_CONTENT
assert utils.rewrite_ui_content(SAMPLE_PRISTINE_UI_CONTENT, "/") == SAMPLE_PRISTINE_UI_CONTENT