#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=/dev/null . "$SCRIPT_DIR/lib/bootstrap.sh" 2>/dev/null || . "$SCRIPT_DIR/../lib/bootstrap.sh" 2>/dev/null || { echo "Error: missing LocalAI bootstrap library: lib/bootstrap.sh" >&2; exit 1; } AI_DIR="$(resolve_ai_dir)" BIN_DIR="$AI_DIR/$LOCALAI_BIN_SUBDIR" CONF_DIR="$AI_DIR/$LOCALAI_CONF_SUBDIR" PORT_FILE="$CONF_DIR/$LOCALAI_PORT_FILE" PID_FILE="$CONF_DIR/$LOCALAI_PID_FILE" PID_START_FILE="$CONF_DIR/$LOCALAI_PID_FILE.start" LOG_FILE="$AI_DIR/$LOCALAI_LOGS_SUBDIR/llama-swap.log" CONFIG_FILE="$CONF_DIR/$LOCALAI_CONFIG_FILE" KEYS_DIR="$CONF_DIR/$LOCALAI_KEYS_SUBDIR" resolve_llama_swap_paths fail_start() { local message="$1" if pid_file_matches_process "$PID_FILE" "$PID_START_FILE"; then kill "$PID" 2>/dev/null || true fi rm -f "$PID_FILE" "$PID_START_FILE" echo "Error: $message. See $LOG_FILE" >&2 exit 1 } mkdir -p "$CONF_DIR" "$AI_DIR/$LOCALAI_LOGS_SUBDIR" if [ -z "$LLAMA_SWAP_BIN" ]; then echo "Error: llama-swap is not installed or is not in PATH." >&2 exit 1 fi if [ ! -x "$BIN_DIR/llama-server" ]; then echo "Error: $BIN_DIR/llama-server is missing or not executable." >&2 exit 1 fi if [ ! -f "$PORT_FILE" ]; then echo "$LOCALAI_DEFAULT_PORT" > "$PORT_FILE" fi PORT=$(<"$PORT_FILE") if ! [[ "$PORT" =~ ^[0-9]+$ ]] || ((PORT < 1 || PORT > 65535)); then echo "Error: invalid port in $PORT_FILE: $PORT" >&2 exit 1 fi if [ -f "$PID_FILE" ]; then PID=$(<"$PID_FILE") if pid_file_matches_process "$PID_FILE" "$PID_START_FILE"; then COMMAND=$(ps -p "$PID" -o comm= 2>/dev/null || true) if [[ "$COMMAND" == *llama-swap* ]]; then echo "LocalAI is already running (PID $PID) on port $PORT." exit 0 fi fi rm -f "$PID_FILE" "$PID_START_FILE" fi # Another program on this port would answer the readiness probe below, so a # llama-swap that failed to bind would be reported as started. if port_is_listening "$PORT"; then echo "Error: port $PORT is already in use by another program, so LocalAI cannot listen on it." >&2 echo "Stop that program, or choose another port: echo NEW_PORT > $PORT_FILE" >&2 exit 1 fi "$BIN_DIR/rebuild-config.sh" if ! localai_has_model_entries "$AI_DIR/$LOCALAI_MODELS_SUBDIR"; then echo "No GGUF models found in $AI_DIR/$LOCALAI_MODELS_SUBDIR." echo "LocalAI will start, but chat requests need a model file and a matching model name." fi nohup "$LLAMA_SWAP_BIN" \ --listen "${LOCALAI_LISTEN_HOST}:${PORT}" \ --config "$CONFIG_FILE" \ --config-dir "$KEYS_DIR" \ > "$LOG_FILE" 2>&1 & PID=$! echo "$PID" > "$PID_FILE" process_start_time "$PID" > "$PID_START_FILE" # Read once: an API key active in config.yaml also protects /running, so the # readiness probe below must authenticate the same way external clients do. api_auth_curl_args for _ in {1..30}; do if ! pid_file_matches_process "$PID_FILE" "$PID_START_FILE"; then fail_start "llama-swap failed to start" fi if command -v curl >/dev/null 2>&1 && curl "${AUTH_CURL_ARGS[@]}" --max-time 2 -fsS "http://${LOCALAI_LISTEN_HOST}:${PORT}/running" >/dev/null 2>&1; then break fi sleep 0.5 done if ! command -v curl >/dev/null 2>&1 || ! curl "${AUTH_CURL_ARGS[@]}" --max-time 2 -fsS "http://${LOCALAI_LISTEN_HOST}:${PORT}/running" >/dev/null 2>&1; then fail_start "llama-swap started but API did not become ready at http://${LOCALAI_LISTEN_HOST}:${PORT}/running" fi # The answer must come from this llama-swap, not one that exited after a # failed bind while something else took the port. if ! pid_file_matches_process "$PID_FILE" "$PID_START_FILE"; then fail_start "llama-swap exited right after starting" fi echo "LocalAI started at http://${LOCALAI_LISTEN_HOST}:${PORT} (PID $PID)" echo "Use 'localai models' to see model names, and 'localai check' to verify the API."