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:
parent
63c0bb3e5a
commit
b245fdb3e7
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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"
|
||||
else
|
||||
echo "LXC container '$LXC_NAME' not found or not configured"
|
||||
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"
|
||||
@ -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
|
||||
|
||||
76
package/secubox/secubox-app-ollama/Makefile
Normal file
76
package/secubox/secubox-app-ollama/Makefile
Normal 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))
|
||||
50
package/secubox/secubox-app-ollama/files/etc/config/ollama
Normal file
50
package/secubox/secubox-app-ollama/files/etc/config/ollama
Normal 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'
|
||||
40
package/secubox/secubox-app-ollama/files/etc/init.d/ollama
Normal file
40
package/secubox/secubox-app-ollama/files/etc/init.d/ollama
Normal 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
|
||||
}
|
||||
462
package/secubox/secubox-app-ollama/files/usr/sbin/ollamactl
Normal file
462
package/secubox/secubox-app-ollama/files/usr/sbin/ollamactl
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user