From 52562f018eded31eb3f29361b7ec84d7f07e8b1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 21:57:47 +0000 Subject: [PATCH] build(ui): build Admin UI from source in every Docker image and on PyPI publish The Next.js bundle under litellm/proxy/_experimental/out has historically been checked into the repository so the proxy could ship a pre-built dashboard. This bloated the repo (~21 MB of generated artifacts) and made every UI PR unreviewable because npm-rebuilt hashed asset filenames showed up as massive diffs. Wire every code path that produces a distributable artifact to rebuild the UI from source instead: - docker/build_admin_ui.sh now always builds the UI (no more enterprise-only early exit), copies it to litellm/proxy/_experimental/out, and restructures HTML so extensionless /ui routes work. - docker/Dockerfile.alpine and docker/Dockerfile.non_root now invoke build_admin_ui.sh (the other Dockerfiles already did). non_root drops its bespoke restructure loop since the build script handles it. - .github/workflows/publish_to_pypi.yml builds the UI before 'uv build' so the published wheel/sdist still ships the dashboard. - ui/litellm-dashboard/build_ui.sh creates the destination dir if missing and restructures HTML routes the same way the Docker pipeline does. - ui/litellm-dashboard/build_release_ui.sh no longer commits artifacts to git; it just delegates to docker/build_admin_ui.sh. Co-authored-by: Mateo Wang --- .github/workflows/publish_to_pypi.yml | 15 ++++ docker/Dockerfile.alpine | 3 + docker/Dockerfile.non_root | 26 +++--- docker/build_admin_ui.sh | 100 +++++++++-------------- ui/litellm-dashboard/build_release_ui.sh | 28 +++---- ui/litellm-dashboard/build_ui.sh | 16 ++-- 6 files changed, 89 insertions(+), 99 deletions(-) diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml index d60254a0ac5..5520989cefb 100644 --- a/.github/workflows/publish_to_pypi.yml +++ b/.github/workflows/publish_to_pypi.yml @@ -121,6 +121,21 @@ jobs: version: "0.10.9" enable-cache: false + - name: Set up Node.js for Admin UI build + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: ui/litellm-dashboard/package-lock.json + + - name: Build Admin UI + # The Next.js bundle under litellm/proxy/_experimental/out is no longer + # checked into the repository; build it here so it gets packaged into + # the published wheel/sdist. + run: | + chmod +x docker/build_admin_ui.sh + ./docker/build_admin_ui.sh + - name: Copy model prices backup run: cp model_prices_and_context_window.json litellm/model_prices_and_context_window_backup.json diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 5de588cf4e4..69caa44033a 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -38,6 +38,9 @@ RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-gr # Copy full source tree COPY . . +# Build Admin UI before final sync +RUN sed -i 's/\r$//' docker/build_admin_ui.sh && chmod +x docker/build_admin_ui.sh && ./docker/build_admin_ui.sh + # Install project and workspace packages (fast - deps already cached) RUN uv sync --frozen --no-default-groups --no-editable \ --extra proxy \ diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index ab40ee138e3..a14664e48d3 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -24,7 +24,8 @@ RUN for i in 1 2 3; do \ curl \ openssl \ libsndfile \ - nodejs && break || sleep 5; \ + nodejs \ + npm && break || sleep 5; \ done ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ @@ -55,22 +56,19 @@ 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. +# Build the Admin UI from source. The pre-built Next.js bundle is no longer +# checked into the repository — build_admin_ui.sh produces it and writes to +# litellm/proxy/_experimental/out (already restructured for /ui routes). +RUN sed -i 's/\r$//' docker/build_admin_ui.sh && chmod +x docker/build_admin_ui.sh && \ + ./docker/build_admin_ui.sh + +# Stage the freshly built Admin UI for the runtime stage. The build script +# already restructured extensionless routes (foo.html -> foo/index.html) to +# match the layout proxy_server.py expects; we just 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/docker/build_admin_ui.sh b/docker/build_admin_ui.sh index efb2bac3535..29c0960b49e 100755 --- a/docker/build_admin_ui.sh +++ b/docker/build_admin_ui.sh @@ -1,73 +1,49 @@ #!/bin/bash -# # try except this script -# set -e +# Builds the Admin UI Next.js bundle and publishes it to +# litellm/proxy/_experimental/out so the proxy can serve it. +# +# This script is invoked from every UI-bearing Dockerfile and from the +# release pipeline. The pre-built bundle is no longer checked into the +# repository, so this script must run successfully whenever the +# resulting Docker image / Python wheel needs to ship the dashboard. + +set -e -# print current dir -echo pwd - -# only run this step for litellm enterprise, we run this if enterprise/enterprise_ui/_enterprise.json exists -if [ ! -f "enterprise/enterprise_ui/enterprise_colors.json" ]; then - echo "Admin UI - using default LiteLLM UI" - exit 0 +# Apply the enterprise color palette when present so OSS and enterprise +# images can share this build script. +if [ -f "enterprise/enterprise_ui/enterprise_colors.json" ]; then + echo "Using enterprise UI color palette" + cp enterprise/enterprise_ui/enterprise_colors.json ui/litellm-dashboard/ui_colors.json +else + echo "Using default LiteLLM UI color palette" fi -echo "Building Custom Admin UI..." - -# Install dependencies -# Check if we are on macOS -if [[ "$(uname)" == "Darwin" ]]; then - # Install dependencies using Homebrew - if ! command -v brew &> /dev/null; then - echo "Error: Homebrew not found. Please install Homebrew and try again." - exit 1 - fi - brew update - brew install curl -else - # Assume Linux, try using apt-get - if command -v apt-get &> /dev/null; then - apt-get update - apt-get install -y curl - elif command -v apk &> /dev/null; then - # Try using apk if apt-get is not available - apk update - apk add curl - else - echo "Error: Unsupported package manager. Cannot install dependencies." - exit 1 - fi -fi -NVM_VERSION="v0.40.4" -NVM_CHECKSUM="4b7412c49960c7d31e8df72da90c1fb5b8cccb419ac99537b737028d497aba4f" -NVM_SCRIPT=$(mktemp) -trap 'rm -f "$NVM_SCRIPT"' EXIT -curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" -o "$NVM_SCRIPT" -if command -v sha256sum &>/dev/null; then - echo "${NVM_CHECKSUM} ${NVM_SCRIPT}" | sha256sum -c - -elif command -v shasum &>/dev/null; then - echo "${NVM_CHECKSUM} ${NVM_SCRIPT}" | shasum -a 256 -c - -else - echo "No sha256 tool found; cannot verify nvm checksum"; exit 1 -fi || { echo "nvm checksum verification failed"; exit 1; } -bash "$NVM_SCRIPT" -source ~/.nvm/nvm.sh -nvm install v18.17.0 -nvm use v18.17.0 - -# copy _enterprise.json from this directory to /ui/litellm-dashboard, and rename it to ui_colors.json -cp enterprise/enterprise_ui/enterprise_colors.json ui/litellm-dashboard/ui_colors.json - -# cd in to /ui/litellm-dashboard cd ui/litellm-dashboard -# ensure have access to build_ui.sh -chmod +x ./build_ui.sh +# Use a deterministic install when a lockfile is present. +if [ -f package-lock.json ]; then + npm ci +else + npm install +fi -# run ./build_ui.sh -./build_ui.sh +npm run build -# return to root directory -cd ../.. \ No newline at end of file +destination_dir="../../litellm/proxy/_experimental/out" +mkdir -p "$destination_dir" +rm -rf "$destination_dir"/* +cp -r ./out/. "$destination_dir"/ +rm -rf ./out + +# Restructure HTML so extensionless routes work (e.g. /ui/login). +# Next.js export produces login.html; the proxy expects login/index.html. +find "$destination_dir" -name '*.html' ! -name 'index.html' | while read -r htmlfile; do + target_dir="${htmlfile%.html}" + mkdir -p "$target_dir" + mv "$htmlfile" "$target_dir/index.html" +done + +cd ../.. diff --git a/ui/litellm-dashboard/build_release_ui.sh b/ui/litellm-dashboard/build_release_ui.sh index 4f1168502c6..fa612a34f7f 100755 --- a/ui/litellm-dashboard/build_release_ui.sh +++ b/ui/litellm-dashboard/build_release_ui.sh @@ -1,22 +1,14 @@ #!/bin/bash set -e -destination_dir="../../litellm/proxy/_experimental/out" +# This script used to rebuild the UI and commit the resulting Next.js +# bundle into git so the proxy could ship a pre-built dashboard. +# +# The bundle is no longer checked into the repository — it is rebuilt +# from source by the Dockerfiles and by the PyPI publish workflow. This +# script now simply triggers a local build (the canonical entry point is +# `docker/build_admin_ui.sh`) for developers who want to preview the +# production-flavoured UI without booting a Docker image. -chmod +x ./build_ui.sh -./build_ui.sh - -commit_message="chore: update Next.js build artifacts ($(date -u +"%Y-%m-%d %H:%M UTC"), node $(node -v))" - -if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then - git add -f "$destination_dir"/ - - if ! git diff --cached --quiet; then - git commit -m "$commit_message" - echo "Git commit created." - else - echo "No changes to commit." - fi -else - echo "Not a git repository. Skipping commit." -fi +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +exec "$repo_root/docker/build_admin_ui.sh" diff --git a/ui/litellm-dashboard/build_ui.sh b/ui/litellm-dashboard/build_ui.sh index aa346c12edc..257ce17e7a0 100755 --- a/ui/litellm-dashboard/build_ui.sh +++ b/ui/litellm-dashboard/build_ui.sh @@ -42,18 +42,24 @@ npm run build if [ $? -eq 0 ]; then echo "Build successful. Copying files..." - # echo current dir echo pwd - # Specify the destination directory destination_dir="../../litellm/proxy/_experimental/out" - # Remove existing files in the destination directory + # The pre-built bundle is no longer checked into the repository, so the + # destination may not exist on a fresh checkout. Create it before copying. + mkdir -p "$destination_dir" rm -rf "$destination_dir"/* - # Copy the contents of the output directory to the specified destination - cp -r ./out/* "$destination_dir" + cp -r ./out/. "$destination_dir"/ + + # Restructure HTML so extensionless routes work (login.html -> login/index.html) + find "$destination_dir" -name '*.html' ! -name 'index.html' | while read -r htmlfile; do + target_dir="${htmlfile%.html}" + mkdir -p "$target_dir" + mv "$htmlfile" "$target_dir/index.html" + done rm -rf ./out