From 3049a3e43c312d8eb5dc1e45f5a817fed4511658 Mon Sep 17 00:00:00 2001 From: Asseel Naji Date: Fri, 20 Mar 2026 23:29:15 +0300 Subject: [PATCH] =?UTF-8?q?fix(ui):=20fix=20/ui/chat=20404=20=E2=80=94=20r?= =?UTF-8?q?estructure=20heuristic=20false-positive=20+=20pre-restructure?= =?UTF-8?q?=20in=20Dockerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Starlette StaticFiles(html=True) serves extensionless routes (e.g. /ui/chat) by looking for {route}/index.html. Next.js static export generates login/index.html for older pages but chat.html (root-level) for the new Chat UI page added in #22937. The _is_ui_pre_restructured() fallback heuristic returned True as soon as it found any {dir}/index.html (e.g. login/index.html), skipping the restructure step and leaving chat.html in place — causing /ui/chat to 404. Fix 1: proxy_server.py — tighten the heuristic to also scan for orphaned root-level .html files; return False if any exist so restructuring runs. Fix 2: Dockerfile — add pre-restructure step (matching Dockerfile.non_root) so the main published image pre-moves all {page}.html → {page}/index.html and writes the .litellm_ui_ready marker, bypassing the heuristic entirely on subsequent starts. --- Dockerfile | 13 +++++++++++++ litellm/proxy/proxy_server.py | 22 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7bda32acf27..28a157b3333 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,19 @@ COPY . . # Convert Windows line endings to Unix and make executable RUN sed -i 's/\r$//' docker/build_admin_ui.sh && chmod +x docker/build_admin_ui.sh && ./docker/build_admin_ui.sh +# Pre-restructure UI: move root-level {page}.html → {page}/index.html so +# Starlette StaticFiles can serve extensionless routes (e.g. /ui/chat). +# Must run before building the wheel since the out/ dir is included in the package. +RUN cd litellm/proxy/_experimental/out && \ + for html_file in *.html; do \ + if [ "$html_file" != "index.html" ] && [ "$html_file" != "404.html" ] && [ -f "$html_file" ]; then \ + folder_name="${html_file%.html}" && \ + mkdir -p "$folder_name" && \ + mv "$html_file" "$folder_name/index.html"; \ + fi; \ + done && \ + touch .litellm_ui_ready + # Build the package RUN rm -rf dist/* && python -m build diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9c29927c5cb..723a2032610 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1203,7 +1203,27 @@ def _is_ui_pre_restructured(ui_dir: str) -> bool: if entry.is_dir() and not entry.name.startswith("_"): index_path = os.path.join(entry.path, "index.html") if os.path.exists(index_path): - # Found at least one restructured route - this proves the pattern + # Found at least one restructured route. + # Also verify no root-level .html files still need restructuring. + # Next.js static export may generate both pre-restructured pages + # (e.g. login/index.html) and new pages as root-level .html files + # (e.g. chat.html), causing the heuristic to fire prematurely. + try: + orphaned = [ + e.name + for e in os.scandir(ui_dir) + if e.is_file() + and e.name.endswith(".html") + and e.name not in ("index.html", "404.html") + ] + except (PermissionError, OSError): + orphaned = [] + if orphaned: + verbose_proxy_logger.debug( + f"Found un-restructured HTML files at root: {orphaned}. " + f"Restructuring needed despite existing {entry.name}/index.html." + ) + return False verbose_proxy_logger.debug( f"Detected restructured UI via pattern: found {entry.name}/index.html" )