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>
This commit is contained in:
CyberMind-FR 2026-01-21 17:56:40 +01:00
parent 63c0bb3e5a
commit b245fdb3e7
8 changed files with 886 additions and 290 deletions

View File

@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-localai
PKG_VERSION:=0.1.0
PKG_RELEASE:=12
PKG_RELEASE:=13
PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-localai
PKG_RELEASE:=5
PKG_RELEASE:=6
PKG_VERSION:=0.1.0
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
@ -14,8 +14,8 @@ define Package/secubox-app-localai
CATEGORY:=Utilities
PKGARCH:=all
SUBMENU:=SecuBox Apps
TITLE:=SecuBox LocalAI - Self-hosted LLM (LXC)
DEPENDS:=+uci +libuci +jsonfilter
TITLE:=SecuBox LocalAI - Self-hosted LLM (Docker)
DEPENDS:=+uci +libuci +jsonfilter +wget-ssl
endef
define Package/secubox-app-localai/description
@ -25,10 +25,10 @@ Features:
- OpenAI-compatible API (drop-in replacement)
- No cloud dependency - all processing on-device
- Support for various models (LLaMA, Mistral, Phi, etc.)
- All backends included (llama-cpp, whisper, etc.)
- Text generation, embeddings, transcription
- Image generation (optional)
Runs in LXC container for isolation.
Runs in Docker/Podman container with all backends.
Configure in /etc/config/localai.
endef
@ -54,17 +54,20 @@ define Package/secubox-app-localai/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
echo ""
echo "LocalAI installed."
echo "LocalAI installed (Docker/Podman version)."
echo ""
echo "Prerequisites: Install podman or docker first"
echo " opkg install podman"
echo ""
echo "To install and start LocalAI:"
echo " localaictl install"
echo " localaictl install # Pull Docker image (~2-4GB)"
echo " /etc/init.d/localai start"
echo ""
echo "API endpoint: http://<router-ip>:8080/v1"
echo "Web UI: http://<router-ip>:8080"
echo ""
echo "Download models with:"
echo " localaictl model-install <model-name>"
echo " localaictl model-install tinyllama"
echo ""
}
exit 0

View File

@ -4,24 +4,30 @@ config main 'main'
option api_host '0.0.0.0'
option data_path '/srv/localai'
option models_path '/srv/localai/models'
option memory_limit '2G'
option memory_limit '2g'
option threads '4'
option context_size '2048'
option debug '0'
option cors '1'
# GPU settings (experimental on ARM64)
config gpu 'gpu'
option enabled '0'
option backend 'vulkan'
# Docker/Podman settings
config docker 'docker'
option image 'localai/localai:v2.25.0-ffmpeg'
# Default model to load on startup
config model 'default'
option enabled '1'
option name 'phi-2'
option name 'tinyllama'
option backend 'llama-cpp'
# Model presets
# Model presets - GGUF format for llama-cpp backend
config preset 'tinyllama'
option name 'tinyllama'
option url 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf'
option size '669M'
option type 'text-generation'
option description 'TinyLlama 1.1B - Ultra-lightweight'
config preset 'phi2'
option name 'phi-2'
option url 'https://huggingface.co/TheBloke/phi-2-GGUF/resolve/main/phi-2.Q4_K_M.gguf'
@ -36,13 +42,6 @@ config preset 'mistral'
option type 'text-generation'
option description 'Mistral 7B Instruct - High quality assistant'
config preset 'tinyllama'
option name 'tinyllama'
option url 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf'
option size '669M'
option type 'text-generation'
option description 'TinyLlama 1.1B - Ultra-lightweight'
config preset 'gte_small'
option name 'gte-small'
option url 'https://huggingface.co/Supabase/gte-small/resolve/main/model.onnx'

View File

