chore(admin-ui): regenerate static export with trailingSlash: true (#28112)
* chore(admin-ui): regenerate static export with trailingSlash: true Rebuilds litellm/proxy/_experimental/out/ from ui/litellm-dashboard with `trailingSlash: true` enabled in next.config.mjs. Next.js now emits every route as <dir>/index.html (e.g. mcp/oauth/callback/index.html) instead of <dir>.html with a sibling metadata-only directory, which fixes the 404 on extensionless URLs served through FastAPI's StaticFiles(html=True) mount. This is the build artifact half of the fix; the config change, Dockerfile cleanup, and regression test live in the follow-up source PR that stacks on top of this branch. * fix(admin-ui): emit nested routes as <dir>/index.html (#28106) Linear and other OAuth providers redirect the user back to /ui/mcp/oauth/callback?code=...&state=... after the consent step. The packaged Next.js static export only produced /ui/mcp/oauth/callback.html, so FastAPI's StaticFiles served a 404 on the extensionless URL and the OAuth handshake never completed. The Dockerfile.non_root build step tried to paper over this at image-build time with `for html_file in *.html; do ...`, but that shell glob does not recurse, so nested routes like mcp/oauth/callback.html were left stranded next to an empty mcp/oauth/callback/ directory containing only Next.js metadata. The runtime restructure step in proxy_server.py was then skipped because the .litellm_ui_ready marker had already been dropped. Set trailingSlash: true in the dashboard's Next.js config so the export emits every nested route as <dir>/index.html natively. The Dockerfile loop is now a no-op for the bundled UI and has been removed; the .litellm_ui_ready marker is still written so the proxy keeps skipping the redundant Python restructure step at startup. Stacks on top of the static export regeneration in the parent branch. * chore: restore origin/litellm_internal_staging out files
This commit is contained in:
parent
c23b19f09c
commit
48d7e15b83
@ -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 \
|
||||
|
||||
@ -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 "<html" in landed.text.lower()
|
||||
|
||||
|
||||
def test_restructure_always_happens(monkeypatch):
|
||||
"""
|
||||
Test that restructuring logic always executes regardless of LITELLM_NON_ROOT setting.
|
||||
|
||||
@ -14,6 +14,7 @@ const nextConfig = {
|
||||
},
|
||||
basePath: "",
|
||||
assetPrefix: "/litellm-asset-prefix",
|
||||
trailingSlash: true,
|
||||
turbopack: {
|
||||
// Must be absolute; "." is no longer allowed
|
||||
root: __dirname,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user