* v0.4.3 — add kimi-k2.6 vision model, default all roles to nemotron-3-nano:30b
- KNOWN_CLOUD_MODELS: add kimi-k2.6 with [vision, tools, thinking, cloud]
- LLMConfig defaults: reranker/scraper/summary/default all set to nemotron-3-nano:30b
- available_models: add nemotron-3-nano:30b and kimi-k2.6
- dashboard.html: add kimi-k2.6 and nemotron-3-nano capability maps
- cli.py setup wizard: update all model prompts/default placeholders
- version bump: 0.4.2 → 0.4.3 in __init__.py, app.py, pyproject.toml
Update-safe: only new model registry entries + default value changes.
Existing configs preserve their saved values (pydantic).
Zero schema or API break.
* feat: cost-weighted analytics (usage_multiplier) + add deepseek-v4 models
- Add usage_multiplier column to analytics DB (auto-resolved from model name)
- Log weighted token totals (prompt*multiplier, completion*multiplier) in summary
- Show cost bars + weighted totals in dashboard analytics tab
- Add per-model multiplier column to analytics table
- Auto-compute total_tokens in analytics if not provided
- Add deepseek-v4-pro (1.0x) and deepseek-v4-flash (0.50x) to KNOWN_CLOUD_MODELS
- Add gemini-3-flash-preview to available_models and dashboard capability maps
- Add new models to config.py available_models list
* feat: auto-infer capabilities and multiplier for unknown models
Previously new Ollama Cloud models showed no capability badges and
assumed 1.00x multiplier until manually added to KNOWN_CLOUD_MODELS.
Now both backend (client.py) and frontend (dashboard.html) fall
back to name-based inference for unknown models:
- Capabilities: 'vision' from vl/gemma/gemini/kimi/deepseek, 'tools'
from coder/minimax/glm/mistral/gpt-oss/devstral/nemotron/deepseek,
'thinking' from deepseek/cogito/reason/think and kimi-k* families
- Multiplier: extract :XXb|:Xt from model name for size-based tier
(<=20b=0.25, <=80b=0.50, <=400b=0.75, >400b/1t=1.00), then fall
back to name heuristics (nano/mini/small=0.25, flash/gemma=0.50,
pro/mistral-large=1.00)
Models explicitly in KNOWN_CLOUD_MODELS still use exact values.
This means new models like deepseek-v5-pro or kimi-k3 show up
correctly in the dashboard immediately without code changes.
* feat: experimental Subscription Value Calculator (ROI) vs OpenRouter
Adds a new experimental feature to the Status tab that compares your
total subscription cost to what you'd pay with OpenRouter's cheapest
provider pricing.
Backend:
- guanaco/roi.py: Fetches live prices from OpenRouter API, matches
Ollama model names to OpenRouter model IDs via family inference
(exact → family prefix → same parameter size → size window →
global average), multiplies tokens by $/Mt cost.
- Calculates this week's cost, extrapolates to 100% weekly usage,
then monthly. Shows ROI multiplier (monthly value / subscription).
- /dashboard/api/roi/calculate: fresh calculation
- /dashboard/api/roi/last: cached last result
- /dashboard/api/roi/reset: clear data
- /dashboard/api/roi/config: enable/disable, set price tier
Dashboard (Status tab):
- Toggle to enable/disable the feature
- Pro (0/mo) / Max (00/mo) / Custom price selector
- Shows: this-week cost, weekly @ 100%, monthly value, ROI multiplier
- Per-model table: prompt_tokens, completion_tokens, $/Mt in/out,
total cost, % of usage
- Warnings for unmatched models and stale prices
- Reset button clears cached data
ROIConfig added to config.py (persisted in config.yaml).
* fix(config): v0.4.3 backward compat and install robustness
Changes:
- Add missing UsageConfig fields (last_plan, redirect_on_full, last_session_reset, last_weekly_reset, last_checked) to fix AttributeError crashes
- Add config migration layer in load_config() for v0.4.2 to v0.4.3+ configs
- Rename package guanaco to guanaco-llm-proxy to avoid PyPI collision
- Add Ollama API key validation during install.sh setup (fixes: use real key var, fix env file write, fix grep pattern)
- Add startup version sanity check detecting repo/venv mismatch
- Fix systemd service WorkingDirectory to repo checkout
- Add GUANACO_CONFIG_DIR env var to systemd service
* feat(v0.4.3): token estimation fallback, ROI dashboard, per-model usage breakdown
Token tracking:
- analytics.py: fall back to skimtoken estimation when API omits usage
(~15% error, better than zero). Proper total_tokens = prompt + completion.
Removed broken usage_multiplier column.
- router.py: unchanged — already passes usage → analytics correctly
Dashboard / ROI:
- roi.py: OpenRouter price fetcher with cache-hit discount logic
- dashboard.py: expose ROI config with cache_hit_pct slider
- dashboard.html: cost breakdown UI, per-model value scoring
Models:
- client.py: added minimax-m3, nemotron-3-ultra, kimi-k2.6 200k context
- client.py: per-model Ollama usage breakdown scraping from /settings
Config:
- config.py: backward compat for installs missing SearchConfig
* feat(usage-levels): scrape real per-tag usage tiers from ollama.com library pages
- Add _fetch_usage_level_sync() to parse ollama.com/library/{model} HTML
- Handle both top-level model badges (x-test-model-cost-slot-active)
and per-tag listings (x-test-model-tag-cost + x-test-model-tag-usage-slot-active)
- Add async fetch_usage_levels() with 1h global cache + parallel fetching
- Wire usage_multiplier into /v1/models and /api/ollama/models responses
- Replace heuristic _get_model_multiplier fallback with real scraped levels
- Fixes gemma3:4b/12b showing 1.00x instead of 0.25x, qwen3-vl 0.25x→0.75x, etc.
---------
Co-authored-by: evangit2 <evangit2@users.norereply.github.com>
Co-authored-by: Evan Rice <evanrice@vt.edu>
490 lines
No EOL
16 KiB
Bash
Executable file
490 lines
No EOL
16 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Guanaco installer
|
|
# Usage: curl -sSL https://raw.githubusercontent.com/evangit2/guanaco/main/install.sh | bash
|
|
#
|
|
# Supports: Linux, macOS, WSL
|
|
|
|
set -euo pipefail
|
|
|
|
REPO="evangit2/guanaco"
|
|
INSTALL_DIR="$HOME/.guanaco"
|
|
VENV_DIR="$INSTALL_DIR/venv"
|
|
BIN_DIR="$HOME/.local/bin"
|
|
DEFAULT_PORT=8080
|
|
|
|
# ── Colors (ANSI escape characters, not strings) ──
|
|
if [ -t 1 ]; then
|
|
RED=$'\033[0;31m'
|
|
GREEN=$'\033[0;32m'
|
|
YELLOW=$'\033[1;33m'
|
|
CYAN=$'\033[0;36m'
|
|
BOLD=$'\033[1m'
|
|
DIM=$'\033[2m'
|
|
RESET=$'\033[0m'
|
|
else
|
|
RED='' GREEN='' YELLOW='' CYAN='' BOLD='' DIM='' RESET=''
|
|
fi
|
|
|
|
# ── Prompt helpers ──
|
|
prompt() {
|
|
local var="$1" question="$2" default="$3"
|
|
if [ -n "$default" ]; then
|
|
printf "${CYAN}${question}${RESET} [${DIM}${default}${RESET}]: "
|
|
else
|
|
printf "${CYAN}${question}${RESET}: "
|
|
fi
|
|
read -r value < /dev/tty || true
|
|
declare -g "$var"="${value:-$default}"
|
|
}
|
|
|
|
prompt_yesno() {
|
|
local var="$1" question="$2" default="${3:-n}"
|
|
local indicator="y/N"
|
|
[ "$default" = "y" ] && indicator="Y/n"
|
|
printf "${CYAN}${question}${RESET} [${indicator}]: "
|
|
read -r value < /dev/tty || true
|
|
value="${value:-$default}"
|
|
[[ "$value" =~ ^[Yy] ]] && declare -g "$var"="y" || declare -g "$var"="n"
|
|
}
|
|
|
|
# ── Print helpers ──
|
|
step() { printf "\n ${BOLD}━━━ %s ━━━${RESET}\n" "$1"; }
|
|
info() { printf " %s\n" "$1"; }
|
|
success() { printf " ${GREEN}✓${RESET} %s\n" "$1"; }
|
|
warn() { printf " ${YELLOW}!${RESET} %s\n" "$1"; }
|
|
fail() { printf " ${RED}✗${RESET} %s\n" "$1"; }
|
|
|
|
# ── Detect platform ──
|
|
detect_platform() {
|
|
local os_name="$(uname -s)"
|
|
case "$os_name" in
|
|
Linux)
|
|
if grep -qi microsoft /proc/version 2>/dev/null; then
|
|
echo "wsl"
|
|
else
|
|
echo "linux"
|
|
fi
|
|
;;
|
|
Darwin)
|
|
echo "macos"
|
|
;;
|
|
*)
|
|
echo "unknown"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
PLATFORM=$(detect_platform)
|
|
|
|
printf "\n${BOLD} Guanaco${RESET}\n"
|
|
printf " OpenAI-compatible LLM proxy for Ollama Cloud\n\n"
|
|
info "Platform: $PLATFORM"
|
|
|
|
# ── Auto-install prereqs ──
|
|
step "Checking prerequisites"
|
|
|
|
# git
|
|
if ! command -v git &>/dev/null; then
|
|
warn "git not found — installing..."
|
|
case "$PLATFORM" in
|
|
linux|wsl)
|
|
sudo apt update -qq 2>/dev/null && sudo apt install -y -qq git 2>/dev/null || \
|
|
sudo dnf install -y -q git 2>/dev/null || \
|
|
sudo pacman -S --noconfirm git 2>/dev/null || {
|
|
fail "Could not install git automatically. Please install it and re-run."
|
|
exit 1
|
|
}
|
|
;;
|
|
macos)
|
|
xcode-select --install 2>/dev/null || true
|
|
;;
|
|
esac
|
|
command -v git &>/dev/null && success "git installed" || { fail "git still not found"; exit 1; }
|
|
fi
|
|
|
|
# python3
|
|
if ! command -v python3 &>/dev/null; then
|
|
warn "Python 3.10+ not found — installing..."
|
|
case "$PLATFORM" in
|
|
macos)
|
|
if command -v brew &>/dev/null; then
|
|
brew install python@3.12
|
|
else
|
|
fail "No Homebrew found. Install Python from https://python.org or install Homebrew first."
|
|
exit 1
|
|
fi
|
|
;;
|
|
linux|wsl)
|
|
sudo apt update -qq 2>/dev/null && sudo apt install -y -qq python3 python3-venv python3-pip 2>/dev/null || \
|
|
sudo dnf install -y -q python3 python3-pip 2>/dev/null || \
|
|
sudo pacman -S --noconfirm python python-pip 2>/dev/null || {
|
|
fail "Could not install Python automatically. Please install it and re-run."
|
|
exit 1
|
|
}
|
|
;;
|
|
*)
|
|
fail "Please install Python 3.10+ from https://python.org and re-run."
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# python3-venv (test actual creation)
|
|
if command -v python3 &>/dev/null; then
|
|
VENV_TEST_DIR=$(mktemp -d)
|
|
if ! python3 -m venv "$VENV_TEST_DIR/test_venv" &>/dev/null; then
|
|
warn "python3-venv not working — installing..."
|
|
case "$PLATFORM" in
|
|
linux|wsl)
|
|
sudo apt install -y -qq python3-venv 2>/dev/null || \
|
|
sudo dnf install -y -q python3-venv 2>/dev/null || {
|
|
fail "Could not install python3-venv. Please install it and re-run."
|
|
exit 1
|
|
}
|
|
;;
|
|
macos)
|
|
warn "venv broken — try: brew reinstall python@3.12"
|
|
;;
|
|
esac
|
|
fi
|
|
rm -rf "$VENV_TEST_DIR"
|
|
fi
|
|
|
|
success "git $(git --version 2>/dev/null | awk '{print $3}')"
|
|
PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "?")
|
|
if python3 -c "import sys; exit(0 if sys.version_info >= (3, 10) else 1)"; then
|
|
success "python $PYTHON_VERSION"
|
|
else
|
|
fail "Python 3.10+ required, found $PYTHON_VERSION"
|
|
exit 1
|
|
fi
|
|
if python3 -m venv /tmp/guanaco_venv_test &>/dev/null; then
|
|
rm -rf /tmp/guanaco_venv_test
|
|
success "venv ok"
|
|
else
|
|
rm -rf /tmp/guanaco_venv_test
|
|
warn "venv missing (will attempt install)"
|
|
fi
|
|
|
|
# ── macOS SSL cert fix ──
|
|
if [ "$PLATFORM" = "macos" ]; then
|
|
SSL_CERT_FILE=$(python3 -c "import certifi; print(certifi.where())" 2>/dev/null || echo "")
|
|
if [ -n "$SSL_CERT_FILE" ]; then
|
|
export SSL_CERT_FILE
|
|
mkdir -p "$INSTALL_DIR"
|
|
grep -q "^export SSL_CERT_FILE=" "$INSTALL_DIR/env" 2>/dev/null && \
|
|
sed -i '' "s|^export SSL_CERT_FILE=.*|export SSL_CERT_FILE=$SSL_CERT_FILE|" "$INSTALL_DIR/env" 2>/dev/null || \
|
|
echo "export SSL_CERT_FILE=$SSL_CERT_FILE" >> "$INSTALL_DIR/env"
|
|
success "ssl_certs configured"
|
|
fi
|
|
fi
|
|
|
|
# ── Configuration ──
|
|
step "Configuration"
|
|
|
|
info "You'll need an Ollama Cloud API key."
|
|
info "Get one at: ${CYAN}https://ollama.com${RESET}"
|
|
echo ""
|
|
|
|
prompt OLLAMA_API_KEY "Enter your Ollama API key" ""
|
|
|
|
if [ -n "$OLLAMA_API_KEY" ]; then
|
|
info "Validating API key with Ollama Cloud..."
|
|
VALIDATE_RESPONSE=$(curl -s -w "\n%{http_code}" "https://api.ollama.com/v1/models" \
|
|
-H "Authorization: Bearer ${OLLAMA_API_KEY}" \
|
|
-H "Accept: application/json" \
|
|
--max-time 10 2>/dev/null || echo "timeout")
|
|
HTTP_CODE=$(echo "$VALIDATE_RESPONSE" | tail -1)
|
|
if [ "$HTTP_CODE" = "200" ]; then
|
|
success "API key validated (models endpoint responds OK)"
|
|
elif [ "$HTTP_CODE" = "401" ]; then
|
|
warn "API key returned 401 Unauthorized from Ollama Cloud"
|
|
warn "Key may be invalid or expired. Guanaco will still install but inference will fail."
|
|
warn "Fix with: guanaco setup (after install)"
|
|
elif [ "$HTTP_CODE" = "timeout" ]; then
|
|
warn "Could not reach Ollama Cloud (timeout). Skipping validation."
|
|
else
|
|
warn "API key validation returned HTTP $HTTP_CODE — key may not work"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$OLLAMA_API_KEY" ]; then
|
|
echo ""
|
|
warn "No API key provided. You can set it later with: guanaco setup"
|
|
echo ""
|
|
fi
|
|
|
|
# ── Network configuration ──
|
|
step "Network configuration"
|
|
|
|
printf " ${YELLOW}Guanaco will start a server on port ${DEFAULT_PORT}.${RESET}\n\n"
|
|
|
|
printf " ${RED} SECURITY WARNING${RESET}\n"
|
|
printf " ${RED} ─────────────────${RESET}\n"
|
|
printf " ${RED} If your machine has automatic port forwarding (some VPS${RESET}\n"
|
|
printf " ${RED} providers, routers with UPnP, Cloudflare tunnels, etc.),${RESET}\n"
|
|
printf " ${RED} this will EXPOSE your Ollama API proxy to the public${RESET}\n"
|
|
printf " ${RED} internet. Anyone who finds it can use your API key and${RESET}\n"
|
|
printf " ${RED} consume your Ollama Cloud quota.${RESET}\n\n"
|
|
printf " ${RED} - Bind to 127.0.0.1 unless you need remote access${RESET}\n"
|
|
printf " ${RED} - Use a firewall or auth proxy if you must expose it${RESET}\n"
|
|
printf " ${RED} - Never run this on a public-facing VPS without auth${RESET}\n\n"
|
|
|
|
prompt PORT "Which port should Guanaco use" "$DEFAULT_PORT"
|
|
|
|
# Detect Tailscale
|
|
TS_IP=""
|
|
if command -v tailscale >/dev/null 2>&1; then
|
|
TS_IP=$(tailscale ip -4 2>/dev/null || true)
|
|
fi
|
|
if [ -z "$TS_IP" ] && [ -S /run/tailscale/tailscaled.sock ]; then
|
|
TS_IP=$(tailscale ip -4 2>/dev/null || true)
|
|
fi
|
|
if [ -z "$TS_IP" ]; then
|
|
TS_IP=$(ip -4 addr show | grep -oP 'inet 100\.\d+\.\d+\.\d+' | head -1 | awk '{print $2}' 2>/dev/null || true)
|
|
fi
|
|
|
|
if [ -n "$TS_IP" ]; then
|
|
printf " ${CYAN}Tailscale detected at ${TS_IP}${RESET}\n"
|
|
prompt_yesno BIND_LOCAL "Bind to 0.0.0.0 (accessible via Tailscale)?" "y"
|
|
if [ "$BIND_LOCAL" = "y" ]; then
|
|
BIND_HOST="0.0.0.0"
|
|
info "Bound to 0.0.0.0 — accessible at http://${TS_IP}:${PORT}/dashboard/"
|
|
else
|
|
BIND_HOST="127.0.0.1"
|
|
fi
|
|
else
|
|
prompt_yesno BIND_LOCAL "Bind to localhost only (127.0.0.1)?" "y"
|
|
if [ "$BIND_LOCAL" = "y" ]; then
|
|
BIND_HOST="127.0.0.1"
|
|
else
|
|
BIND_HOST="0.0.0.0"
|
|
warn "Binding to 0.0.0.0 — accessible from ALL interfaces. Ensure you have auth/firewall."
|
|
fi
|
|
fi
|
|
|
|
# ── Install ──
|
|
step "Installing"
|
|
|
|
# Clone or update
|
|
if [ -d "$INSTALL_DIR/repo" ]; then
|
|
info "Updating existing installation..."
|
|
cd "$INSTALL_DIR/repo"
|
|
git pull --ff-only || { warn "Could not pull updates. Using existing version."; }
|
|
else
|
|
info "Downloading Guanaco..."
|
|
git clone "https://github.com/$REPO.git" "$INSTALL_DIR/repo"
|
|
cd "$INSTALL_DIR/repo"
|
|
fi
|
|
|
|
# Create venv
|
|
info "Setting up virtual environment..."
|
|
python3 -m venv "$VENV_DIR"
|
|
|
|
# Source platform env if exists
|
|
if [ -f "$INSTALL_DIR/env" ]; then
|
|
source "$INSTALL_DIR/env"
|
|
fi
|
|
|
|
# Install
|
|
info "Installing dependencies..."
|
|
"$VENV_DIR/bin/pip" install -e . --quiet 2>&1 | tail -1
|
|
|
|
# ── Write config ──
|
|
info "Writing configuration..."
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
|
|
cat > "$INSTALL_DIR/config.yaml" << EOF
|
|
# Guanaco configuration
|
|
# Generated by install.sh on $(date -I)
|
|
|
|
router:
|
|
host: "${BIND_HOST}"
|
|
port: ${PORT}
|
|
|
|
llm:
|
|
provider: ollama_cloud
|
|
base_url: "https://api.ollama.com/v1"
|
|
api_key_env: OLLAMA_API_KEY
|
|
|
|
fallback:
|
|
enabled: false
|
|
provider: ""
|
|
api_key: ""
|
|
model: ""
|
|
primary_timeout: 30.0
|
|
stream_chunk_timeout: 180.0
|
|
max_tokens: 128000
|
|
|
|
cache:
|
|
enabled: false
|
|
EOF
|
|
|
|
# Write API key env file
|
|
if [ -n "$OLLAMA_API_KEY" ]; then
|
|
# Preserve existing env lines except old OLLAMA_API_KEY
|
|
TMP_ENV=$(mktemp)
|
|
grep -v "^export OLLAMA_API_KEY=" "$INSTALL_DIR/env" 2>/dev/null > "$TMP_ENV" || true
|
|
echo "export OLLAMA_API_KEY=\"${OLLAMA_API_KEY}\"" >> "$TMP_ENV"
|
|
mv "$TMP_ENV" "$INSTALL_DIR/env"
|
|
fi
|
|
|
|
# ── Create guanaco binary ──
|
|
mkdir -p "$BIN_DIR"
|
|
|
|
cat > "$BIN_DIR/guanaco" << SCRIPT
|
|
#!/usr/bin/env bash
|
|
GUANACO_DIR="$INSTALL_DIR"
|
|
if [ -f "\$GUANACO_DIR/env" ]; then
|
|
source "\$GUANACO_DIR/env"
|
|
fi
|
|
exec "$VENV_DIR/bin/python" -m guanaco.cli "\$@"
|
|
SCRIPT
|
|
chmod +x "$BIN_DIR/guanaco"
|
|
|
|
# Also create 'oct' alias for backward compat
|
|
cat > "$BIN_DIR/oct" << SCRIPT
|
|
#!/usr/bin/env bash
|
|
GUANACO_DIR="$INSTALL_DIR"
|
|
if [ -f "\$GUANACO_DIR/env" ]; then
|
|
source "\$GUANACO_DIR/env"
|
|
fi
|
|
exec "$VENV_DIR/bin/python" -m guanaco.cli "\$@"
|
|
SCRIPT
|
|
chmod +x "$BIN_DIR/oct"
|
|
|
|
# ── Add to PATH if needed ──
|
|
DETECT_SHELL="${SHELL##*/}"
|
|
case "$DETECT_SHELL" in
|
|
zsh) PROFILE_FILE="$HOME/.zshrc" ;;
|
|
bash) PROFILE_FILE="$HOME/.bashrc" ;;
|
|
*) PROFILE_FILE="$HOME/.profile" ;;
|
|
esac
|
|
|
|
if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
|
|
echo ""
|
|
warn "$BIN_DIR is not in your PATH."
|
|
info "Adding to $PROFILE_FILE..."
|
|
echo "" >> "$PROFILE_FILE"
|
|
echo "# Added by Guanaco" >> "$PROFILE_FILE"
|
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$PROFILE_FILE"
|
|
export PATH="$BIN_DIR:$PATH"
|
|
success "Added to $PROFILE_FILE"
|
|
fi
|
|
|
|
# ── Install complete ──
|
|
step "Install complete"
|
|
|
|
success "Guanaco installed successfully"
|
|
echo ""
|
|
printf " ${BOLD}Start the server:${RESET}\n"
|
|
info "$BIN_DIR/guanaco start"
|
|
echo ""
|
|
printf " ${DIM}Or reload your shell and use 'guanaco' directly:${RESET}\n"
|
|
info "source $PROFILE_FILE"
|
|
echo ""
|
|
printf " ${BOLD}Dashboard:${RESET}\n"
|
|
if [ -n "$TS_IP" ]; then
|
|
info "http://${TS_IP}:${PORT}/dashboard/"
|
|
info "http://127.0.0.1:${PORT}/dashboard/ (local)"
|
|
else
|
|
info "http://${BIND_HOST}:${PORT}/dashboard/"
|
|
fi
|
|
echo ""
|
|
printf " ${BOLD}CLI commands:${RESET}\n"
|
|
printf " %-24s %s\n" "guanaco status" "Show service & connection status"
|
|
printf " %-24s %s\n" "guanaco models" "List available cloud models"
|
|
printf " %-24s %s\n" "guanaco usage" "Check your Ollama Cloud usage/quota"
|
|
printf " %-24s %s\n" "guanaco analytics" "View request analytics & stats"
|
|
printf " %-24s %s\n" "guanaco key generate" "Generate an API key"
|
|
printf " %-24s %s\n" "guanaco config --show" "Show current configuration"
|
|
printf " %-24s %s\n" "guanaco setup" "Reconfigure (API key, ports, etc.)"
|
|
|
|
# ── Service setup ──
|
|
echo ""
|
|
printf " ${BOLD}How should Guanaco run?${RESET}\n"
|
|
printf " 1) Foreground — press Ctrl+C to stop\n"
|
|
printf " 2) systemd service — auto-starts on boot, runs in background\n"
|
|
echo ""
|
|
prompt_yesno USE_SYSTEMD "Install as systemd service?" "y"
|
|
|
|
SERVICE_NAME="guanaco"
|
|
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
|
|
|
if [ "$USE_SYSTEMD" = "y" ]; then
|
|
printf "\n"
|
|
info "Installing Guanaco as a systemd service..."
|
|
|
|
CONFIG_DIR="$INSTALL_DIR"
|
|
if [ ! -d "$CONFIG_DIR" ]; then
|
|
CONFIG_DIR="$HOME/.guanaco"
|
|
fi
|
|
|
|
cat << SERVICEEOF | sudo tee "$SERVICE_FILE" > /dev/null
|
|
[Unit]
|
|
Description=Guanaco - LLM Proxy & Dashboard
|
|
Documentation=https://github.com/evangit2/guanaco
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$(whoami)
|
|
Group=$(whoami)
|
|
Environment=PATH=${VENV_DIR}/bin:/usr/bin:/usr/local/bin
|
|
Environment=GUANACO_CONFIG_DIR=${CONFIG_DIR}
|
|
WorkingDirectory=${INSTALL_DIR}
|
|
ExecStart=${VENV_DIR}/bin/python -m uvicorn guanaco.app:create_app --factory --host ${BIND_HOST} --port ${PORT} --log-level info
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
Environment=GUANACO_CONFIG_DIR=${CONFIG_DIR}
|
|
WorkingDirectory=${INSTALL_DIR}/repo
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SERVICEEOF
|
|
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable "$SERVICE_NAME" 2>/dev/null
|
|
sudo systemctl restart "$SERVICE_NAME"
|
|
sleep 2
|
|
|
|
STATUS=$(sudo systemctl is-active "$SERVICE_NAME" 2>/dev/null)
|
|
if [ "$STATUS" = "active" ]; then
|
|
echo ""
|
|
success "Guanaco service is running!"
|
|
if [ -n "$TS_IP" ]; then
|
|
printf " ${BOLD}Dashboard:${RESET} http://${TS_IP}:${PORT}/dashboard/\n"
|
|
else
|
|
printf " ${BOLD}Dashboard:${RESET} http://${BIND_HOST}:${PORT}/dashboard/\n"
|
|
fi
|
|
echo ""
|
|
info "Manage with:"
|
|
printf " %-30s %s\n" "sudo systemctl status guanaco" "Check status"
|
|
printf " %-30s %s\n" "sudo systemctl stop guanaco" "Stop service"
|
|
printf " %-30s %s\n" "sudo systemctl restart guanaco" "Restart service"
|
|
printf " %-30s %s\n" "sudo journalctl -u guanaco -f" "View live logs"
|
|
else
|
|
echo ""
|
|
warn "Service may not have started. Check logs:"
|
|
info "sudo journalctl -u guanaco -n 50"
|
|
fi
|
|
else
|
|
echo ""
|
|
info "Starting Guanaco in foreground..."
|
|
info "Press Ctrl+C to stop. Run 'guanaco install' later for background service."
|
|
echo ""
|
|
"$BIN_DIR/guanaco" start
|
|
fi
|
|
|
|
# ── Platform tips ──
|
|
if [ "$PLATFORM" = "macos" ]; then
|
|
printf "\n ${DIM}macOS: To auto-start on login, see contrib/com.guanaco.start.plist${RESET}\n"
|
|
fi
|
|
if [ "$PLATFORM" = "wsl" ]; then
|
|
WIN_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
printf " ${DIM}WSL: Access dashboard from Windows at http://${WIN_IP}:${PORT}/dashboard${RESET}\n"
|
|
fi
|
|
|
|
printf "\n ${DIM}Docs: https://github.com/$REPO${RESET}\n" |