diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index 2729babb6d..8717e5b3fc 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -55,22 +55,10 @@ COPY . . # Set non-root flag for build time consistency ENV LITELLM_NON_ROOT=true -# Stage the pre-built Admin UI from the checked-in Next.js static export. -# _experimental/out/ is regenerated as part of the release runbook. -# Restructure extensionless routes (foo.html -> foo/index.html) to match the layout -# proxy_server.py expects, and drop a readiness marker. RUN mkdir -p /var/lib/litellm/ui /var/lib/litellm/assets && \ cp -r /app/litellm/proxy/_experimental/out/. /var/lib/litellm/ui/ && \ cp /app/litellm/proxy/logo.jpg /var/lib/litellm/assets/logo.jpg && \ - ( cd /var/lib/litellm/ui && \ - for html_file in *.html; do \ - if [ "$html_file" != "index.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 ) + touch /var/lib/litellm/ui/.litellm_ui_ready RUN --mount=type=cache,target=/app/.cache/uv,id=litellm-uv-cache \ if [ "$PROXY_EXTRAS_SOURCE" = "published" ]; then \ diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index ae0996d16d..ba9c3b75ba 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -604,6 +604,49 @@ def test_ui_extensionless_route_requires_restructure(tmp_path): assert "login" in response.text +def test_admin_ui_export_serves_nested_extensionless_routes(): + out_dir = ( + Path(litellm.__file__).parent / "proxy" / "_experimental" / "out" + ) + assert out_dir.is_dir(), f"missing UI export at {out_dir}" + + nested_html_offenders = [ + path.relative_to(out_dir).as_posix() + for path in out_dir.rglob("*.html") + if path.parent != out_dir + and path.name != "index.html" + and "_next" not in path.parts + and "litellm-asset-prefix" not in path.parts + ] + assert not nested_html_offenders, ( + "Nested routes must be named index.html. Offenders: " + f"{nested_html_offenders}" + ) + + callback_index = out_dir / "mcp" / "oauth" / "callback" / "index.html" + assert callback_index.is_file(), ( + f"MCP OAuth callback page must exist at {callback_index}; " + "without it /ui/mcp/oauth/callback 404s after Linear redirects back." + ) + + fastapi_app = FastAPI() + fastapi_app.mount( + "/ui", StaticFiles(directory=str(out_dir), html=True), name="ui" + ) + client = TestClient(fastapi_app) + + redirect = client.get( + "/ui/mcp/oauth/callback?code=abc&state=xyz", + follow_redirects=False, + ) + assert redirect.status_code == 307 + assert redirect.headers["location"].endswith("/ui/mcp/oauth/callback/?code=abc&state=xyz") + + landed = client.get("/ui/mcp/oauth/callback?code=abc&state=xyz") + assert landed.status_code == 200 + assert "