This commit is contained in:
Shehryar Ali 2026-11-04 06:01:28 +00:00 • committed by GitHub
commit f5849fdf0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 40 deletions

View file

@ -2729,6 +2729,58 @@ try:
except (PermissionError, OSError) as e:
return False, str(e)
def _rewrite_ui_config_path(content: str, server_root_path: str) -> str:
"""
Point the UI's config request at server_root_path, keeping already-rewritten paths.
The UI files are rewritten in place on every startup, and when server_root_path ends
in /litellm the rewritten path still contains the original one, so a plain replace
would prepend the root path again on each restart.
"""
ui_config_path: Final = f"{server_root_path}/.well-known/litellm-ui-config"
return ui_config_path.join(
part.replace("/litellm/.well-known/litellm-ui-config", ui_config_path)
for part in content.split(ui_config_path)
)
def _apply_server_root_path_to_ui(ui_dir: str, server_root_path: str) -> None:
for root, _dirs, files in os.walk(ui_dir):
for filename in files:
file_path = os.path.join(root, filename)
# Skip binary files and files that don't need path replacement
if filename.endswith(
(
".png",
".jpg",
".jpeg",
".gif",
".ico",
".woff",
".woff2",
".ttf",
".eot",
)
):
continue
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}",
)
# Replace the /.well-known/litellm-ui-config with the server root path
modified_content = _rewrite_ui_config_path(modified_content, server_root_path)
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
# 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.
@ -2814,46 +2866,7 @@ try:
ui_path,
)
else:
# Iterate through files in the UI directory
for root, dirs, files in os.walk(ui_path):
for filename in files:
file_path = os.path.join(root, filename)
# Skip binary files and files that don't need path replacement
if filename.endswith(
(
".png",
".jpg",
".jpeg",
".gif",
".ico",
".woff",
".woff2",
".ttf",
".eot",
)
):
continue
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}",
)
# 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)
except (UnicodeDecodeError, PermissionError, OSError):
# Skip binary files or files we can't write to
continue
_apply_server_root_path_to_ui(ui_path, server_root_path)
# # Mount the _next directory at the root level
app.mount(

View file

@ -3246,6 +3246,26 @@ def test_non_root_ui_path_logic(monkeypatch, tmp_path, ui_exists, ui_has_content
assert mock_logger.info.call_count == 0
@pytest.mark.parametrize("server_root_path", ["/foo", "/litellm", "/foo/litellm"])
def test_server_root_path_ui_rewrite_is_stable_across_restarts(tmp_path, server_root_path):
from litellm.proxy.proxy_server import _apply_server_root_path_to_ui
chunk = tmp_path / "_next" / "static" / "chunks" / "app.js"
chunk.parent.mkdir(parents=True)
chunk.write_text(
'fetch(`${g}/litellm/.well-known/litellm-ui-config`);const u={url:"/litellm/.well-known/litellm-ui-config"}',
encoding="utf-8",
)
for _ in range(3):
_apply_server_root_path_to_ui(str(tmp_path), server_root_path)
assert chunk.read_text(encoding="utf-8") == (
f"fetch(`${{g}}{server_root_path}/.well-known/litellm-ui-config`);"
f'const u={{url:"{server_root_path}/.well-known/litellm-ui-config"}}'
)
@pytest.mark.asyncio
async def test_get_config_callbacks_with_all_types(client_no_auth):
"""