2026-05-31 23:58:26 +09:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Odysseus — first-time setup script.
|
|
|
|
|
|
|
|
|
|
Creates data directories, initializes the database, and sets up an
|
|
|
|
|
initial admin user. Safe to re-run (skips what already exists).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
2026-06-07 19:28:37 +03:00
|
|
|
import platform
|
2026-05-31 23:58:26 +09:00
|
|
|
import shutil
|
2026-06-07 19:28:37 +03:00
|
|
|
import subprocess
|
2026-05-31 23:58:26 +09:00
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
2026-06-08 09:58:52 +02:00
|
|
|
sys.path.insert(0, BASE_DIR)
|
|
|
|
|
from src.constants import (
|
|
|
|
|
DATA_DIR, AUTH_FILE, UPLOAD_DIR, PERSONAL_DIR, PERSONAL_UPLOADS_DIR,
|
|
|
|
|
TTS_CACHE_DIR, GENERATED_IMAGES_DIR, DEEP_RESEARCH_DIR, CHROMA_DIR,
|
2026-06-16 02:52:15 -05:00
|
|
|
RAG_DIR, MEMORY_VECTORS_DIR, PASSWORD_MIN_LENGTH,
|
2026-06-08 09:58:52 +02:00
|
|
|
)
|
2026-06-16 02:52:15 -05:00
|
|
|
from core.auth import RESERVED_USERNAMES
|
2026-05-31 23:58:26 +09:00
|
|
|
|
|
|
|
|
DIRS = [
|
|
|
|
|
DATA_DIR,
|
2026-06-08 09:58:52 +02:00
|
|
|
UPLOAD_DIR,
|
|
|
|
|
PERSONAL_DIR,
|
|
|
|
|
PERSONAL_UPLOADS_DIR,
|
|
|
|
|
TTS_CACHE_DIR,
|
|
|
|
|
GENERATED_IMAGES_DIR,
|
|
|
|
|
DEEP_RESEARCH_DIR,
|
|
|
|
|
CHROMA_DIR,
|
|
|
|
|
RAG_DIR,
|
|
|
|
|
MEMORY_VECTORS_DIR,
|
2026-05-31 23:58:26 +09:00
|
|
|
os.path.join(BASE_DIR, "logs"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_dirs():
|
|
|
|
|
for d in DIRS:
|
|
|
|
|
os.makedirs(d, exist_ok=True)
|
|
|
|
|
print(f" [ok] {os.path.relpath(d, BASE_DIR)}/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_database():
|
|
|
|
|
"""Create all SQLAlchemy tables."""
|
|
|
|
|
sys.path.insert(0, BASE_DIR)
|
|
|
|
|
os.environ.setdefault("DATABASE_URL", f"sqlite:///{os.path.join(DATA_DIR, 'app.db')}")
|
|
|
|
|
|
|
|
|
|
from core.database import Base, engine
|
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
|
print(" [ok] Database initialized")
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 21:14:37 -07:00
|
|
|
def _prompt_admin_credentials():
|
|
|
|
|
"""Interactively ask for admin username and password when running in a terminal."""
|
|
|
|
|
import getpass
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
print(" Set up your admin account:")
|
|
|
|
|
print(" (Press Enter to accept defaults)")
|
|
|
|
|
print()
|
|
|
|
|
|
2026-06-16 02:52:15 -05:00
|
|
|
while True:
|
|
|
|
|
username = input(" Username [admin]: ").strip().lower()
|
|
|
|
|
if not username:
|
|
|
|
|
username = "admin"
|
|
|
|
|
if username in RESERVED_USERNAMES:
|
|
|
|
|
print(f" '{username}' is a reserved username. Choose another.")
|
|
|
|
|
continue
|
|
|
|
|
break
|
2026-06-01 21:14:37 -07:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
password = getpass.getpass(" Password: ")
|
|
|
|
|
if not password:
|
|
|
|
|
print(" Password cannot be empty.")
|
|
|
|
|
continue
|
2026-06-16 02:52:15 -05:00
|
|
|
if len(password) < PASSWORD_MIN_LENGTH:
|
|
|
|
|
print(f" Password must be at least {PASSWORD_MIN_LENGTH} characters.")
|
|
|
|
|
continue
|
2026-06-01 21:14:37 -07:00
|
|
|
confirm = getpass.getpass(" Confirm password: ")
|
|
|
|
|
if password != confirm:
|
|
|
|
|
print(" Passwords don't match. Try again.")
|
|
|
|
|
continue
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
return username, password
|
|
|
|
|
|
|
|
|
|
|
2026-05-31 23:58:26 +09:00
|
|
|
def create_default_admin():
|
|
|
|
|
"""Create an initial admin user if none exists."""
|
2026-06-08 09:58:52 +02:00
|
|
|
auth_path = AUTH_FILE
|
2026-05-31 23:58:26 +09:00
|
|
|
if os.path.exists(auth_path):
|
|
|
|
|
print(" [skip] auth.json already exists")
|
2026-06-01 10:55:42 +03:00
|
|
|
return "exists"
|
2026-05-31 23:58:26 +09:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import bcrypt
|
|
|
|
|
import json
|
|
|
|
|
|
2026-06-01 21:14:37 -07:00
|
|
|
# Priority: env vars > interactive prompt > random password
|
|
|
|
|
username = os.getenv("ODYSSEUS_ADMIN_USER", "").strip().lower()
|
|
|
|
|
password = os.getenv("ODYSSEUS_ADMIN_PASSWORD", "").strip()
|
|
|
|
|
|
|
|
|
|
if username and password:
|
2026-06-16 02:52:15 -05:00
|
|
|
# Both provided via env — validate before using
|
|
|
|
|
if username in RESERVED_USERNAMES:
|
|
|
|
|
print(f" [error] ODYSSEUS_ADMIN_USER '{username}' is a reserved username")
|
|
|
|
|
return "failed"
|
|
|
|
|
if len(password) < PASSWORD_MIN_LENGTH:
|
|
|
|
|
print(f" [error] ODYSSEUS_ADMIN_PASSWORD must be at least {PASSWORD_MIN_LENGTH} characters")
|
|
|
|
|
return "failed"
|
2026-06-01 21:14:37 -07:00
|
|
|
elif sys.stdin.isatty() and not os.getenv("ODYSSEUS_SKIP_ADMIN_PROMPT"):
|
|
|
|
|
# Interactive terminal — ask the user
|
|
|
|
|
username, password = _prompt_admin_credentials()
|
|
|
|
|
else:
|
|
|
|
|
# Non-interactive (Docker, CI) — fall back to generated password
|
|
|
|
|
username = username or "admin"
|
|
|
|
|
password = password or __import__("secrets").token_urlsafe(18)
|
|
|
|
|
|
|
|
|
|
username = username or "admin"
|
2026-05-31 23:58:26 +09:00
|
|
|
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
|
|
|
|
auth_data = {
|
|
|
|
|
"users": {
|
|
|
|
|
username: {
|
|
|
|
|
"password_hash": hashed,
|
|
|
|
|
"is_admin": True,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-01 15:09:47 +09:00
|
|
|
with open(auth_path, "w", encoding="utf-8") as f:
|
2026-05-31 23:58:26 +09:00
|
|
|
json.dump(auth_data, f, indent=2)
|
2026-06-01 21:14:37 -07:00
|
|
|
|
|
|
|
|
if sys.stdin.isatty() and not os.getenv("ODYSSEUS_ADMIN_PASSWORD"):
|
|
|
|
|
print(f" [ok] Admin account created ({username})")
|
|
|
|
|
else:
|
|
|
|
|
print(f" [ok] Initial admin user created ({username})")
|
|
|
|
|
if not os.getenv("ODYSSEUS_ADMIN_PASSWORD"):
|
|
|
|
|
print(f" Temporary password: {password}")
|
|
|
|
|
print(f" ** Change it after first login. Set ODYSSEUS_ADMIN_PASSWORD to choose your own. **")
|
2026-06-01 10:55:42 +03:00
|
|
|
return "created"
|
2026-06-07 19:28:37 +03:00
|
|
|
except ImportError as e:
|
|
|
|
|
if "incompatible architecture" in str(e).lower():
|
|
|
|
|
# bcrypt is present but built for the wrong CPU architecture — the
|
|
|
|
|
# same Apple Silicon mismatch check_arch() guards against, caught here
|
|
|
|
|
# for the rarer case of an x86 wheel inside an arm64 venv.
|
|
|
|
|
print(" [error] bcrypt loaded with the wrong CPU architecture.")
|
|
|
|
|
print(" Rebuild the venv with an arm64 Python:")
|
|
|
|
|
print(" rm -rf venv && /opt/homebrew/bin/python3.11 -m venv venv")
|
|
|
|
|
print(" ./venv/bin/pip install -r requirements.txt")
|
|
|
|
|
return "skipped"
|
2026-05-31 23:58:26 +09:00
|
|
|
print(" [warn] bcrypt not installed — skipping admin user creation")
|
|
|
|
|
print(" Run: pip install bcrypt")
|
2026-06-01 10:55:42 +03:00
|
|
|
return "skipped"
|
2026-05-31 23:58:26 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_env():
|
|
|
|
|
"""Copy .env.example to .env if it doesn't exist."""
|
|
|
|
|
env_path = os.path.join(BASE_DIR, ".env")
|
|
|
|
|
example_path = os.path.join(BASE_DIR, ".env.example")
|
|
|
|
|
if os.path.exists(env_path):
|
|
|
|
|
print(" [skip] .env already exists")
|
|
|
|
|
return
|
|
|
|
|
if os.path.exists(example_path):
|
|
|
|
|
import shutil
|
|
|
|
|
shutil.copy2(example_path, env_path)
|
|
|
|
|
print(" [ok] .env created from .env.example")
|
|
|
|
|
print(" ** Edit .env with your LLM host and API keys **")
|
|
|
|
|
else:
|
|
|
|
|
print(" [warn] .env.example not found — create .env manually")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_deps():
|
|
|
|
|
"""Check for common missing dependencies."""
|
|
|
|
|
missing = []
|
|
|
|
|
for mod in ["fastapi", "uvicorn", "sqlalchemy", "bcrypt", "httpx", "dotenv"]:
|
|
|
|
|
try:
|
|
|
|
|
__import__(mod)
|
|
|
|
|
except ImportError:
|
|
|
|
|
missing.append(mod)
|
|
|
|
|
if missing:
|
|
|
|
|
print(f"\n [warn] Missing packages: {', '.join(missing)}")
|
|
|
|
|
print(f" Run: pip install -r requirements.txt")
|
|
|
|
|
else:
|
|
|
|
|
print(" [ok] All core dependencies installed")
|
|
|
|
|
|
|
|
|
|
if os.name != "nt" and shutil.which("tmux") is None:
|
|
|
|
|
print("\n [warn] tmux not found")
|
|
|
|
|
print(" Cookbook uses tmux for background downloads and model serves.")
|
|
|
|
|
print(" Install it with your OS package manager, for example:")
|
Add macOS Apple Silicon Cookbook support
* 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>
2026-06-01 15:29:19 +09:30
|
|
|
if sys.platform == "darwin":
|
|
|
|
|
print(" brew install tmux")
|
|
|
|
|
else:
|
|
|
|
|
print(" sudo apt install tmux")
|
|
|
|
|
print(" sudo pacman -S tmux")
|
|
|
|
|
print(" sudo dnf install tmux")
|
2026-05-31 23:58:26 +09:00
|
|
|
elif os.name != "nt":
|
|
|
|
|
print(" [ok] tmux installed")
|
|
|
|
|
|
|
|
|
|
|
2026-06-07 19:28:37 +03:00
|
|
|
def check_arch():
|
|
|
|
|
"""Stop early, with guidance, if we're on Apple Silicon but running an
|
|
|
|
|
Intel (x86_64) Python through Rosetta.
|
|
|
|
|
|
|
|
|
|
A venv built with such an interpreter installs and loads compiled packages
|
|
|
|
|
(bcrypt, pydantic-core, onnxruntime, …) for the wrong CPU architecture, then
|
|
|
|
|
dies deep inside an import with a cryptic
|
|
|
|
|
"(mach-o file, but is an incompatible architecture)" error. Catching it here
|
|
|
|
|
turns that into one clear, actionable message.
|
|
|
|
|
"""
|
|
|
|
|
if sys.platform != "darwin" or platform.machine() == "arm64":
|
|
|
|
|
return # Not macOS, or already an arm64-native interpreter — nothing to do.
|
|
|
|
|
|
|
|
|
|
# platform.machine() == "x86_64": either a genuine Intel Mac (fine) or an x86
|
|
|
|
|
# interpreter running under Rosetta on Apple Silicon (the case we must catch).
|
|
|
|
|
try:
|
|
|
|
|
translated = subprocess.run(
|
|
|
|
|
["sysctl", "-n", "sysctl.proc_translated"],
|
|
|
|
|
capture_output=True, text=True, timeout=5,
|
|
|
|
|
).stdout.strip()
|
|
|
|
|
except Exception:
|
|
|
|
|
translated = ""
|
|
|
|
|
if translated != "1":
|
|
|
|
|
return # Genuine Intel Mac — carry on.
|
|
|
|
|
|
|
|
|
|
print("\n [error] This is an Apple Silicon Mac, but setup is running under an")
|
|
|
|
|
print(" Intel (x86_64) Python through Rosetta. Compiled packages would")
|
|
|
|
|
print(' load as the wrong architecture and crash with "incompatible')
|
|
|
|
|
print(' architecture" later on.')
|
|
|
|
|
print("\n Rebuild the environment with Homebrew's arm64 Python:")
|
|
|
|
|
print(" brew install python@3.11 # if you don't have it yet")
|
|
|
|
|
print(" rm -rf venv")
|
|
|
|
|
print(" /opt/homebrew/bin/python3.11 -m venv venv")
|
|
|
|
|
print(" ./venv/bin/pip install -r requirements.txt")
|
|
|
|
|
print(" ./venv/bin/python setup.py")
|
|
|
|
|
print("\n Tip: ./start-macos.sh does all of this with the right Python.\n")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
2026-05-31 23:58:26 +09:00
|
|
|
def main():
|
|
|
|
|
print("\n=== Odysseus Setup ===\n")
|
|
|
|
|
|
2026-06-23 23:38:05 +05:30
|
|
|
# Load .env so pre-seeded ODYSSEUS_ADMIN_USER / ODYSSEUS_ADMIN_PASSWORD (and
|
|
|
|
|
# other deployment vars) are honored on native installs, not just when they
|
|
|
|
|
# are exported in the shell. Mirrors app.py: encoding="utf-8-sig" tolerates a
|
|
|
|
|
# UTF-8 BOM in a Notepad-saved .env. load_dotenv does not override already
|
|
|
|
|
# exported OS env vars, so the existing precedence is preserved. python-dotenv
|
|
|
|
|
# is a hard dependency (requirements.txt) and is verified by check_deps below.
|
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
load_dotenv(os.path.join(BASE_DIR, ".env"), encoding="utf-8-sig")
|
|
|
|
|
|
2026-06-07 19:28:37 +03:00
|
|
|
# Fail fast with a clear message if the CPU architecture is wrong (Apple
|
|
|
|
|
# Silicon under an x86/Rosetta Python) before importing anything native.
|
|
|
|
|
check_arch()
|
|
|
|
|
|
2026-05-31 23:58:26 +09:00
|
|
|
print("1. Creating directories...")
|
|
|
|
|
create_dirs()
|
|
|
|
|
|
|
|
|
|
print("\n2. Environment file...")
|
|
|
|
|
create_env()
|
|
|
|
|
|
|
|
|
|
print("\n3. Checking dependencies...")
|
|
|
|
|
check_deps()
|
|
|
|
|
|
|
|
|
|
print("\n4. Initializing database...")
|
|
|
|
|
try:
|
|
|
|
|
init_database()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f" [warn] Database init failed: {e}")
|
|
|
|
|
print(" This is OK if dependencies aren't installed yet.")
|
|
|
|
|
|
|
|
|
|
print("\n5. Creating initial admin...")
|
2026-06-01 10:55:42 +03:00
|
|
|
|
|
|
|
|
admin_status = "failed"
|
|
|
|
|
|
2026-05-31 23:58:26 +09:00
|
|
|
try:
|
2026-06-01 10:55:42 +03:00
|
|
|
admin_status = create_default_admin()
|
2026-05-31 23:58:26 +09:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f" [warn] Admin creation failed: {e}")
|
2026-06-01 10:55:42 +03:00
|
|
|
admin_status = "failed"
|
2026-05-31 23:58:26 +09:00
|
|
|
|
|
|
|
|
print("\n=== Setup complete ===")
|
Add macOS Apple Silicon Cookbook support
* 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>
2026-06-01 15:29:19 +09:30
|
|
|
# start-macos.sh launches the server itself (on its own port) right after
|
|
|
|
|
# this, so suppress the manual hint there to avoid a contradictory URL.
|
|
|
|
|
if not os.getenv("ODYSSEUS_SKIP_RUN_HINT"):
|
|
|
|
|
print(f"\nStart the server with:")
|
|
|
|
|
print(f" python -m uvicorn app:app --host 127.0.0.1 --port 7000")
|
|
|
|
|
print(f"\nThen open http://localhost:7000")
|
2026-06-01 10:55:42 +03:00
|
|
|
|
|
|
|
|
# Cleaned, action-focused final instruction strings
|
|
|
|
|
if admin_status == "created":
|
2026-06-01 21:14:37 -07:00
|
|
|
print("Login with your admin credentials.\n")
|
2026-06-01 10:55:42 +03:00
|
|
|
elif admin_status == "exists":
|
|
|
|
|
print("Login with your existing admin credentials.\n")
|
|
|
|
|
elif admin_status == "skipped":
|
|
|
|
|
print("Admin creation did not happen: dependencies are missing.\nRun 'pip install bcrypt' and rerun setup.\n")
|
|
|
|
|
elif admin_status == "failed":
|
|
|
|
|
print("Admin creation did not happen: a system or file error occurred.\nCheck write permissions for the 'data' directory and rerun setup.\n")
|
|
|
|
|
else: # handling "failed" or any unhandled edge case
|
|
|
|
|
print("Admin creation did not happen: a system or file error occurred.\nCheck write permissions for the 'data' directory and rerun setup.\n")
|
2026-05-31 23:58:26 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|