1#!/usr/bin/env bash2# LLM API — install on a clean Apple Silicon Mac.3# Usage: ./scripts/install.sh [--root ~/llm-api] [--no-llama] [--no-web] [--test]4set -euo pipefail56ROOT="${LLM_API_ROOT:-$HOME/llm-api}"7WITH_LLAMA=18WITH_WEB=19RUN_TEST=010for a in "$@"; do11 case "$a" in12 --root) shift; ROOT="$1";;13 --root=*) ROOT="${a#--root=}";;14 --no-llama) WITH_LLAMA=0;;15 --no-web) WITH_WEB=0;;16 --test) RUN_TEST=1;;17 esac18done19HERE="$(cd "$(dirname "$0")/.." && pwd)"20say() { printf "\033[1;34m==>\033[0m %s\n" "$*"; }21warn() { printf "\033[1;33m!!\033[0m %s\n" "$*"; }2223# 1. platform checks -------------------------------------------------------24[ "$(uname -s)" = "Darwin" ] || { echo "macOS required"; exit 1; }25[ "$(uname -m)" = "arm64" ] || { echo "Apple Silicon (arm64) required"; exit 1; }26CHIP=$(sysctl -n machdep.cpu.brand_string)27MEM_GB=$(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 ))28FREE_GB=$(df -g "$HOME" | awk 'NR==2 {print $4}')29say "Detected $CHIP, ${MEM_GB} GB unified memory, ${FREE_GB} GB free on $HOME"30[ "$MEM_GB" -ge 16 ] || warn "Less than 16 GB of memory: only small models will run."31[ "$FREE_GB" -ge 120 ] || warn "Less than 120 GB free: downloads stop at the MIN_FREE_DISK_GB reserve."32export PATH="/opt/homebrew/bin:$HOME/.local/bin:$PATH"3334# 2. toolchain ---------------------------------------------------------------35xcode-select -p >/dev/null 2>&1 || { say "Installing Xcode Command Line Tools (a dialog may appear)"; xcode-select --install || true; }36command -v brew >/dev/null || { say "Installing Homebrew"; /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; eval "$(/opt/homebrew/bin/brew shellenv)"; }37command -v uv >/dev/null || { say "Installing uv"; brew install uv; }38if [ "$WITH_WEB" = 1 ]; then39 command -v node >/dev/null || { say "Installing Node"; brew install node; }40 command -v pnpm >/dev/null || { say "Installing pnpm"; brew install pnpm; }41fi42if [ "$WITH_LLAMA" = 1 ]; then43 command -v llama-server >/dev/null || { say "Installing llama.cpp (Metal)"; brew install llama.cpp; }44fi4546# 3. directories ---------------------------------------------------------------47say "Creating $ROOT"48mkdir -p "$ROOT"/{models/{mlx,gguf,embeddings,rerankers,vision,manifests},data,logs}49chmod 700 "$ROOT/data"5051# 4. python env -----------------------------------------------------------------52cd "$HERE/server"53if [ ! -x .venv/bin/python ]; then54 say "Creating Python 3.13 virtualenv"55 uv venv --python 3.13 .venv56fi57say "Installing Python dependencies (MLX, MLX-LM, mlx-vlm, FastAPI…)"58uv pip install --python .venv/bin/python -q -e ".[mlx,test]" "mlx-vlm>=0.7" asgi-lifespan59.venv/bin/python - <<'PY'60import mlx.core as mx, mlx_lm61print(f" mlx {mx.__version__} · mlx-lm {mlx_lm.__version__} · Metal device: {mx.default_device()}")62PY6364# 5. web ------------------------------------------------------------------------65if [ "$WITH_WEB" = 1 ]; then66 say "Installing and building the dashboard"67 (cd "$HERE/web" && pnpm install --frozen-lockfile --silent && NEXT_TELEMETRY_DISABLED=1 pnpm build | tail -3)68fi6970# 6. env file (never overwrite) ----------------------------------------------------71if [ ! -f "$ROOT/.env" ]; then72 say "Writing $ROOT/.env from .env.example (edit ADMIN_EMAIL / ADMIN_PASSWORD / HF_TOKEN)"73 sed "s#/Users/USERNAME/llm-api#$ROOT#" "$HERE/.env.example" > "$ROOT/.env"74 chmod 600 "$ROOT/.env"75else76 say "$ROOT/.env already exists — left untouched"77fi7879# 7. database + scan ------------------------------------------------------------------80say "Initializing database and scanning $ROOT/models"81(cd "$ROOT" && "$HERE/server/.venv/bin/llm-api" scan | tail -5)8283# 8. optional local inference test -------------------------------------------------------84if [ "$RUN_TEST" = 1 ]; then85 say "Downloading a tiny test model and running one generation"86 "$HERE/server/.venv/bin/hf" download mlx-community/Qwen3-0.6B-4bit --local-dir "$ROOT/models/mlx/qwen/Qwen3-0.6B-4bit" >/dev/null87 "$HERE/server/.venv/bin/python" -m mlx_lm generate --model "$ROOT/models/mlx/qwen/Qwen3-0.6B-4bit" --prompt "Say hello in one word." --max-tokens 8 | tail -388fi8990cat <<EOF9192Done. Next steps:93 1. Edit $ROOT/.env (ADMIN_EMAIL, ADMIN_PASSWORD, HF_TOKEN, PUBLIC_URL)94 2. Start the API: cd $ROOT && $HERE/server/.venv/bin/llm-api serve95 3. Start the dashboard: cd $HERE/web && pnpm start -p 8301 -H 127.0.0.196 4. Open http://127.0.0.1:8300 — or put a TLS reverse proxy in front for https://$(grep -E '^DOMAIN=' "$ROOT/.env" | cut -d= -f2)97 Process management: see docs/deployment.md (PM2 / launchd examples).98EOF99