From aeb333273d554c823214d3f6494db03fe0d1c113 Mon Sep 17 00:00:00 2001 From: shehryars715 Date: Mon, 5 Oct 2026 00:39:31 +0500 Subject: [PATCH] fix(proxy): stop prepending SERVER_ROOT_PATH to the UI config path on every restart The proxy rewrites the packaged UI files in place on each start. For a SERVER_ROOT_PATH ending in /litellm (e.g. /foo/litellm), the rewritten config path still contains the original one, so every restart prepended the root path again (/foo/foo/litellm/.well-known/litellm-ui-config), the request 404'd and the UI stayed on "Loading...". Leave already-rewritten paths alone so the rewrite is idempotent. Fixes #44517 --- litellm/proxy/proxy_server.py | 19 +++++++++++---- .../proxy/proxy_server/test_ui_config_path.py | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 tests/unit/proxy/proxy_server/test_ui_config_path.py diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 089647c6297..e1a522f0621 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2365,6 +2365,20 @@ 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 = 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) + ) + # 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. @@ -2480,10 +2494,7 @@ try: ) # 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", - ) + 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) diff --git a/tests/unit/proxy/proxy_server/test_ui_config_path.py b/tests/unit/proxy/proxy_server/test_ui_config_path.py new file mode 100644 index 00000000000..934f71ed97b --- /dev/null +++ b/tests/unit/proxy/proxy_server/test_ui_config_path.py @@ -0,0 +1,24 @@ +"""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"}}' + )