secubox-openwrt/package/secubox/secubox-app-localai/files/usr/sbin/localaictl
CyberMind-FR b245fdb3e7 feat(localai,ollama): Switch LocalAI to Docker and add Ollama package
LocalAI changes:
- Rewrite localaictl to use Docker/Podman instead of standalone binary
- Use localai/localai:v2.25.0-ffmpeg image with all backends included
- Fix llama-cpp backend not found issue
- Auto-detect podman or docker runtime
- Update UCI config with Docker settings

New Ollama package:
- Add secubox-app-ollama as lighter alternative to LocalAI
- Native ARM64 support with backends included
- Simple CLI: ollamactl pull/run/list
- Docker image ~1GB vs 2-4GB for LocalAI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 17:56:40 +01:00

545 lines
14 KiB
Bash

#!/bin/sh
# SecuBox LocalAI manager - Docker/Podman container support
# Copyright (C) 2025 CyberMind.fr
#
# Uses LocalAI Docker image with all backends included (llama-cpp, etc.)
CONFIG="localai"
CONTAINER_NAME="localai"
LOCALAI_VERSION="v2.25.0"
# Docker image with all backends included
LOCALAI_IMAGE="localai/localai:${LOCALAI_VERSION}-ffmpeg"
# Paths
DATA_PATH="/srv/localai"
MODELS_PATH="/srv/localai/models"
usage() {
cat <<'EOF'
Usage: localaictl <command>
Commands:
install Pull Docker image and setup LocalAI
check Run prerequisite checks
update Update LocalAI Docker image
status Show container and service status
logs Show LocalAI logs (use -f to follow)
shell Open shell in container
Model Management:
models List installed models
model-install <n> Install model from preset or URL
model-remove <n> Remove installed model
Service Control:
service-run Internal: run container under procd
service-stop Stop container
API Endpoints (default port 8080):
/v1/chat/completions - Chat completion (OpenAI compatible)
/v1/completions - Text completion
/v1/embeddings - Generate embeddings
/v1/models - List available models
/ - Web UI
Configuration: /etc/config/localai
EOF
}
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
log_info() { echo "[INFO] $*"; logger -t localai "$*"; }
log_warn() { echo "[WARN] $*" >&2; logger -t localai -p warning "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t localai -p err "$*"; }
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
# Load configuration with defaults
load_config() {
api_port="$(uci_get main.api_port || echo 8080)"
api_host="$(uci_get main.api_host || echo 0.0.0.0)"
data_path="$(uci_get main.data_path || echo /srv/localai)"
models_path="$(uci_get main.models_path || echo /srv/localai/models)"
memory_limit="$(uci_get main.memory_limit || echo 2g)"
threads="$(uci_get main.threads || echo 4)"
context_size="$(uci_get main.context_size || echo 2048)"
debug="$(uci_get main.debug || echo 0)"
cors="$(uci_get main.cors || echo 1)"
# Docker settings
docker_image="$(uci_get docker.image || echo $LOCALAI_IMAGE)"
# Ensure paths exist
[ -d "$data_path" ] || mkdir -p "$data_path"
[ -d "$models_path" ] || mkdir -p "$models_path"
}
# =============================================================================
# CONTAINER RUNTIME DETECTION
# =============================================================================
# Detect available container runtime (podman preferred, then docker)
detect_runtime() {
if command -v podman >/dev/null 2>&1; then
RUNTIME="podman"
elif command -v docker >/dev/null 2>&1; then
RUNTIME="docker"
else
RUNTIME=""
fi
echo "$RUNTIME"
}
has_runtime() {
[ -n "$(detect_runtime)" ]
}
run_container() {
local runtime=$(detect_runtime)
[ -z "$runtime" ] && { log_error "No container runtime found"; return 1; }
$runtime "$@"
}
# =============================================================================
# CONTAINER MANAGEMENT
# =============================================================================
container_exists() {
run_container ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
}
container_running() {
run_container ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
}
container_stop() {
if container_running; then
log_info "Stopping container..."
run_container stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if container_exists; then
run_container rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
}
container_pull() {
load_config
log_info "Pulling LocalAI Docker image: $docker_image"
log_info "This may take several minutes (image is ~2-4GB)..."
if run_container pull "$docker_image"; then
log_info "Image pulled successfully"
return 0
else
log_error "Failed to pull image"
return 1
fi
}
container_run() {
load_config
container_stop
log_info "Starting LocalAI container..."
log_info "Image: $docker_image"
log_info "API: http://${api_host}:${api_port}"
log_info "Models: $models_path"
# Build environment variables
local env_args=""
env_args="$env_args -e LOCALAI_THREADS=$threads"
env_args="$env_args -e LOCALAI_CONTEXT_SIZE=$context_size"
[ "$debug" = "1" ] && env_args="$env_args -e LOCALAI_DEBUG=true"
[ "$cors" = "1" ] && env_args="$env_args -e LOCALAI_CORS=true"
# Run container in foreground (for procd)
exec run_container run --rm \
--name "$CONTAINER_NAME" \
-p "${api_port}:8080" \
-v "${models_path}:/models:rw" \
-v "${data_path}:/build:rw" \
--memory="$memory_limit" \
$env_args \
"$docker_image"
}
container_status() {
load_config
local runtime=$(detect_runtime)
echo "=== LocalAI Status ==="
echo ""
echo "Container Runtime: ${runtime:-NOT FOUND}"
echo ""
if [ -n "$runtime" ]; then
if container_running; then
echo "Container Status: RUNNING"
echo ""
run_container ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
elif container_exists; then
echo "Container Status: STOPPED"
else
echo "Container Status: NOT CREATED"
fi
fi
echo ""
echo "=== Configuration ==="
echo "Image: $docker_image"
echo "API port: $api_port"
echo "Data path: $data_path"
echo "Models path: $models_path"
echo "Memory limit: $memory_limit"
echo "Threads: $threads"
echo "Context size: $context_size"
echo ""
# Check API health
if wget -q -O - "http://127.0.0.1:$api_port/readyz" 2>/dev/null | grep -q "ok"; then
echo "API Status: HEALTHY"
# List loaded models via API
echo ""
echo "=== Loaded Models (via API) ==="
local models=$(wget -q -O - "http://127.0.0.1:$api_port/v1/models" 2>/dev/null)
if [ -n "$models" ]; then
echo "$models" | jsonfilter -e '@.data[*].id' 2>/dev/null | while read model; do
echo " - $model"
done
fi
else
echo "API Status: NOT RESPONDING"
fi
}
container_logs() {
if [ "$1" = "-f" ]; then
run_container logs -f "$CONTAINER_NAME"
else
run_container logs --tail 100 "$CONTAINER_NAME"
fi
}
container_shell() {
run_container exec -it "$CONTAINER_NAME" /bin/sh
}
# =============================================================================
# MODEL MANAGEMENT
# =============================================================================
cmd_models() {
load_config
echo "=== Installed Models ==="
echo ""
if [ -d "$models_path" ]; then
local count=0
for model in "$models_path"/*.gguf "$models_path"/*.bin "$models_path"/*.onnx; do
[ -f "$model" ] || continue
count=$((count + 1))
local name=$(basename "$model")
local size=$(ls -lh "$model" | awk '{print $5}')
echo " $count. $name ($size)"
done
# Also check for yaml configs (gallery models)
for yaml in "$models_path"/*.yaml; do
[ -f "$yaml" ] || continue
local name=$(basename "$yaml" .yaml)
echo " - $name (config)"
done
if [ "$count" -eq 0 ]; then
echo " No models installed"
echo ""
echo "Install a model with:"
echo " localaictl model-install tinyllama"
echo " localaictl model-install phi2"
fi
else
echo " Models directory not found: $models_path"
fi
echo ""
echo "=== Available Presets ==="
echo ""
# List presets from UCI config
uci show localai 2>/dev/null | grep "=preset" | while read line; do
local section=$(echo "$line" | cut -d. -f2 | cut -d= -f1)
local name=$(uci_get "$section.name")
local desc=$(uci_get "$section.description")
local size=$(uci_get "$section.size")
[ -n "$name" ] && echo " $name - $desc ($size)"
done
}
cmd_model_install() {
load_config
require_root
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: localaictl model-install <preset-name|url>"; return 1; }
mkdir -p "$models_path"
# Check if it's a preset
local preset_url=""
local preset_file=""
# Search presets in UCI
for section in $(uci show localai 2>/dev/null | grep "=preset" | cut -d. -f2 | cut -d= -f1); do
local pname=$(uci_get "$section.name")
if [ "$pname" = "$model_name" ]; then
preset_url=$(uci_get "$section.url")
preset_file=$(basename "$preset_url")
break
fi
done
if [ -n "$preset_url" ]; then
log_info "Installing preset model: $model_name"
log_info "URL: $preset_url"
log_info "This may take a while depending on model size..."
if wget --show-progress -O "$models_path/$preset_file" "$preset_url"; then
log_info "Model downloaded: $models_path/$preset_file"
# Create model config YAML for LocalAI
cat > "$models_path/$model_name.yaml" << EOF
name: $model_name
backend: llama-cpp
parameters:
model: $preset_file
context_size: $context_size
threads: $threads
EOF
log_info "Model config created: $models_path/$model_name.yaml"
log_info ""
log_info "Model '$model_name' installed successfully!"
log_info "Restart LocalAI to load: /etc/init.d/localai restart"
else
log_error "Failed to download model"
return 1
fi
elif echo "$model_name" | grep -q "^http"; then
# Direct URL download
local filename=$(basename "$model_name")
log_info "Downloading model from URL..."
if wget --show-progress -O "$models_path/$filename" "$model_name"; then
log_info "Model installed: $models_path/$filename"
else
log_error "Failed to download model"
return 1
fi
else
log_error "Unknown model or preset: $model_name"
echo ""
echo "Available presets:"
uci show localai 2>/dev/null | grep "=preset" | while read line; do
local section=$(echo "$line" | cut -d. -f2 | cut -d= -f1)
local pname=$(uci_get "$section.name")
[ -n "$pname" ] && echo " - $pname"
done
return 1
fi
}
cmd_model_remove() {
load_config
require_root
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: localaictl model-remove <model-name>"; return 1; }
# Find and remove model files
local found=0
for ext in gguf bin onnx yaml; do
local file="$models_path/$model_name.$ext"
if [ -f "$file" ]; then
rm -f "$file"
log_info "Removed: $file"
found=1
fi
done
# Also try to match partial names (model file might have different name)
for file in "$models_path"/*"$model_name"*; do
if [ -f "$file" ]; then
rm -f "$file"
log_info "Removed: $file"
found=1
fi
done
if [ "$found" -eq 0 ]; then
log_warn "Model not found: $model_name"
else
log_info "Restart LocalAI to apply: /etc/init.d/localai restart"
fi
}
# =============================================================================
# COMMANDS
# =============================================================================
cmd_install() {
require_root
load_config
if ! has_runtime; then
log_error "No container runtime found!"
log_error "Install podman or docker first:"
log_error " opkg update && opkg install podman"
exit 1
fi
local runtime=$(detect_runtime)
log_info "Installing LocalAI using $runtime..."
log_info "Image: $docker_image"
# Create directories
mkdir -p "$data_path"
mkdir -p "$models_path"
# Pull the image
container_pull || exit 1
# Enable service
uci_set main.enabled '1'
/etc/init.d/localai enable
log_info ""
log_info "LocalAI installed successfully!"
log_info ""
log_info "Start with: /etc/init.d/localai start"
log_info "API endpoint: http://<router-ip>:$api_port/v1"
log_info "Web UI: http://<router-ip>:$api_port"
log_info ""
log_info "Install a model to get started:"
log_info " localaictl model-install tinyllama # Lightweight (669MB)"
log_info " localaictl model-install phi2 # Balanced (1.6GB)"
}
cmd_check() {
load_config
echo "=== Prerequisite Check ==="
echo ""
# Check container runtime
local runtime=$(detect_runtime)
if [ -n "$runtime" ]; then
echo "[OK] Container runtime: $runtime"
$runtime --version 2>/dev/null | head -1
else
echo "[FAIL] No container runtime found"
echo " Install: opkg install podman"
fi
echo ""
# Check storage
local storage_path=$(dirname "$data_path")
local storage_avail=$(df -h "$storage_path" 2>/dev/null | tail -1 | awk '{print $4}')
echo "Storage available: $storage_avail (at $storage_path)"
echo " Note: LocalAI image requires ~2-4GB"
echo " Models require 500MB-8GB each"
echo ""
# Check memory
local mem_total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
local mem_gb=$((mem_total / 1024 / 1024))
echo "System memory: ${mem_gb}GB"
if [ "$mem_gb" -lt 2 ]; then
echo "[WARN] Low memory! LocalAI requires at least 2GB RAM"
else
echo "[OK] Memory sufficient"
fi
echo ""
# Check if image exists
if [ -n "$runtime" ]; then
if $runtime images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "localai"; then
echo "[OK] LocalAI image found"
else
echo "[INFO] LocalAI image not downloaded yet"
echo " Run: localaictl install"
fi
fi
}
cmd_update() {
require_root
load_config
log_info "Updating LocalAI..."
# Stop if running
container_stop
# Pull latest image
container_pull || exit 1
# Restart if was enabled
if [ "$(uci_get main.enabled)" = "1" ]; then
/etc/init.d/localai restart
else
log_info "Update complete. Start manually with: /etc/init.d/localai start"
fi
}
cmd_status() {
container_status
}
cmd_logs() {
container_logs "$@"
}
cmd_shell() {
if ! container_running; then
log_error "Container not running. Start with: /etc/init.d/localai start"
exit 1
fi
container_shell
}
cmd_service_run() {
require_root
load_config
if ! has_runtime; then
log_error "No container runtime found"
exit 1
fi
container_run
}
cmd_service_stop() {
require_root
container_stop
}
# Main Entry Point
case "${1:-}" in
install) shift; cmd_install "$@" ;;
check) shift; cmd_check "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;;
models) shift; cmd_models "$@" ;;
model-install) shift; cmd_model_install "$@" ;;
model-remove) shift; cmd_model_remove "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
help|--help|-h|'') usage ;;
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
esac