* Add Apple Silicon (Metal) GPU detection and unified-memory fit tuning hardware.py detects Apple Silicon locally and over SSH, reporting backend=metal, the chip name, and a RAM-scaled fraction of unified memory as the usable GPU budget. fit.py gains an M1-M4 memory-bandwidth table for realistic tok/s and drops vLLM-only formats (AWQ/GPTQ/FP8) that can't be served on Metal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 32ac81dbc680361463a088dae867d555d5a79c3b) * Generate macOS/Metal serve commands and surface the Metal GPU cookbook_routes.py adds a macOS serve path (Ollama, Metal-aware llama.cpp build using `sysctl hw.ncpu` instead of `nproc`, and a clear error if vLLM is attempted). The frontend defaults Metal serving to llama.cpp and offers llama.cpp/Ollama instead of vLLM/SGLang. The odysseus-cookbook CLI's `gpus` command reports the Metal GPU via sysctl/vm_stat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 4ba01ce25d256ae032029898f361c824a34fcd4b) * Add launchd LaunchAgent for macOS (systemd equivalent) com.odysseus.ui.plist + install-service-macos.sh run Odysseus at login and restart on crash, the macOS counterpart to odysseus-ui.service. The installer auto-fills paths from the venv, so there's no hand-editing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 3d4b6b2c7b8b31af32201ed278115df9a559dea9) * Document macOS install (brew, Ollama, AirPlay port, launchd) README + setup.py cover the Homebrew / Apple Silicon path: brew install python@3.11 tmux ollama, Metal serving via Ollama/llama.cpp, the launchd service, and the macOS AirPlay Receiver conflict on ports 7000/5000. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 8dc9a3578a1726f070ed9f75c0958ae291a6d966) * Add downloadable macOS launcher app builder build-macos-app.sh generates dist/Odysseus.app and a drag-to-Applications dist/Odysseus.dmg. The app starts the local server from this repo's venv and opens the UI in a chrome-less app window (Chromium --app mode, falling back to the default browser). It's a launcher wrapper — it drives the venv rather than bundling Python — so the install path is baked in at build time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 7927940c3810ee34640803b198d334a6ac93474d) * Harden macOS Cookbook support: hide MLX, fix Metal build cache Builds on the adopted PR #213 macOS/Metal work with two fixes and tests: - fit.py: always drop MLX-quantized models. Odysseus only generates serve commands for llama.cpp/Ollama (Metal) and vLLM/SGLang (CUDA); MLX needs the mlx_lm runtime and the catalog's MLX repos ship no GGUF alternative, so they were surfaced on Apple Silicon but could never be served. - cookbook_routes.py (macOS branch only): `rm -rf build` before configure so a poisoned CMakeCache from a prior failed CUDA attempt can't make every later build fail; explicit -DCMAKE_BUILD_TYPE=Release; a clear "brew install cmake" hint if cmake is missing. Linux/CUDA path unchanged. - tests/test_hwfit_macos.py: MLX hidden on metal, MLX still hidden on CUDA (regression guard), Metal detection on Apple Silicon, and skipped on Linux/Intel (proves non-macOS detection is untouched). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Propagate unified_memory flag and document macOS GPU/Docker caveat - hardware.py: detect_system now carries the unified_memory flag from GPU detection into the system dict (it was set by _detect_apple_silicon / AMD-APU detection but dropped during result assembly, so the API always reported null). Lets callers distinguish unified from discrete VRAM. - README: prominent warning that Docker on Apple Silicon can't reach the Metal GPU (runs a Linux VM) — Cookbook must run natively for GPU serving; fix stale text that said Cookbook recommends MLX models (now hidden as unservable). - test: detect_system propagates unified_memory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Put Odysseus's venv bin on PATH for cookbook runners Native (non-Docker) installs run from a virtualenv whose bin holds the `hf` CLI and `python3` the cookbook download/serve tmux scripts shell out to. Those scripts start in a fresh login shell with the venv NOT activated, so on a native macOS install `hf download` failed with "hf: command not found" — and the `pip --user` self-heal missed because macOS has no bare `pip` command. - cookbook_helpers.py: _local_tooling_path_export() — pure helper returning a PATH export for the running interpreter's bin dir (escaped for double quotes). - cookbook_routes.py: download + serve runners prepend that dir on local runs (gated off SSH/Windows); swap the `pip` install fallbacks to `python3 -m pip`. - tests: helper output for normal and spaced paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Document macOS llama.cpp serving prerequisites Clarify the two serving paths on Apple Silicon: the recommended zero-build route (brew install llama.cpp ships a Metal llama-server Cookbook finds on PATH), and the from-source fallback, which requires cmake + Xcode Command Line Tools. Without those the build is skipped and serving silently degrades to a slow CPU build, so new users now know to install them (or use the prebuilt) up front. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Recommend only GGUF-servable models on Metal Apple Silicon's only serving engines are llama.cpp and Ollama, both GGUF-only (vLLM/SGLang are CUDA/ROCm and don't run on macOS). The catalog tags raw safetensors repos with a default Q4_K_M quant, so the fit-ranking was recommending ~397/501 models that have no GGUF and fail to serve on Metal with "No GGUF found" (e.g. microsoft/Phi-mini-MoE-instruct). Drop any model without a real GGUF (is_gguf/gguf_sources) on Apple Silicon — subsumes the previous AWQ/GPTQ/FP8 special-case into one rule. On CUDA these stay visible since vLLM serves safetensors directly. Metal recommendations go 501 -> 104, all actually servable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Remove macOS launchd LaunchAgent (cherry-picked extra) Drop the launchd service from the PR #213 cherry-picks: the install-service-macos.sh installer, the com.odysseus.ui.plist template, and the README section documenting them. Tangential to the core Cookbook/Metal support and not wanted. The build-macos-app.sh launcher is kept. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add one-command macOS quick start (start-macos.sh) Running Odysseus natively on a Mac previously meant ~7 manual terminal steps (brew deps, venv, activate, pip, setup.py, uvicorn with the right port) — not friendly for a generic macOS user, and the native run is required because Docker on macOS can't reach the Metal GPU. - start-macos.sh: installs Homebrew deps (python@3.11, tmux, prebuilt Metal llama.cpp), creates the venv, installs requirements, runs setup, and launches on a non-AirPlay port (7860). Idempotent; re-run to start again. - README: the Apple Silicon section now leads with this one-command quick start and the clickable .app, with engine/port/manual details folded into a collapsible block. Added a pointer at the top of the manual-install section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * macOS quick start: auto-open browser when ready The "open this URL" line scrolled out of view as uvicorn kept logging after it, so users missed it. Now start-macos.sh waits (in the background) until the server accepts connections, prints a boxed "ready" banner at that point (i.e. after the startup burst, not before), and opens the URL in the default browser automatically. Skippable with ODYSSEUS_NO_OPEN=1 for headless/SSH use. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Don't assume/force a specific Python version on macOS The README claimed "system Python is 3.9" — a machine-specific generalization that's often wrong (macOS ships no recent Python by default; many users already have 3.11+). Make it generic, and make start-macos.sh detect an existing Python 3.11+ and use it, only installing python@3.11 when none is found instead of forcing it on top of the user's Python. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Align start-macos.sh venv path with build-macos-app.sh start-macos.sh created the environment in .venv/, but build-macos-app.sh and the manual install steps use venv/ — so the clickable .app wouldn't reuse the quick-start's environment and would rebuild a second one. Use venv/ everywhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * README: state clearly that MLX is unsupported on Apple Silicon Odysseus has no mlx_lm runtime; it serves GGUF (llama.cpp/Ollama) and CUDA (vLLM/SGLang) only. MLX-only models can't run on a Mac and are hidden from Cookbook — make that explicit in both the quick start and the details. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * start-macos.sh: build the venv with an arm64 Python on Apple Silicon A clean-room run surfaced this: with a universal2/x86 Python (e.g. the python.org installer under /usr/local), the venv's compiled extensions install as arm64 but get loaded as x86_64 when launched from the .app bundle, so it crashes with "incompatible architecture (have arm64, need x86_64)". The terminal run happened to work only because a universal binary defaults to arm64 there. On Apple Silicon, look only under /opt/homebrew (arm64-only) for the build Python, and install Homebrew's python@3.11 if none is present — so the venv is arm64-only and launches correctly from both the terminal and the .app. Intel and non-mac paths are unchanged. Verified end-to-end in a clean clone: .app now boots on Metal with no arch error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address dev-exp review: macOS setup robustness + doc/UX fixes From the voltagent dev-exp review of the branch: - README: fix broken anchor links (the em-dash heading produced a slug the links didn't match); simplify the heading to a stable slug. - cookbook_routes.py: add /opt/homebrew/bin and /usr/local/bin to the serve PATH so a brew-installed llama-server/ollama is found instead of falling back to a slow source build. - start-macos.sh: guard against an empty Python path; fail fast with a clear message on port-in-use; ERR trap with a "safe to re-run" message; show pip progress (drop --quiet on the slow requirements install); stop the background browser-opener cleanly on exit/Ctrl+C (no orphaned poller). - setup.py: bind hint to 127.0.0.1; suppress the manual run-hint when launched by start-macos.sh (ODYSSEUS_SKIP_RUN_HINT) so the URL isn't contradictory. - build-macos-app.sh: the .app only opens the browser once the server is actually ready (not after the readiness timeout). - cookbookServe.js: drop "Diffusers" from the Metal backend picker — diffusion_server.py is CUDA-only, so it was an unservable option on macOS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: yunggilja <yunggilja@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
140 lines
5.9 KiB
Bash
Executable File
140 lines
5.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# Odysseus — one-command quick start for macOS (Apple Silicon).
|
|
#
|
|
# ./start-macos.sh
|
|
#
|
|
# Installs everything Odysseus needs via Homebrew, sets up a local Python
|
|
# environment, and launches the app — so a generic Mac user can run it without
|
|
# knowing anything about venvs, pip, or uvicorn. Safe to re-run; it skips work
|
|
# that's already done.
|
|
#
|
|
# Why native (not Docker): Cookbook serves models on whatever machine Odysseus
|
|
# runs on, and Docker on macOS is a Linux VM with no access to the Metal GPU.
|
|
# Running natively lets Cookbook detect and use your Mac's GPU.
|
|
set -e
|
|
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$REPO_DIR"
|
|
|
|
PORT="${ODYSSEUS_PORT:-7860}" # 7860, not 7000 — macOS AirPlay Receiver holds 7000.
|
|
|
|
# Friendly message on any failure — re-running is safe (every step is idempotent).
|
|
trap 'echo; echo "✗ Setup failed above. It is safe to re-run ./start-macos.sh."; exit 1' ERR
|
|
|
|
echo "▶ Odysseus quick start for macOS"
|
|
|
|
# Fail fast if the port is already taken (e.g. a previous run still running).
|
|
if (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then
|
|
echo "✗ Port $PORT is already in use. Stop what's using it, or pick another port:"
|
|
echo " ODYSSEUS_PORT=7900 ./start-macos.sh"
|
|
exit 1
|
|
fi
|
|
|
|
# 1. Homebrew — the macOS package manager. We can't safely auto-install it
|
|
# (it wants its own interactive confirmation), so point the user at it.
|
|
if ! command -v brew >/dev/null 2>&1; then
|
|
echo
|
|
echo "Homebrew is required but not installed. Install it (one command), then re-run this script:"
|
|
echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
|
echo
|
|
echo "More info: https://brew.sh"
|
|
exit 1
|
|
fi
|
|
|
|
# 2. Find a Python 3.11+ to build the environment with.
|
|
# On Apple Silicon we require an *arm64* interpreter (Homebrew's, under
|
|
# /opt/homebrew). A universal2 or x86 Python — e.g. the python.org installer
|
|
# at /usr/local — produces a venv whose compiled extensions get loaded as the
|
|
# wrong architecture when launched from the .app bundle (Cookbook then dies
|
|
# with "incompatible architecture"). So on arm64 we only look under
|
|
# /opt/homebrew and install Homebrew's python@3.11 if it's missing. On Intel
|
|
# (or non-mac) we just use whatever Python 3.11+ is on PATH.
|
|
PY=""
|
|
if [ "$(uname -m)" = "arm64" ]; then
|
|
cands="/opt/homebrew/bin/python3.13 /opt/homebrew/bin/python3.12 /opt/homebrew/bin/python3.11"
|
|
else
|
|
cands="python3 python3.13 python3.12 python3.11"
|
|
fi
|
|
for cand in $cands; do
|
|
p="$(command -v "$cand" 2>/dev/null)" || continue
|
|
if "$p" -c 'import sys; raise SystemExit(0 if sys.version_info[:2] >= (3, 11) else 1)' 2>/dev/null; then
|
|
PY="$p"; break
|
|
fi
|
|
done
|
|
|
|
# System dependencies:
|
|
# - tmux : Cookbook runs model downloads/serves in the background
|
|
# - llama.cpp : a prebuilt, Metal-enabled llama-server so Cookbook can serve
|
|
# GGUF models on the GPU with no compile step
|
|
# - python@3.11 : installed only if no suitable (arm64) Python was found above
|
|
echo "▶ Installing dependencies (Homebrew)…"
|
|
if [ -n "$PY" ]; then
|
|
echo " (using $("$PY" --version 2>&1) at $PY)"
|
|
brew install tmux llama.cpp
|
|
else
|
|
brew install python@3.11 tmux llama.cpp
|
|
PY="$(command -v /opt/homebrew/bin/python3.11 || command -v python3.11 || true)"
|
|
fi
|
|
|
|
if [ -z "$PY" ] || [ ! -x "$PY" ]; then
|
|
echo "✗ Couldn't find a Python 3.11+ to build the environment with."
|
|
echo " Check: ls /opt/homebrew/bin/python3* (or install one: brew install python@3.11)"
|
|
exit 1
|
|
fi
|
|
|
|
# 3. Python environment + dependencies (kept inside the repo, in venv/).
|
|
# Named `venv` to match the manual steps and build-macos-app.sh, so the
|
|
# clickable .app reuses this same environment.
|
|
if [ ! -d venv ]; then
|
|
echo "▶ Creating Python environment…"
|
|
"$PY" -m venv venv
|
|
fi
|
|
echo "▶ Installing Python packages (first run downloads a few — can take a few minutes)…"
|
|
./venv/bin/python -m pip install --quiet --upgrade pip
|
|
# Not --quiet: this is the slow step, so show progress (and any real errors).
|
|
./venv/bin/python -m pip install -r requirements.txt
|
|
|
|
# 4. First-run setup: creates data dirs and prints an initial admin password
|
|
# the first time (idempotent — does nothing if already set up). Suppress its
|
|
# manual run hint — we launch the server ourselves just below.
|
|
echo "▶ Preparing Odysseus…"
|
|
ODYSSEUS_SKIP_RUN_HINT=1 ./venv/bin/python setup.py
|
|
|
|
# 5. Launch. Bind to loopback only (safe default).
|
|
URL="http://127.0.0.1:$PORT"
|
|
|
|
# Open the browser automatically once the server is accepting connections — so
|
|
# the URL isn't lost in the startup logs that keep scrolling. Runs in the
|
|
# background and is cleaned up when the server stops. Skip with
|
|
# ODYSSEUS_NO_OPEN=1 (e.g. over SSH / headless).
|
|
POLLER_PID=""
|
|
if [ -z "$ODYSSEUS_NO_OPEN" ] && command -v open >/dev/null 2>&1; then
|
|
(
|
|
for _ in $(seq 1 90); do
|
|
if (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then
|
|
printf '\n'
|
|
printf ' ┌────────────────────────────────────────────┐\n'
|
|
printf ' │ ✓ Odysseus is ready — opening your browser │\n'
|
|
printf ' │ %-40s │\n' "$URL"
|
|
printf ' │ (Press Ctrl+C in this window to stop) │\n'
|
|
printf ' └────────────────────────────────────────────┘\n\n'
|
|
open "$URL"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
) &
|
|
POLLER_PID=$!
|
|
fi
|
|
|
|
# Setup is done — drop the setup-failure handler, and clean up the background
|
|
# opener when the server exits or the user presses Ctrl+C.
|
|
trap - ERR
|
|
trap '[ -n "$POLLER_PID" ] && kill "$POLLER_PID" 2>/dev/null' EXIT INT TERM
|
|
|
|
echo
|
|
echo "▶ Starting Odysseus — it will open in your browser at $URL"
|
|
echo " (this takes a few seconds; press Ctrl+C here to stop)"
|
|
echo
|
|
./venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port "$PORT"
|