fix(ui): fix /ui/chat 404 — restructure heuristic false-positive + pre-restructure in Dockerfile

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.
This commit is contained in:
Asseel Naji 2026-03-20 23:29:15 +03:00
parent 50f88c8642
commit 3049a3e43c
2 changed files with 34 additions and 1 deletions

View file

@ -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

View file

@ -1203,7 +1203,27 @@ try:
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"
)