@ -1,25 +1,27 @@
#!/bin/sh
# SecuBox LocalAI manager - LXC container support
# 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"
LXC_NAME="localai"
OPKG_UPDATED=0
LOCALAI_VERSION="v3.10.0"
CONTAINER_NAME="localai"
LOCALAI_VERSION="v2.25.0"
# Docker image with all backends included
LOCALAI_IMAGE="localai/localai:${LOCALAI_VERSION}-ffmpeg"
# Paths
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
DATA_PATH="/srv/localai"
MODELS_PATH="/srv/localai/models"
usage() {
cat <<'EOF'
Usage: localaictl <command>
Commands:
install Install prerequisites and create LXC container
install Pull Docker image and setup LocalAI
check Run prerequisite checks
update Update LocalAI in container
update Update LocalAI Docker image
status Show container and service status
logs Show LocalAI logs (use -f to follow)
shell Open shell in container
@ -28,8 +30,6 @@ Model Management:
models List installed models
model-install <n> Install model from preset or URL
model-remove <n> Remove installed model
model-load <n> Load model into memory
model-unload <n> Unload model from memory
Service Control:
service-run Internal: run container under procd
@ -48,13 +48,12 @@ EOF
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
log_info() { echo "[INFO] $*"; }
log_warn() { echo "[WARN] $*" >&2; }
log_error() { echo "[ERROR] $*" >&2; }
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}; }
uci_get_list() { uci -q get ${CONFIG}.$1 2>/dev/null; }
# Load configuration with defaults
load_config() {
@ -62,235 +61,133 @@ load_config() {
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)"
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)"
gpu_enabled="$(uci_get gpu.enabled || echo 0)"
gpu_backend="$(uci_get gpu.backend || echo vulkan)"
}
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
# Docker settings
docker_image="$(uci_get docker.image || echo $LOCALAI_IMAGE)"
has_lxc() {
command -v lxc-start >/dev/null 2>&1 && \
command -v lxc-stop >/dev/null 2>&1
}
# Ensure required packages are installed
ensure_packages() {
require_root
for pkg in "$@"; do
if ! opkg list-installed | grep -q "^$pkg "; then
if [ "$OPKG_UPDATED" -eq 0 ]; then
opkg update || return 1
OPKG_UPDATED=1
fi
opkg install "$pkg" || return 1
fi
done
# Ensure paths exist
[ -d "$data_path" ] || mkdir -p "$data_path"
[ -d "$models_path" ] || mkdir -p "$models_path"
}
# =============================================================================
# LXC CONTAINER FUNCTIONS
# CONTAINER RUNTIME DETECTION
# =============================================================================
lxc_check_prereqs() {
log_info "Checking LXC prerequisites..."
ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1
if [ ! -d /sys/fs/cgroup ]; then
log_error "cgroups not mounted at /sys/fs/cgroup"
return 1
fi
log_info "LXC ready"
}
lxc_create_rootfs() {
load_config
if [ -d "$LXC_ROOTFS" ] && [ -x "$LXC_ROOTFS/usr/bin/local-ai" ]; then
log_info "LXC rootfs already exists with LocalAI"
return 0
fi
log_info "Creating LXC rootfs for LocalAI..."
ensure_dir "$LXC_PATH/$LXC_NAME"
lxc_download_binary || return 1
lxc_create_config || return 1
log_info "LXC rootfs created successfully"
}
lxc_download_binary() {
local rootfs="$LXC_ROOTFS"
local arch
# Detect architecture - LocalAI uses lowercase format: local-ai-vX.X.X-linux-arm64
case "$(uname -m)" in
x86_64) arch="linux-x86_64" ;;
aarch64) arch="linux-arm64" ;;
armv7l) arch="linux-arm" ;;
*) arch="linux-x86_64" ;;
esac
log_info "Downloading LocalAI $LOCALAI_VERSION for $arch..."
ensure_dir "$rootfs/usr/bin"
ensure_dir "$rootfs/data"
ensure_dir "$rootfs/models"
ensure_dir "$rootfs/tmp"
ensure_dir "$rootfs/etc"
# Download LocalAI binary - format: local-ai-v3.10.0-linux-arm64
local binary_url="https://github.com/mudler/LocalAI/releases/download/${LOCALAI_VERSION}/local-ai-${LOCALAI_VERSION}-${arch}"
log_info "Downloading from: $binary_url"
if wget -q --show-progress -O "$rootfs/usr/bin/local-ai" "$binary_url"; then
chmod +x "$rootfs/usr/bin/local-ai"
log_info "LocalAI binary downloaded successfully ($(ls -sh "$rootfs/usr/bin/local-ai" | cut -d' ' -f1))"
# 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
log_error "Failed to download LocalAI binary"
log_error "URL: $binary_url"
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
# Create minimal rootfs structure
mkdir -p "$rootfs/bin" "$rootfs/lib" "$rootfs/proc" "$rootfs/sys" "$rootfs/dev"
# Create resolv.conf
echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf"
# Create startup script
cat > "$rootfs/usr/bin/start-localai.sh" << 'START'
#!/bin/sh
export PATH="/usr/bin:/bin:$PATH"
cd /data
# Read environment variables
API_PORT="${LOCALAI_API_PORT:-8080}"
API_HOST="${LOCALAI_API_HOST:-0.0.0.0}"
THREADS="${LOCALAI_THREADS:-4}"
CONTEXT_SIZE="${LOCALAI_CONTEXT_SIZE:-2048}"
DEBUG="${LOCALAI_DEBUG:-0}"
CORS="${LOCALAI_CORS:-1}"
GPU_ENABLED="${LOCALAI_GPU_ENABLED:-0}"
# Build args
ARGS="--address ${API_HOST}:${API_PORT}"
ARGS="$ARGS --models-path /models"
ARGS="$ARGS --threads $THREADS"
ARGS="$ARGS --context-size $CONTEXT_SIZE"
[ "$DEBUG" = "1" ] && ARGS="$ARGS --debug"
[ "$CORS" = "1" ] && ARGS="$ARGS --cors"
echo "Starting LocalAI..."
echo "API: http://${API_HOST}:${API_PORT}"
echo "Models path: /models"
echo "Threads: $THREADS, Context: $CONTEXT_SIZE"
exec /usr/bin/local-ai $ARGS
START
chmod +x "$rootfs/usr/bin/start-localai.sh"
log_info "LocalAI binary and startup script installed"
}
lxc_create_config() {
container_run() {
load_config
container_stop
# Build command line flags
local cors_flag=""
local debug_flag=""
[ "$cors" = "1" ] && cors_flag=" --cors"
[ "$debug" = "1" ] && debug_flag=" --debug"
log_info "Starting LocalAI container..."
log_info "Image: $docker_image"
log_info "API: http://${api_host}:${api_port}"
log_info "Models: $models_path"
cat > "$LXC_CONFIG" << EOF
# LocalAI LXC Configuration
lxc.uts.name = $LXC_NAME
# 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"
# Root filesystem
lxc.rootfs.path = dir:$LXC_ROOTFS
# Network - use host network for simplicity
lxc.net.0.type = none
# Mounts
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.mount.entry = $data_path data none bind,create=dir 0 0
lxc.mount.entry = $models_path models none bind,create=dir 0 0
# Environment variables for configuration
lxc.environment = LOCALAI_API_PORT=$api_port
lxc.environment = LOCALAI_API_HOST=$api_host
lxc.environment = LOCALAI_THREADS=$threads
lxc.environment = LOCALAI_CONTEXT_SIZE=$context_size
lxc.environment = LOCALAI_DEBUG=$debug
lxc.environment = LOCALAI_CORS=$cors
lxc.environment = LOCALAI_GPU_ENABLED=$gpu_enabled
# Capabilities
lxc.cap.drop = sys_admin sys_module mac_admin mac_override
# cgroups limits
lxc.cgroup.memory.limit_in_bytes = $memory_limit
# Run binary directly (no shell needed in minimal rootfs)
lxc.init.cmd = /usr/bin/local-ai --address ${api_host}:${api_port} --models-path /models --threads $threads --context-size $context_size${cors_flag}${debug_flag}
# Console
lxc.console.size = 4096
lxc.pty.max = 1024
EOF
log_info "LXC config created at $LXC_CONFIG"
# 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"
}
lxc_stop() {
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
lxc-stop -n "$LXC_NAME" -k >/dev/null 2>&1 || true
fi
}
lxc_run() {
container_status() {
load_config
lxc_stop
local runtime=$(detect_runtime)
if [ ! -f "$LXC_CONFIG" ]; then
log_error "LXC not configured. Run 'localaictl install' first."
return 1
fi
# Regenerate config to pick up any UCI changes
lxc_create_config
# Ensure mount points exist
ensure_dir "$data_path"
ensure_dir "$models_path"
log_info "Starting LocalAI LXC container..."
log_info "API endpoint: http://0.0.0.0:$api_port/v1"
log_info "Web UI: http://0.0.0.0:$api_port"
log_info "Models path: $models_path"
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
}
lxc_status() {
load_config
echo "=== LocalAI Status ==="
echo ""
echo "Container Runtime: ${runtime:-NOT FOUND}"
echo ""
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
lxc-info -n "$LXC_NAME"
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 "LXC container '$LXC_NAME' not found or not configured"
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"
@ -302,29 +199,31 @@ lxc_status() {
# 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
}
lxc_logs() {
container_logs() {
if [ "$1" = "-f" ]; then
logread -f -e localai
run_container logs -f "$CONTAINER_NAME"
else
logread -e localai | tail -100
run_container logs --tail 100 "$CONTAINER_NAME"
fi
}
lxc_shell() {
lxc-attach -n "$LXC_NAME" -- /bin/sh
}
lxc_destroy() {
lxc_stop
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
rm -rf "$LXC_PATH/$LXC_NAME"
log_info "LXC container destroyed"
fi
container_shell() {
run_container exec -it "$CONTAINER_NAME" /bin/sh
}
# =============================================================================
@ -346,12 +245,19 @@ cmd_models() {
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 phi2"
echo " localaictl model-install tinyllama"
echo " localaictl model-install phi2"
fi
else
echo " Models directory not found: $models_path"
@ -362,7 +268,7 @@ cmd_models() {
echo ""
# List presets from UCI config
uci show localai 2>/dev/null | grep "preset\[" | while read line; do
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")
@ -378,13 +284,13 @@ cmd_model_install() {
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: localaictl model-install <preset-name|url>"; return 1; }
ensure_dir "$models_path"
mkdir -p "$models_path"
# Check if it's a preset
local preset_url=""
local preset_file=""
# Search presets
# 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
@ -397,11 +303,12 @@ cmd_model_install() {
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 -O "$models_path/$preset_file" "$preset_url"; then
log_info "Model installed: $models_path/$preset_file"
if wget --show-progress -O "$models_path/$preset_file" "$preset_url"; then
log_info "Model downloaded: $models_path/$preset_file"
# Create model config YAML
# Create model config YAML for LocalAI
cat > "$models_path/$model_name.yaml" << EOF
name: $model_name
backend: llama-cpp
@ -411,6 +318,9 @@ 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
@ -420,7 +330,7 @@ EOF
local filename=$(basename "$model_name")
log_info "Downloading model from URL..."
if wget -O "$models_path/$filename" "$model_name"; then
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"
@ -428,13 +338,13 @@ EOF
fi
else
log_error "Unknown model or preset: $model_name"
# List available presets from UCI
local presets=""
for section in $(uci show localai 2>/dev/null | grep "=preset" | cut -d. -f2 | cut -d= -f1); do
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" ] && presets="$presets $pname"
[ -n "$pname" ] && echo " - $pname"
done
log_info "Available presets:$presets"
return 1
fi
}
@ -457,7 +367,20 @@ cmd_model_remove() {
fi
done
[ "$found" -eq 0 ] && log_warn "Model not found: $model_name"
# 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
}
# =============================================================================
@ -468,26 +391,34 @@ cmd_install() {
require_root
load_config
if ! has_lxc; then
log_error "LXC not available. Install lxc packages first."
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
log_info "Installing LocalAI..."
local runtime=$(detect_runtime)
log_info "Installing LocalAI using $runtime..."
log_info "Image: $docker_image"
# Create directories
ensure_dir "$data_path"
ensure_dir "$models_path"
mkdir -p "$data_path"
mkdir -p "$models_path"
lxc_check_prereqs || exit 1
lxc_create_rootfs || exit 1
# Pull the image
container_pull || exit 1
# Enable service
uci_set main.enabled '1'
/etc/init.d/localai enable
log_info "LocalAI installed."
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)"
@ -497,21 +428,48 @@ cmd_install() {
cmd_check() {
load_config
log_info "Checking prerequisites..."
if has_lxc; then
log_info "LXC: available"
lxc_check_prereqs
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
log_warn "LXC: not available"
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))
log_info "System memory: ${mem_gb}GB"
echo "System memory: ${mem_gb}GB"
if [ "$mem_gb" -lt 2 ]; then
log_warn "Low memory! LocalAI requires at least 2GB RAM for most models"
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
}
@ -520,44 +478,52 @@ cmd_update() {
load_config
log_info "Updating LocalAI..."
lxc_destroy
lxc_create_rootfs || exit 1
if /etc/init.d/localai enabled >/dev/null 2>&1; then
# 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. Restart manually to apply."
log_info "Update complete. Start manually with: /etc/init.d/localai start"
fi
}
cmd_status() {
lxc_status
container_status
}
cmd_logs() {
lxc_logs "$@"
container_logs "$@"
}
cmd_shell() {
lxc_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_lxc; then
log_error "LXC not available"
if ! has_runtime; then
log_error "No container runtime found"
exit 1
fi
lxc_check_prereqs || exit 1
lxc_run
container_run
}
cmd_service_stop() {
require_root
lxc_stop
container_stop
}
# Main Entry Point

View File

@ -0,0 +1,76 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-ollama
PKG_RELEASE:=1
PKG_VERSION:=0.1.0
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-ollama
SECTION:=utils
CATEGORY:=Utilities
PKGARCH:=all
SUBMENU:=SecuBox Apps
TITLE:=SecuBox Ollama - Local LLM Runtime
DEPENDS:=+uci +libuci +jsonfilter +wget-ssl
endef
define Package/secubox-app-ollama/description
Ollama - Simple local LLM runtime for SecuBox-powered OpenWrt systems.
Features:
- Easy model management (ollama pull, ollama run)
- OpenAI-compatible API
- Native ARM64 support with backends included
- Lightweight compared to LocalAI
- Support for LLaMA, Mistral, Phi, Gemma models
Runs in Docker/Podman container.
Configure in /etc/config/ollama.
endef
define Package/secubox-app-ollama/conffiles
/etc/config/ollama
endef
define Build/Compile
endef
define Package/secubox-app-ollama/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/ollama $(1)/etc/config/ollama
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/ollama $(1)/etc/init.d/ollama
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/ollamactl $(1)/usr/sbin/ollamactl
endef
define Package/secubox-app-ollama/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
echo ""
echo "Ollama installed."
echo ""
echo "Prerequisites: Install podman or docker first"
echo " opkg install podman"
echo ""
echo "To install and start Ollama:"
echo " ollamactl install # Pull Docker image (~1GB)"
echo " /etc/init.d/ollama start"
echo ""
echo "API endpoint: http://<router-ip>:11434/api"
echo ""
echo "Download and run models:"
echo " ollamactl pull tinyllama"
echo " ollamactl run tinyllama"
echo ""
}
exit 0
endef
$(eval $(call BuildPackage,secubox-app-ollama))

View File

@ -0,0 +1,50 @@
config main 'main'
option enabled '0'
option api_port '11434'
option api_host '0.0.0.0'
option data_path '/srv/ollama'
option memory_limit '2g'
# Docker/Podman settings
config docker 'docker'
option image 'ollama/ollama:latest'
# Default model to pull on install
config model 'default'
option enabled '1'
option name 'tinyllama'
# Available models (informational - managed by Ollama)
# Use: ollamactl pull <model> to download
# Lightweight models (< 2GB)
config model_info 'tinyllama'
option name 'tinyllama'
option size '637M'
option description 'TinyLlama 1.1B - Ultra-lightweight, fast responses'
config model_info 'phi'
option name 'phi'
option size '1.6G'
option description 'Microsoft Phi-2 - Small but capable'
config model_info 'gemma'
option name 'gemma:2b'
option size '1.4G'
option description 'Google Gemma 2B - Efficient and modern'
# Medium models (2-5GB)
config model_info 'mistral'
option name 'mistral'
option size '4.1G'
option description 'Mistral 7B - High quality general assistant'
config model_info 'llama2'
option name 'llama2'
option size '3.8G'
option description 'Meta LLaMA 2 7B - Popular general model'
config model_info 'codellama'
option name 'codellama'
option size '3.8G'
option description 'Code LLaMA - Specialized for coding tasks'

View File

@ -0,0 +1,40 @@
#!/bin/sh /etc/rc.common
# SecuBox Ollama - Local LLM runtime
# Copyright (C) 2025 CyberMind.fr
START=95
STOP=10
USE_PROCD=1
PROG=/usr/sbin/ollamactl
start_service() {
local enabled
config_load ollama
config_get enabled main enabled '0'
[ "$enabled" = "1" ] || {
echo "Ollama is disabled. Enable with: uci set ollama.main.enabled=1"
return 0
}
procd_open_instance
procd_set_param command $PROG service-run
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
stop_service() {
$PROG service-stop
}
service_triggers() {
procd_add_reload_trigger "ollama"
}
reload_service() {
stop
start
}

View File

@ -0,0 +1,462 @@
#!/bin/sh
# SecuBox Ollama manager - Docker/Podman container support
# Copyright (C) 2025 CyberMind.fr
#
# Ollama is a simpler alternative to LocalAI with better ARM64 support
CONFIG="ollama"
CONTAINER_NAME="ollama"
OLLAMA_IMAGE="ollama/ollama:latest"
# Paths
DATA_PATH="/srv/ollama"
usage() {
cat <<'EOF'
Usage: ollamactl <command>
Commands:
install Pull Docker image and setup Ollama
check Run prerequisite checks
update Update Ollama Docker image
status Show container and service status
logs Show Ollama logs (use -f to follow)
shell Open shell in container
Model Management:
list List downloaded models
pull <model> Download a model (e.g., tinyllama, mistral, llama2)
rm <model> Remove a model
run <model> Interactive chat with a model (CLI)
Service Control:
service-run Internal: run container under procd
service-stop Stop container
API Endpoints (default port 11434):
/api/generate - Generate text completion
/api/chat - Chat completion
/api/embeddings - Generate embeddings
/api/tags - List models
Configuration: /etc/config/ollama
EOF
}
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
log_info() { echo "[INFO] $*"; logger -t ollama "$*"; }
log_warn() { echo "[WARN] $*" >&2; logger -t ollama -p warning "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t ollama -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 11434)"
api_host="$(uci_get main.api_host || echo 0.0.0.0)"
data_path="$(uci_get main.data_path || echo /srv/ollama)"
memory_limit="$(uci_get main.memory_limit || echo 2g)"
# Docker settings
docker_image="$(uci_get docker.image || echo $OLLAMA_IMAGE)"
# Ensure paths exist
[ -d "$data_path" ] || mkdir -p "$data_path"
}
# =============================================================================
# CONTAINER RUNTIME DETECTION
# =============================================================================
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 Ollama Docker image: $docker_image"
log_info "This may take a few minutes (~1GB)..."
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 Ollama container..."
log_info "Image: $docker_image"
log_info "API: http://${api_host}:${api_port}"
log_info "Data: $data_path"
# Run container in foreground (for procd)
exec run_container run --rm \
--name "$CONTAINER_NAME" \
-p "${api_port}:11434" \
-v "${data_path}:/root/.ollama:rw" \
--memory="$memory_limit" \
"$docker_image"
}
container_status() {
load_config
local runtime=$(detect_runtime)
echo "=== Ollama 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 "Memory limit: $memory_limit"
echo ""
# Check API health
if wget -q -O /dev/null "http://127.0.0.1:$api_port" 2>/dev/null; then
echo "API Status: HEALTHY"
# List models via API
echo ""
echo "=== Downloaded Models ==="
local models=$(wget -q -O - "http://127.0.0.1:$api_port/api/tags" 2>/dev/null)
if [ -n "$models" ]; then
echo "$models" | jsonfilter -e '@.models[*].name' 2>/dev/null | while read model; do
echo " - $model"
done
else
echo " No models downloaded yet"
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 (via Ollama API/CLI)
# =============================================================================
cmd_list() {
load_config
if container_running; then
# Use API to list models
local models=$(wget -q -O - "http://127.0.0.1:$api_port/api/tags" 2>/dev/null)
if [ -n "$models" ]; then
echo "=== Downloaded Models ==="
echo ""
echo "$models" | jsonfilter -e '@.models[*].name' 2>/dev/null | while read model; do
# Get size from same response
local size=$(echo "$models" | jsonfilter -e "@.models[@.name='$model'].size" 2>/dev/null)
if [ -n "$size" ]; then
# Convert to human readable
local size_gb=$(echo "scale=2; $size / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "?")
echo " $model (${size_gb}GB)"
else
echo " $model"
fi
done
else
echo "No models downloaded yet"
echo ""
echo "Download a model with:"
echo " ollamactl pull tinyllama"
fi
else
log_error "Ollama not running. Start with: /etc/init.d/ollama start"
return 1
fi
echo ""
echo "=== Available Models (popular) ==="
echo " tinyllama - 637MB - Ultra-lightweight"
echo " phi - 1.6GB - Microsoft Phi-2"
echo " gemma:2b - 1.4GB - Google Gemma 2B"
echo " mistral - 4.1GB - High quality assistant"
echo " llama2 - 3.8GB - Meta LLaMA 2 7B"
echo " codellama - 3.8GB - Code specialized"
echo ""
echo "More at: https://ollama.com/library"
}
cmd_pull() {
load_config
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: ollamactl pull <model-name>"; return 1; }
if ! container_running; then
log_error "Ollama not running. Start with: /etc/init.d/ollama start"
return 1
fi
log_info "Pulling model: $model_name"
log_info "This may take several minutes depending on model size..."
# Use exec to run ollama pull inside container
run_container exec "$CONTAINER_NAME" ollama pull "$model_name"
}
cmd_rm() {
load_config
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: ollamactl rm <model-name>"; return 1; }
if ! container_running; then
log_error "Ollama not running. Start with: /etc/init.d/ollama start"
return 1
fi
log_info "Removing model: $model_name"
run_container exec "$CONTAINER_NAME" ollama rm "$model_name"
}
cmd_run() {
load_config
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: ollamactl run <model-name>"; return 1; }
if ! container_running; then
log_error "Ollama not running. Start with: /etc/init.d/ollama start"
return 1
fi
log_info "Starting chat with: $model_name"
log_info "Type your message and press Enter. Use Ctrl+D to exit."
echo ""
# Interactive chat
run_container exec -it "$CONTAINER_NAME" ollama run "$model_name"
}
# =============================================================================
# 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 Ollama using $runtime..."
log_info "Image: $docker_image"
# Create directories
mkdir -p "$data_path"
# Pull the image
container_pull || exit 1
# Enable service
uci_set main.enabled '1'
/etc/init.d/ollama enable
log_info ""
log_info "Ollama installed successfully!"
log_info ""
log_info "Start with: /etc/init.d/ollama start"
log_info "API endpoint: http://<router-ip>:$api_port/api"
log_info ""
log_info "Download and use a model:"
log_info " ollamactl pull tinyllama"
log_info " ollamactl run tinyllama"
}
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: Ollama image requires ~1GB"
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! Ollama 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 "ollama"; then
echo "[OK] Ollama image found"
else
echo "[INFO] Ollama image not downloaded yet"
echo " Run: ollamactl install"
fi
fi
}
cmd_update() {
require_root
load_config
log_info "Updating Ollama..."
# 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/ollama restart
else
log_info "Update complete. Start manually with: /etc/init.d/ollama 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/ollama 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 "$@" ;;
list) shift; cmd_list "$@" ;;
pull) shift; cmd_pull "$@" ;;
rm) shift; cmd_rm "$@" ;;
run) shift; cmd_run "$@" ;;
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