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 <mateo-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-05-04 21:57:47 +00:00
parent c38e23e6d3
commit 52562f018e
No known key found for this signature in database
6 changed files with 89 additions and 99 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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