diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index e1a522f0621..2f9aab2ad10 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2379,6 +2379,44 @@ try: 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 /index.html. @@ -2464,43 +2502,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 = _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 + _apply_server_root_path_to_ui(ui_path, server_root_path) # # Mount the _next directory at the root level app.mount( diff --git a/tests/unit/proxy/proxy_server/test_ui_config_path.py b/tests/unit/proxy/proxy_server/test_ui_config_path.py deleted file mode 100644 index 934f71ed97b..00000000000 --- a/tests/unit/proxy/proxy_server/test_ui_config_path.py +++ /dev/null @@ -1,24 +0,0 @@ -"""SERVER_ROOT_PATH rewrite of the UI's config request (#44517).""" - -from __future__ import annotations - -import pytest - -from litellm.proxy.proxy_server import _rewrite_ui_config_path - -# The two shapes the config path takes in the packaged UI bundle -PACKAGED_UI_CHUNK = ( - 'fetch(`${g}/litellm/.well-known/litellm-ui-config`);const u={url:"/litellm/.well-known/litellm-ui-config"}' -) - - -@pytest.mark.parametrize("server_root_path", ["/foo", "/litellm", "/foo/litellm"]) -def test_restarts_keep_a_single_root_path_prefix(server_root_path: str) -> None: - rewritten = PACKAGED_UI_CHUNK - for _ in range(3): # the proxy rewrites the same UI files on every start - rewritten = _rewrite_ui_config_path(rewritten, server_root_path) - - assert rewritten == ( - f"fetch(`${{g}}{server_root_path}/.well-known/litellm-ui-config`);" - f'const u={{url:"{server_root_path}/.well-known/litellm-ui-config"}}' - ) diff --git a/tests/unit/proxy/test_proxy_server.py b/tests/unit/proxy/test_proxy_server.py index 79f4edd4d62..d9c257b6128 100644 --- a/tests/unit/proxy/test_proxy_server.py +++ b/tests/unit/proxy/test_proxy_server.py @@ -3020,6 +3020,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): """