feat(localai): Rewrite secubox-app-localai with native binary download
- Replace Docker/LXC-based approach with direct binary download - Download LocalAI v2.25.0 binary from GitHub releases - Add localaictl CLI for install, model management, and service control - Change default port to 8081 (avoid CrowdSec conflict on 8080) - Remove secubox-app-localai-wb (merged into secubox-app-localai) - Add model presets: tinyllama, phi2, mistral Usage: localaictl install localaictl model-install tinyllama /etc/init.d/localai enable && /etc/init.d/localai start Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e50dcf6aee
commit
612a1be6ea
@ -1,82 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
#
|
||||
# LocalAI-WB - LocalAI With Build support
|
||||
# Management scripts + option to build from source via toolchain
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-localai-wb
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_ARCH:=all
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-localai-wb
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
PKGARCH:=all
|
||||
SUBMENU:=SecuBox Apps
|
||||
TITLE:=LocalAI-WB - Build from source with llama-cpp
|
||||
DEPENDS:=+uci +libuci +jsonfilter +wget-ssl
|
||||
endef
|
||||
|
||||
define Package/secubox-app-localai-wb/description
|
||||
LocalAI management package with native build support.
|
||||
|
||||
Provides tools to:
|
||||
- Build LocalAI from source with llama-cpp backend
|
||||
- Cross-compile via OpenWrt toolchain
|
||||
- Manage models and service
|
||||
|
||||
For ARM64: Compiles llama-cpp backend natively.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-localai-wb/conffiles
|
||||
/etc/config/localai-wb
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-app-localai-wb/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/etc/config/localai-wb $(1)/etc/config/localai-wb
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/etc/init.d/localai-wb $(1)/etc/init.d/localai-wb
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/localai-wb-ctl $(1)/usr/sbin/localai-wb-ctl
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/localai-wb
|
||||
$(INSTALL_BIN) ./files/usr/share/localai-wb/build-sdk.sh $(1)/usr/share/localai-wb/build-sdk.sh
|
||||
|
||||
$(INSTALL_DIR) $(1)/opt/localai/bin
|
||||
$(INSTALL_DIR) $(1)/srv/localai/models
|
||||
endef
|
||||
|
||||
define Package/secubox-app-localai-wb/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
echo ""
|
||||
echo "LocalAI-WB installed."
|
||||
echo ""
|
||||
echo "Check prerequisites:"
|
||||
echo " localai-wb-ctl check"
|
||||
echo ""
|
||||
echo "Build from source:"
|
||||
echo " localai-wb-ctl build"
|
||||
echo ""
|
||||
echo "Or cross-compile with SDK (see /usr/share/localai-wb/build-sdk.sh)"
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-localai-wb))
|
||||
@ -1,36 +0,0 @@
|
||||
config main 'main'
|
||||
option enabled '0'
|
||||
option installed '0'
|
||||
option api_port '8080'
|
||||
option api_host '0.0.0.0'
|
||||
option data_path '/srv/localai'
|
||||
option models_path '/srv/localai/models'
|
||||
option threads '4'
|
||||
option context_size '2048'
|
||||
option debug '0'
|
||||
option cors '1'
|
||||
|
||||
# Build settings
|
||||
config build 'build'
|
||||
option version 'v2.25.0'
|
||||
option build_type 'generic'
|
||||
option backends 'llama-cpp'
|
||||
|
||||
# Model presets
|
||||
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 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'
|
||||
option size '1.6G'
|
||||
option description 'Microsoft Phi-2 - Compact and efficient'
|
||||
|
||||
config preset 'mistral'
|
||||
option name 'mistral-7b'
|
||||
option url 'https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf'
|
||||
option size '4.1G'
|
||||
option description 'Mistral 7B Instruct - High quality'
|
||||
@ -1,76 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# LocalAI-WB init script
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/opt/localai/bin/local-ai
|
||||
ALT_PROG=/usr/bin/local-ai-wb
|
||||
CONFIG=localai-wb
|
||||
|
||||
start_service() {
|
||||
local enabled
|
||||
config_load "$CONFIG"
|
||||
config_get enabled main enabled '0'
|
||||
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
# Find binary
|
||||
local binary=""
|
||||
if [ -x "$PROG" ]; then
|
||||
binary="$PROG"
|
||||
elif [ -x "$ALT_PROG" ]; then
|
||||
binary="$ALT_PROG"
|
||||
elif [ -x "/usr/bin/local-ai" ]; then
|
||||
binary="/usr/bin/local-ai"
|
||||
else
|
||||
logger -t localai-wb -p err "LocalAI binary not found. Run: localai-wb-ctl build"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Load configuration
|
||||
local api_port api_host models_path threads context_size debug cors
|
||||
config_get api_port main api_port '8080'
|
||||
config_get api_host main api_host '0.0.0.0'
|
||||
config_get models_path main models_path '/srv/localai/models'
|
||||
config_get threads main threads '4'
|
||||
config_get context_size main context_size '2048'
|
||||
config_get debug main debug '0'
|
||||
config_get cors main cors '1'
|
||||
|
||||
# Ensure models directory exists
|
||||
mkdir -p "$models_path"
|
||||
|
||||
# Build command arguments
|
||||
local args="--address ${api_host}:${api_port}"
|
||||
args="$args --models-path $models_path"
|
||||
args="$args --threads $threads"
|
||||
args="$args --context-size $context_size"
|
||||
[ "$cors" = "1" ] && args="$args --cors"
|
||||
[ "$debug" = "1" ] && args="$args --debug"
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$binary" $args
|
||||
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/localai-wb.pid
|
||||
procd_close_instance
|
||||
|
||||
logger -t localai-wb "Started LocalAI on ${api_host}:${api_port}"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
logger -t localai-wb "Stopping LocalAI"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONFIG"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
@ -1,568 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SecuBox LocalAI-WB (With Build) - Compile from source with llama-cpp backend
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
#
|
||||
# This package builds LocalAI natively on ARM64 with llama-cpp backend
|
||||
|
||||
CONFIG="localai-wb"
|
||||
LOCALAI_VERSION="v2.25.0"
|
||||
BUILD_DIR="/opt/localai-build"
|
||||
INSTALL_DIR="/opt/localai"
|
||||
DATA_DIR="/srv/localai"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: localai-wb-ctl <command>
|
||||
|
||||
Build Commands:
|
||||
check Check build prerequisites
|
||||
install-deps Install build dependencies
|
||||
build Build LocalAI with llama-cpp backend
|
||||
build-minimal Build LocalAI with minimal backends (faster)
|
||||
clean Clean build directory
|
||||
|
||||
Service Commands:
|
||||
start Start LocalAI service
|
||||
stop Stop LocalAI service
|
||||
restart Restart LocalAI service
|
||||
status Show service status
|
||||
logs Show logs (use -f to follow)
|
||||
|
||||
Model Commands:
|
||||
models List installed models
|
||||
model-install <n> Install model from preset or URL
|
||||
model-remove <n> Remove installed model
|
||||
|
||||
Backend Commands:
|
||||
backends List available backends
|
||||
backend-install <n> Install additional backend
|
||||
|
||||
This package compiles LocalAI from source with llama-cpp backend.
|
||||
Requires: ~4GB RAM, ~10GB storage, 30-60 min build time.
|
||||
|
||||
Configuration: /etc/config/localai-wb
|
||||
EOF
|
||||
}
|
||||
|
||||
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
|
||||
|
||||
log_info() { echo "[INFO] $*"; logger -t localai-wb "$*"; }
|
||||
log_warn() { echo "[WARN] $*" >&2; logger -t localai-wb -p warning "$*"; }
|
||||
log_error() { echo "[ERROR] $*" >&2; logger -t localai-wb -p err "$*"; }
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||
|
||||
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 $DATA_DIR)"
|
||||
models_path="$(uci_get main.models_path || echo $DATA_DIR/models)"
|
||||
threads="$(uci_get main.threads || echo 4)"
|
||||
context_size="$(uci_get main.context_size || echo 2048)"
|
||||
debug="$(uci_get main.debug || echo 0)"
|
||||
|
||||
mkdir -p "$data_path" "$models_path"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# BUILD PREREQUISITES
|
||||
# =============================================================================
|
||||
|
||||
cmd_check() {
|
||||
echo "=== LocalAI Build Prerequisites Check ==="
|
||||
echo ""
|
||||
|
||||
local all_ok=1
|
||||
|
||||
# Check Go
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
local go_ver=$(go version | grep -oE 'go[0-9]+\.[0-9]+' | head -1)
|
||||
echo "[OK] Go: $go_ver"
|
||||
else
|
||||
echo "[FAIL] Go not installed"
|
||||
echo " Install: opkg install golang"
|
||||
all_ok=0
|
||||
fi
|
||||
|
||||
# Check Git
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
echo "[OK] Git installed"
|
||||
else
|
||||
echo "[FAIL] Git not installed"
|
||||
echo " Install: opkg install git git-http"
|
||||
all_ok=0
|
||||
fi
|
||||
|
||||
# Check make
|
||||
if command -v make >/dev/null 2>&1; then
|
||||
echo "[OK] Make installed"
|
||||
else
|
||||
echo "[FAIL] Make not installed"
|
||||
echo " Install: opkg install make"
|
||||
all_ok=0
|
||||
fi
|
||||
|
||||
# Check gcc/g++
|
||||
if command -v gcc >/dev/null 2>&1 && command -v g++ >/dev/null 2>&1; then
|
||||
echo "[OK] GCC/G++ installed"
|
||||
else
|
||||
echo "[FAIL] GCC/G++ not installed"
|
||||
echo " Install: opkg install gcc g++"
|
||||
all_ok=0
|
||||
fi
|
||||
|
||||
# Check cmake
|
||||
if command -v cmake >/dev/null 2>&1; then
|
||||
echo "[OK] CMake installed"
|
||||
else
|
||||
echo "[WARN] CMake not installed (optional)"
|
||||
echo " Install: opkg install cmake"
|
||||
fi
|
||||
|
||||
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! Build needs at least 2GB RAM"
|
||||
echo " Consider using swap or building on another machine"
|
||||
else
|
||||
echo "[OK] Memory sufficient for build"
|
||||
fi
|
||||
|
||||
# Check storage
|
||||
local storage_avail=$(df -m /opt 2>/dev/null | tail -1 | awk '{print $4}')
|
||||
if [ -n "$storage_avail" ]; then
|
||||
local storage_gb=$((storage_avail / 1024))
|
||||
echo "Storage available: ${storage_gb}GB at /opt"
|
||||
if [ "$storage_avail" -lt 5000 ]; then
|
||||
echo "[WARN] Low storage! Build needs ~10GB"
|
||||
else
|
||||
echo "[OK] Storage sufficient"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check architecture
|
||||
local arch=$(uname -m)
|
||||
echo "Architecture: $arch"
|
||||
case "$arch" in
|
||||
aarch64) echo "[OK] ARM64 supported" ;;
|
||||
x86_64) echo "[OK] x86_64 supported" ;;
|
||||
*) echo "[WARN] Architecture may have limited support" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
if [ $all_ok -eq 1 ]; then
|
||||
echo "Ready to build! Run: localai-wb-ctl build"
|
||||
else
|
||||
echo "Install missing dependencies first: localai-wb-ctl install-deps"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_install_deps() {
|
||||
require_root
|
||||
log_info "Installing build dependencies..."
|
||||
|
||||
opkg update
|
||||
|
||||
# Core build tools
|
||||
opkg install git git-http make gcc g++ cmake
|
||||
|
||||
# Go compiler
|
||||
opkg install golang
|
||||
|
||||
# Additional libraries
|
||||
opkg install libc libstdcpp libpthread
|
||||
|
||||
log_info "Dependencies installed. Run: localai-wb-ctl check"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# BUILD LOCALAI FROM SOURCE
|
||||
# =============================================================================
|
||||
|
||||
cmd_build() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
log_info "Building LocalAI from source with llama-cpp backend..."
|
||||
log_info "This will take 30-60 minutes. Go get coffee!"
|
||||
echo ""
|
||||
|
||||
# Create build directory
|
||||
mkdir -p "$BUILD_DIR"
|
||||
cd "$BUILD_DIR"
|
||||
|
||||
# Clone or update repository
|
||||
if [ -d "$BUILD_DIR/LocalAI" ]; then
|
||||
log_info "Updating existing repository..."
|
||||
cd "$BUILD_DIR/LocalAI"
|
||||
git fetch --all
|
||||
git checkout "$LOCALAI_VERSION" 2>/dev/null || git checkout main
|
||||
git pull || true
|
||||
else
|
||||
log_info "Cloning LocalAI repository..."
|
||||
git clone --depth 1 --branch "$LOCALAI_VERSION" https://github.com/mudler/LocalAI.git 2>/dev/null || \
|
||||
git clone https://github.com/mudler/LocalAI.git
|
||||
cd "$BUILD_DIR/LocalAI"
|
||||
git checkout "$LOCALAI_VERSION" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_info "Repository ready at $BUILD_DIR/LocalAI"
|
||||
log_info ""
|
||||
log_info "Starting build with llama-cpp backend..."
|
||||
log_info "Build options:"
|
||||
log_info " - Backend: llama-cpp (GGUF models)"
|
||||
log_info " - Type: generic CPU"
|
||||
log_info ""
|
||||
|
||||
# Set build environment
|
||||
export CGO_ENABLED=1
|
||||
export GOFLAGS="-mod=mod"
|
||||
|
||||
# Build with llama-cpp backend only (fastest)
|
||||
BUILD_GRPC_FOR_BACKEND_LLAMA=true \
|
||||
GRPC_BACKENDS="backend-assets/grpc/llama-cpp" \
|
||||
BUILD_TYPE=generic \
|
||||
make build 2>&1 | tee "$BUILD_DIR/build.log"
|
||||
|
||||
if [ -f "$BUILD_DIR/LocalAI/local-ai" ]; then
|
||||
log_info "Build successful!"
|
||||
|
||||
# Install binary
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
cp "$BUILD_DIR/LocalAI/local-ai" "$INSTALL_DIR/bin/"
|
||||
chmod +x "$INSTALL_DIR/bin/local-ai"
|
||||
|
||||
# Copy backend assets
|
||||
if [ -d "$BUILD_DIR/LocalAI/backend-assets" ]; then
|
||||
cp -r "$BUILD_DIR/LocalAI/backend-assets" "$INSTALL_DIR/"
|
||||
fi
|
||||
|
||||
# Create symlink
|
||||
ln -sf "$INSTALL_DIR/bin/local-ai" /usr/bin/local-ai
|
||||
|
||||
log_info ""
|
||||
log_info "LocalAI installed to $INSTALL_DIR"
|
||||
log_info "Binary: $INSTALL_DIR/bin/local-ai"
|
||||
log_info ""
|
||||
log_info "Enable and start service:"
|
||||
log_info " uci set localai-wb.main.enabled=1 && uci commit"
|
||||
log_info " /etc/init.d/localai-wb start"
|
||||
log_info ""
|
||||
log_info "Check backends:"
|
||||
log_info " local-ai backends list"
|
||||
|
||||
uci_set main.installed '1'
|
||||
else
|
||||
log_error "Build failed! Check $BUILD_DIR/build.log"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_build_minimal() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
log_info "Building LocalAI with minimal configuration..."
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
cd "$BUILD_DIR"
|
||||
|
||||
if [ ! -d "$BUILD_DIR/LocalAI" ]; then
|
||||
log_info "Cloning repository..."
|
||||
git clone --depth 1 https://github.com/mudler/LocalAI.git
|
||||
fi
|
||||
|
||||
cd "$BUILD_DIR/LocalAI"
|
||||
|
||||
# Minimal build - just the core with llama-cpp
|
||||
log_info "Building minimal LocalAI (llama-cpp only)..."
|
||||
|
||||
export CGO_ENABLED=1
|
||||
|
||||
# Build only llama-cpp grpc backend
|
||||
make BUILD_TYPE=generic \
|
||||
BUILD_GRPC_FOR_BACKEND_LLAMA=true \
|
||||
GRPC_BACKENDS="backend-assets/grpc/llama-cpp" \
|
||||
build 2>&1 | tee "$BUILD_DIR/build-minimal.log"
|
||||
|
||||
if [ -f "$BUILD_DIR/LocalAI/local-ai" ]; then
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
cp "$BUILD_DIR/LocalAI/local-ai" "$INSTALL_DIR/bin/"
|
||||
chmod +x "$INSTALL_DIR/bin/local-ai"
|
||||
ln -sf "$INSTALL_DIR/bin/local-ai" /usr/bin/local-ai
|
||||
|
||||
log_info "Minimal build complete!"
|
||||
uci_set main.installed '1'
|
||||
else
|
||||
log_error "Build failed!"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_clean() {
|
||||
require_root
|
||||
log_info "Cleaning build directory..."
|
||||
rm -rf "$BUILD_DIR"
|
||||
log_info "Build directory cleaned"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SERVICE MANAGEMENT
|
||||
# =============================================================================
|
||||
|
||||
is_running() {
|
||||
pgrep -f "$INSTALL_DIR/bin/local-ai" >/dev/null 2>&1 || \
|
||||
pgrep -x "local-ai" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
if ! [ -x "$INSTALL_DIR/bin/local-ai" ]; then
|
||||
log_error "LocalAI not installed. Run: localai-wb-ctl build"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if is_running; then
|
||||
log_warn "Already running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Starting LocalAI..."
|
||||
/etc/init.d/localai-wb start
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
require_root
|
||||
/etc/init.d/localai-wb stop
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
require_root
|
||||
/etc/init.d/localai-wb restart
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
load_config
|
||||
|
||||
echo "=== LocalAI-WB Status ==="
|
||||
echo ""
|
||||
|
||||
if [ -x "$INSTALL_DIR/bin/local-ai" ]; then
|
||||
echo "Installation: INSTALLED"
|
||||
echo "Binary: $INSTALL_DIR/bin/local-ai"
|
||||
local version=$("$INSTALL_DIR/bin/local-ai" --version 2>/dev/null | head -1 || echo "unknown")
|
||||
echo "Version: $version"
|
||||
else
|
||||
echo "Installation: NOT INSTALLED"
|
||||
echo "Run: localai-wb-ctl build"
|
||||
return
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if is_running; then
|
||||
echo "Service: RUNNING"
|
||||
local pid=$(pgrep -f "$INSTALL_DIR/bin/local-ai" | head -1)
|
||||
echo "PID: $pid"
|
||||
else
|
||||
echo "Service: STOPPED"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Configuration:"
|
||||
echo " API: http://${api_host}:${api_port}"
|
||||
echo " Models: $models_path"
|
||||
echo " Threads: $threads"
|
||||
echo " Context: $context_size"
|
||||
|
||||
echo ""
|
||||
|
||||
# Check API health
|
||||
if is_running; then
|
||||
if wget -q -O /dev/null "http://127.0.0.1:$api_port/readyz" 2>/dev/null; then
|
||||
echo "API Status: HEALTHY"
|
||||
else
|
||||
echo "API Status: NOT RESPONDING"
|
||||
fi
|
||||
fi
|
||||
|
||||
# List backends
|
||||
if [ -x "$INSTALL_DIR/bin/local-ai" ]; then
|
||||
echo ""
|
||||
echo "=== Backends ==="
|
||||
"$INSTALL_DIR/bin/local-ai" backends list 2>/dev/null || echo " (service not running)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
if [ "$1" = "-f" ]; then
|
||||
logread -f -e localai-wb
|
||||
else
|
||||
logread -e localai-wb | tail -100
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# BACKEND MANAGEMENT
|
||||
# =============================================================================
|
||||
|
||||
cmd_backends() {
|
||||
if [ -x "$INSTALL_DIR/bin/local-ai" ]; then
|
||||
"$INSTALL_DIR/bin/local-ai" backends list
|
||||
else
|
||||
log_error "LocalAI not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_backend_install() {
|
||||
local backend="$1"
|
||||
[ -z "$backend" ] && { echo "Usage: localai-wb-ctl backend-install <backend>"; return 1; }
|
||||
|
||||
if [ -x "$INSTALL_DIR/bin/local-ai" ]; then
|
||||
log_info "Installing backend: $backend"
|
||||
"$INSTALL_DIR/bin/local-ai" backends install "$backend"
|
||||
else
|
||||
log_error "LocalAI not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# 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; do
|
||||
[ -f "$model" ] || continue
|
||||
count=$((count + 1))
|
||||
local name=$(basename "$model")
|
||||
local size=$(ls -lh "$model" | awk '{print $5}')
|
||||
echo " $count. $name ($size)"
|
||||
done
|
||||
[ "$count" -eq 0 ] && echo " No models installed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Available Presets ==="
|
||||
echo " tinyllama - 669MB - TinyLlama 1.1B"
|
||||
echo " phi2 - 1.6GB - Microsoft Phi-2"
|
||||
echo " mistral - 4.1GB - Mistral 7B Instruct"
|
||||
echo ""
|
||||
echo "Install: localai-wb-ctl model-install <name>"
|
||||
}
|
||||
|
||||
cmd_model_install() {
|
||||
load_config
|
||||
require_root
|
||||
|
||||
local model_name="$1"
|
||||
[ -z "$model_name" ] && { echo "Usage: localai-wb-ctl model-install <name|url>"; return 1; }
|
||||
|
||||
mkdir -p "$models_path"
|
||||
|
||||
# Preset URLs
|
||||
case "$model_name" in
|
||||
tinyllama)
|
||||
local url="https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
|
||||
local filename="tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
|
||||
;;
|
||||
phi2)
|
||||
local url="https://huggingface.co/TheBloke/phi-2-GGUF/resolve/main/phi-2.Q4_K_M.gguf"
|
||||
local filename="phi-2.Q4_K_M.gguf"
|
||||
;;
|
||||
mistral)
|
||||
local url="https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf"
|
||||
local filename="mistral-7b-instruct-v0.2.Q4_K_M.gguf"
|
||||
;;
|
||||
http*)
|
||||
local url="$model_name"
|
||||
local filename=$(basename "$url")
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown model: $model_name"
|
||||
log_error "Use preset name or full URL"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
log_info "Downloading: $filename"
|
||||
log_info "URL: $url"
|
||||
|
||||
if wget --show-progress -O "$models_path/$filename" "$url"; then
|
||||
# Create YAML config for the model
|
||||
local model_id="${filename%.*}"
|
||||
cat > "$models_path/$model_id.yaml" << EOF
|
||||
name: $model_id
|
||||
backend: llama-cpp
|
||||
parameters:
|
||||
model: $filename
|
||||
context_size: $context_size
|
||||
threads: $threads
|
||||
EOF
|
||||
log_info "Model installed: $model_id"
|
||||
log_info "Restart service to load: /etc/init.d/localai-wb restart"
|
||||
else
|
||||
log_error "Download failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_model_remove() {
|
||||
load_config
|
||||
require_root
|
||||
|
||||
local model_name="$1"
|
||||
[ -z "$model_name" ] && { echo "Usage: localai-wb-ctl model-remove <name>"; return 1; }
|
||||
|
||||
local found=0
|
||||
for ext in gguf bin yaml yml; do
|
||||
if [ -f "$models_path/$model_name.$ext" ]; then
|
||||
rm -f "$models_path/$model_name.$ext"
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
|
||||
for file in "$models_path"/*"$model_name"*; do
|
||||
[ -f "$file" ] && rm -f "$file" && found=1
|
||||
done
|
||||
|
||||
[ $found -eq 1 ] && log_info "Model removed: $model_name" || log_warn "Model not found"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
|
||||
case "${1:-}" in
|
||||
check) cmd_check ;;
|
||||
install-deps) cmd_install_deps ;;
|
||||
build) cmd_build ;;
|
||||
build-minimal) cmd_build_minimal ;;
|
||||
clean) cmd_clean ;;
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
status) cmd_status ;;
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
backends) cmd_backends ;;
|
||||
backend-install) shift; cmd_backend_install "$@" ;;
|
||||
models) cmd_models ;;
|
||||
model-install) shift; cmd_model_install "$@" ;;
|
||||
model-remove) shift; cmd_model_remove "$@" ;;
|
||||
help|--help|-h|'') usage ;;
|
||||
*) echo "Unknown: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
@ -1,279 +0,0 @@
|
||||
#!/bin/bash
|
||||
# LocalAI Cross-Compile Script for OpenWrt SDK
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
#
|
||||
# Run this on your build machine (Linux x86_64) with OpenWrt SDK
|
||||
# The resulting binary can be copied to your ARM64 OpenWrt device
|
||||
|
||||
set -e
|
||||
|
||||
LOCALAI_VERSION="${LOCALAI_VERSION:-v2.25.0}"
|
||||
BUILD_DIR="${BUILD_DIR:-/tmp/localai-build}"
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-./output}"
|
||||
|
||||
# Target architecture (default: aarch64 for ARM64)
|
||||
TARGET_ARCH="${TARGET_ARCH:-aarch64}"
|
||||
TARGET_OS="linux"
|
||||
|
||||
# OpenWrt SDK path (set this to your SDK location)
|
||||
SDK_PATH="${SDK_PATH:-}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
LocalAI Cross-Compile Script for OpenWrt
|
||||
|
||||
Usage: $0 [options]
|
||||
|
||||
Options:
|
||||
--sdk PATH Path to OpenWrt SDK (required for cross-compile)
|
||||
--arch ARCH Target architecture: aarch64, x86_64 (default: aarch64)
|
||||
--version VER LocalAI version (default: $LOCALAI_VERSION)
|
||||
--output DIR Output directory (default: ./output)
|
||||
--native Build natively (no cross-compile)
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
# Cross-compile for ARM64 using OpenWrt SDK
|
||||
$0 --sdk /path/to/openwrt-sdk --arch aarch64
|
||||
|
||||
# Build natively on current machine
|
||||
$0 --native
|
||||
|
||||
# Build specific version
|
||||
$0 --native --version v2.24.0
|
||||
|
||||
Requirements:
|
||||
- Go 1.21+
|
||||
- Git
|
||||
- GCC/G++ (or cross-compiler from SDK)
|
||||
- CMake
|
||||
- Make
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
log_info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
log_warn() { echo -e "\033[0;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*"; }
|
||||
|
||||
check_deps() {
|
||||
log_info "Checking dependencies..."
|
||||
|
||||
local missing=0
|
||||
|
||||
if ! command -v go &>/dev/null; then
|
||||
log_error "Go not found. Install Go 1.21+"
|
||||
missing=1
|
||||
else
|
||||
log_info "Go: $(go version | head -1)"
|
||||
fi
|
||||
|
||||
if ! command -v git &>/dev/null; then
|
||||
log_error "Git not found"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if ! command -v make &>/dev/null; then
|
||||
log_error "Make not found"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if ! command -v cmake &>/dev/null; then
|
||||
log_warn "CMake not found (may be needed for some backends)"
|
||||
fi
|
||||
|
||||
[ $missing -eq 1 ] && exit 1
|
||||
}
|
||||
|
||||
setup_cross_compile() {
|
||||
if [ -z "$SDK_PATH" ]; then
|
||||
log_error "SDK_PATH not set. Use --sdk option or set SDK_PATH environment variable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$SDK_PATH" ]; then
|
||||
log_error "SDK path does not exist: $SDK_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Setting up cross-compile environment..."
|
||||
|
||||
# Find toolchain
|
||||
local toolchain_dir=$(find "$SDK_PATH" -type d -name "toolchain-*" | head -1)
|
||||
if [ -z "$toolchain_dir" ]; then
|
||||
log_error "Toolchain not found in SDK"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local bin_dir="$toolchain_dir/bin"
|
||||
|
||||
# Detect cross-compiler prefix
|
||||
local cc_prefix=""
|
||||
case "$TARGET_ARCH" in
|
||||
aarch64)
|
||||
cc_prefix=$(ls "$bin_dir"/*-linux-*-gcc 2>/dev/null | head -1 | xargs basename | sed 's/-gcc$//')
|
||||
;;
|
||||
x86_64)
|
||||
cc_prefix=$(ls "$bin_dir"/*-linux-*-gcc 2>/dev/null | head -1 | xargs basename | sed 's/-gcc$//')
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$cc_prefix" ]; then
|
||||
log_error "Cross-compiler not found for $TARGET_ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PATH="$bin_dir:$PATH"
|
||||
export CC="${cc_prefix}-gcc"
|
||||
export CXX="${cc_prefix}-g++"
|
||||
export AR="${cc_prefix}-ar"
|
||||
export STRIP="${cc_prefix}-strip"
|
||||
|
||||
log_info "Cross-compiler: $CC"
|
||||
|
||||
# Set Go cross-compile vars
|
||||
export CGO_ENABLED=1
|
||||
export GOOS="$TARGET_OS"
|
||||
|
||||
case "$TARGET_ARCH" in
|
||||
aarch64) export GOARCH="arm64" ;;
|
||||
x86_64) export GOARCH="amd64" ;;
|
||||
*) log_error "Unknown arch: $TARGET_ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
log_info "Target: $GOOS/$GOARCH"
|
||||
}
|
||||
|
||||
setup_native() {
|
||||
log_info "Setting up native build..."
|
||||
export CGO_ENABLED=1
|
||||
export CC=gcc
|
||||
export CXX=g++
|
||||
|
||||
# Detect native arch
|
||||
case "$(uname -m)" in
|
||||
x86_64) export GOARCH="amd64" ;;
|
||||
aarch64) export GOARCH="arm64" ;;
|
||||
*) export GOARCH="amd64" ;;
|
||||
esac
|
||||
export GOOS="linux"
|
||||
|
||||
log_info "Building for: $GOOS/$GOARCH (native)"
|
||||
}
|
||||
|
||||
clone_repo() {
|
||||
log_info "Preparing LocalAI source..."
|
||||
mkdir -p "$BUILD_DIR"
|
||||
cd "$BUILD_DIR"
|
||||
|
||||
if [ -d "LocalAI" ]; then
|
||||
log_info "Updating existing repository..."
|
||||
cd LocalAI
|
||||
git fetch --all
|
||||
git checkout "$LOCALAI_VERSION" 2>/dev/null || git checkout main
|
||||
else
|
||||
log_info "Cloning LocalAI $LOCALAI_VERSION..."
|
||||
git clone --depth 1 --branch "$LOCALAI_VERSION" https://github.com/mudler/LocalAI.git 2>/dev/null || \
|
||||
git clone https://github.com/mudler/LocalAI.git
|
||||
cd LocalAI
|
||||
git checkout "$LOCALAI_VERSION" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_info "Source ready at $BUILD_DIR/LocalAI"
|
||||
}
|
||||
|
||||
build_localai() {
|
||||
cd "$BUILD_DIR/LocalAI"
|
||||
|
||||
log_info "Building LocalAI with llama-cpp backend..."
|
||||
log_info "This may take 15-30 minutes..."
|
||||
echo ""
|
||||
|
||||
# Build with llama-cpp backend
|
||||
BUILD_GRPC_FOR_BACKEND_LLAMA=true \
|
||||
GRPC_BACKENDS="backend-assets/grpc/llama-cpp" \
|
||||
BUILD_TYPE=generic \
|
||||
make build 2>&1 | tee "$BUILD_DIR/build.log"
|
||||
|
||||
if [ -f "local-ai" ]; then
|
||||
log_info "Build successful!"
|
||||
|
||||
# Strip binary to reduce size
|
||||
if [ -n "$STRIP" ] && command -v "$STRIP" &>/dev/null; then
|
||||
log_info "Stripping binary..."
|
||||
$STRIP local-ai || true
|
||||
fi
|
||||
|
||||
# Copy to output
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
cp local-ai "$OUTPUT_DIR/local-ai-${GOARCH}"
|
||||
|
||||
local size=$(ls -lh "$OUTPUT_DIR/local-ai-${GOARCH}" | awk '{print $5}')
|
||||
log_info ""
|
||||
log_info "Output: $OUTPUT_DIR/local-ai-${GOARCH} ($size)"
|
||||
log_info ""
|
||||
log_info "Copy to OpenWrt device:"
|
||||
log_info " scp $OUTPUT_DIR/local-ai-${GOARCH} root@<router>:/opt/localai/bin/local-ai"
|
||||
log_info " ssh root@<router> chmod +x /opt/localai/bin/local-ai"
|
||||
else
|
||||
log_error "Build failed! Check $BUILD_DIR/build.log"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
NATIVE_BUILD=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--sdk)
|
||||
SDK_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--arch)
|
||||
TARGET_ARCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
LOCALAI_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--native)
|
||||
NATIVE_BUILD=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Main
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ LocalAI Cross-Compile for OpenWrt ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
check_deps
|
||||
|
||||
if [ $NATIVE_BUILD -eq 1 ]; then
|
||||
setup_native
|
||||
else
|
||||
setup_cross_compile
|
||||
fi
|
||||
|
||||
clone_repo
|
||||
build_localai
|
||||
|
||||
log_info "Done!"
|
||||
@ -1,35 +1,44 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
#
|
||||
# LocalAI - Native LLM with pre-built binary
|
||||
# Downloads ARM64/x86_64 binary from GitHub releases
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-localai
|
||||
PKG_RELEASE:=12
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_ARCH:=all
|
||||
PKG_VERSION:=2.25.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-localai
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
PKGARCH:=all
|
||||
SUBMENU:=SecuBox Apps
|
||||
TITLE:=SecuBox LocalAI - Self-hosted LLM (Docker)
|
||||
DEPENDS:=+uci +libuci +jsonfilter +wget-ssl
|
||||
TITLE:=LocalAI - Native LLM Server
|
||||
URL:=https://localai.io
|
||||
DEPENDS:=@(aarch64||x86_64) +libstdcpp +libpthread +wget-ssl +ca-certificates
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/secubox-app-localai/description
|
||||
LocalAI - Self-hosted, privacy-first AI/LLM for SecuBox-powered OpenWrt systems.
|
||||
LocalAI native binary package for OpenWrt.
|
||||
|
||||
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
|
||||
- OpenAI-compatible REST API
|
||||
- GGUF model support (LLaMA, Mistral, Phi, TinyLlama, etc.)
|
||||
- Controller CLI (localaictl)
|
||||
- Automatic binary download from GitHub
|
||||
|
||||
Runs in Docker/Podman container with all backends.
|
||||
Configure in /etc/config/localai.
|
||||
The binary is downloaded on first run via 'localaictl install'.
|
||||
|
||||
API: http://<router-ip>:8081/v1
|
||||
endef
|
||||
|
||||
define Package/secubox-app-localai/conffiles
|
||||
@ -37,6 +46,7 @@ define Package/secubox-app-localai/conffiles
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
# Nothing to compile - binary downloaded at runtime
|
||||
endef
|
||||
|
||||
define Package/secubox-app-localai/install
|
||||
@ -48,27 +58,23 @@ define Package/secubox-app-localai/install
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/localaictl $(1)/usr/sbin/localaictl
|
||||
|
||||
$(INSTALL_DIR) $(1)/srv/localai/models
|
||||
endef
|
||||
|
||||
define Package/secubox-app-localai/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
echo ""
|
||||
echo "LocalAI installed (Docker/Podman version)."
|
||||
echo "SecuBox LocalAI installed"
|
||||
echo ""
|
||||
echo "Prerequisites: Install podman or docker first"
|
||||
echo " opkg install podman"
|
||||
echo ""
|
||||
echo "To install and start LocalAI:"
|
||||
echo " localaictl install # Pull Docker image (~2-4GB)"
|
||||
echo "Quick start:"
|
||||
echo " localaictl install"
|
||||
echo " localaictl model-install tinyllama"
|
||||
echo " /etc/init.d/localai enable"
|
||||
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 tinyllama"
|
||||
echo ""
|
||||
echo "API: http://<router-ip>:8081/v1"
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
@ -1,57 +1,30 @@
|
||||
config main 'main'
|
||||
option enabled '0'
|
||||
option api_port '8080'
|
||||
option installed '0'
|
||||
option api_port '8081'
|
||||
option api_host '0.0.0.0'
|
||||
option data_path '/srv/localai'
|
||||
option models_path '/srv/localai/models'
|
||||
option memory_limit '2g'
|
||||
option threads '4'
|
||||
option context_size '2048'
|
||||
option debug '0'
|
||||
option cors '1'
|
||||
# Runtime: 'lxc', 'docker', 'podman', or 'auto' (auto-detect)
|
||||
option runtime 'auto'
|
||||
|
||||
# LXC settings (for runtime=lxc)
|
||||
config lxc 'lxc'
|
||||
option path '/srv/lxc'
|
||||
option version 'v3.10.0'
|
||||
|
||||
# Docker/Podman settings (for runtime=docker or podman)
|
||||
config docker 'docker'
|
||||
option image 'localai/localai:v3.10.0-ffmpeg'
|
||||
|
||||
# Default model to load on startup
|
||||
config model 'default'
|
||||
option enabled '1'
|
||||
option name 'tinyllama'
|
||||
option backend 'llama-cpp'
|
||||
|
||||
# Model presets - GGUF format for llama-cpp backend
|
||||
# Model presets
|
||||
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'
|
||||
option size '1.6G'
|
||||
option type 'text-generation'
|
||||
option description 'Microsoft Phi-2 - Compact and efficient'
|
||||
|
||||
config preset 'mistral'
|
||||
option name 'mistral-7b'
|
||||
option url 'https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf'
|
||||
option size '4.1G'
|
||||
option type 'text-generation'
|
||||
option description 'Mistral 7B Instruct - High quality assistant'
|
||||
|
||||
config preset 'gte_small'
|
||||
option name 'gte-small'
|
||||
option url 'https://huggingface.co/Supabase/gte-small/resolve/main/model.onnx'
|
||||
option size '67M'
|
||||
option type 'embeddings'
|
||||
option description 'GTE Small - Fast embeddings'
|
||||
option description 'Mistral 7B Instruct - High quality'
|
||||
|
||||
@ -1,37 +1,76 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# SecuBox LocalAI - Self-hosted LLM service
|
||||
# LocalAI init script
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
START=95
|
||||
START=99
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/sbin/localaictl
|
||||
PROG=/usr/bin/local-ai
|
||||
CONFIG=localai
|
||||
BACKEND_ASSETS=/usr/share/localai/backend-assets
|
||||
|
||||
start_service() {
|
||||
local enabled
|
||||
config_load localai
|
||||
config_load "$CONFIG"
|
||||
config_get enabled main enabled '0'
|
||||
|
||||
[ "$enabled" = "1" ] || {
|
||||
echo "LocalAI is disabled. Enable with: uci set localai.main.enabled=1"
|
||||
return 0
|
||||
}
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
# Find binary
|
||||
local binary=""
|
||||
if [ -x "$PROG" ]; then
|
||||
binary="$PROG"
|
||||
elif [ -x "/opt/localai/bin/local-ai" ]; then
|
||||
binary="/opt/localai/bin/local-ai"
|
||||
else
|
||||
logger -t localai -p err "LocalAI binary not found. Run: localaictl install"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Load configuration
|
||||
local api_port api_host models_path threads context_size debug cors
|
||||
config_get api_port main api_port '8081'
|
||||
config_get api_host main api_host '0.0.0.0'
|
||||
config_get models_path main models_path '/srv/localai/models'
|
||||
config_get threads main threads '4'
|
||||
config_get context_size main context_size '2048'
|
||||
config_get debug main debug '0'
|
||||
config_get cors main cors '1'
|
||||
|
||||
# Ensure models directory exists
|
||||
mkdir -p "$models_path"
|
||||
|
||||
# Build command arguments - use 'run' subcommand
|
||||
local args="run --address ${api_host}:${api_port}"
|
||||
args="$args --models-path $models_path"
|
||||
args="$args --threads $threads"
|
||||
args="$args --context-size $context_size"
|
||||
[ "$cors" = "1" ] && args="$args --cors"
|
||||
[ "$debug" = "1" ] && args="$args --debug"
|
||||
|
||||
# Point to backend assets if they exist
|
||||
if [ -d "$BACKEND_ASSETS" ]; then
|
||||
args="$args --backend-assets-path $BACKEND_ASSETS"
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command $PROG service-run
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_set_param command "$binary" $args
|
||||
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/localai.pid
|
||||
procd_close_instance
|
||||
|
||||
logger -t localai "Started LocalAI on ${api_host}:${api_port}"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
$PROG service-stop
|
||||
logger -t localai "Stopping LocalAI"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "localai"
|
||||
procd_add_reload_trigger "$CONFIG"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
|
||||
@ -1,44 +1,45 @@
|
||||
#!/bin/sh
|
||||
# SecuBox LocalAI manager - Multi-runtime support (LXC, Docker, Podman)
|
||||
# SecuBox LocalAI Controller
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
#
|
||||
# LocalAI native binary management
|
||||
|
||||
CONFIG="localai"
|
||||
CONTAINER_NAME="localai"
|
||||
LOCALAI_VERSION="v3.10.0"
|
||||
BINARY="/usr/bin/local-ai"
|
||||
DATA_DIR="/srv/localai"
|
||||
BACKEND_ASSETS="/usr/share/localai/backend-assets"
|
||||
LOCALAI_VERSION="2.25.0"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: localaictl <command>
|
||||
|
||||
Commands:
|
||||
install Install LocalAI (auto-detect or use configured runtime)
|
||||
install --lxc Force LXC installation (standalone binary)
|
||||
install --docker Force Docker installation (full image with backends)
|
||||
install --podman Force Podman installation (full image with backends)
|
||||
check Run prerequisite checks
|
||||
update Update LocalAI
|
||||
status Show container and service status
|
||||
logs Show LocalAI logs (use -f to follow)
|
||||
shell Open shell in container
|
||||
Install Commands:
|
||||
install Download LocalAI binary from GitHub
|
||||
uninstall Remove LocalAI binary
|
||||
|
||||
Model Management:
|
||||
Service Commands:
|
||||
start Start LocalAI service
|
||||
stop Stop LocalAI service
|
||||
restart Restart LocalAI service
|
||||
status Show service status
|
||||
logs Show logs (use -f to follow)
|
||||
|
||||
Model Commands:
|
||||
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
|
||||
Backend Commands:
|
||||
backends List available backends
|
||||
|
||||
Runtimes:
|
||||
lxc - LXC container with rootfs extracted from Docker image
|
||||
(includes all backends: llama-cpp, whisper, etc.)
|
||||
Downloads via Docker Registry API - no daemon required!
|
||||
docker - Run Docker container directly (requires dockerd)
|
||||
podman - Run Podman container directly (rootless)
|
||||
API Endpoints (default port 8081):
|
||||
/v1/models - List models
|
||||
/v1/chat/completions - Chat completion
|
||||
/v1/completions - Text completion
|
||||
/readyz - Health check
|
||||
|
||||
Configuration: /etc/config/localai
|
||||
Set runtime with: uci set localai.main.runtime=<lxc|docker|podman|auto>
|
||||
EOF
|
||||
}
|
||||
|
||||
@ -51,582 +52,238 @@ 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_port="$(uci_get main.api_port || echo 8081)"
|
||||
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)"
|
||||
data_path="$(uci_get main.data_path || echo $DATA_DIR)"
|
||||
models_path="$(uci_get main.models_path || echo $DATA_DIR/models)"
|
||||
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)"
|
||||
runtime="$(uci_get main.runtime || echo auto)"
|
||||
|
||||
# LXC settings
|
||||
lxc_path="$(uci_get lxc.path || echo /srv/lxc)"
|
||||
lxc_version="$(uci_get lxc.version || echo $LOCALAI_VERSION)"
|
||||
|
||||
# Docker settings
|
||||
docker_image="$(uci_get docker.image || echo localai/localai:${LOCALAI_VERSION}-ffmpeg)"
|
||||
|
||||
# Ensure paths exist
|
||||
[ -d "$data_path" ] || mkdir -p "$data_path"
|
||||
[ -d "$models_path" ] || mkdir -p "$models_path"
|
||||
mkdir -p "$data_path" "$models_path"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# RUNTIME DETECTION
|
||||
# INSTALL/UNINSTALL
|
||||
# =============================================================================
|
||||
|
||||
detect_runtime() {
|
||||
local configured="$runtime"
|
||||
|
||||
# If auto or empty, detect available runtime
|
||||
if [ "$configured" = "auto" ] || [ -z "$configured" ]; then
|
||||
# Check what's already running first
|
||||
if command -v lxc-info >/dev/null 2>&1 && lxc-info -n "$CONTAINER_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||
echo "lxc"
|
||||
return
|
||||
fi
|
||||
if command -v podman >/dev/null 2>&1 && podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "podman"
|
||||
return
|
||||
fi
|
||||
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "docker"
|
||||
return
|
||||
fi
|
||||
|
||||
# Nothing running, check what's installed (prefer docker/podman for backends)
|
||||
if command -v podman >/dev/null 2>&1; then
|
||||
echo "podman"
|
||||
elif command -v docker >/dev/null 2>&1; then
|
||||
echo "docker"
|
||||
elif command -v lxc-start >/dev/null 2>&1; then
|
||||
echo "lxc"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
echo "$configured"
|
||||
fi
|
||||
}
|
||||
|
||||
has_runtime() {
|
||||
local rt=$(detect_runtime)
|
||||
[ -n "$rt" ]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# CONTAINER STATE CHECKS
|
||||
# =============================================================================
|
||||
|
||||
is_running() {
|
||||
load_config
|
||||
local rt=$(detect_runtime)
|
||||
|
||||
case "$rt" in
|
||||
lxc)
|
||||
lxc-info -n "$CONTAINER_NAME" -s 2>/dev/null | grep -q "RUNNING"
|
||||
;;
|
||||
podman)
|
||||
podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
|
||||
;;
|
||||
docker)
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
|
||||
;;
|
||||
*)
|
||||
pgrep -f "local-ai" >/dev/null 2>&1
|
||||
;;
|
||||
get_arch() {
|
||||
local arch=$(uname -m)
|
||||
case "$arch" in
|
||||
aarch64) echo "arm64" ;;
|
||||
x86_64) echo "amd64" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
container_exists() {
|
||||
load_config
|
||||
local rt=$(detect_runtime)
|
||||
|
||||
case "$rt" in
|
||||
lxc)
|
||||
[ -d "$lxc_path/$CONTAINER_NAME" ]
|
||||
;;
|
||||
podman)
|
||||
podman ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
|
||||
;;
|
||||
docker)
|
||||
docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# LXC FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
lxc_stop() {
|
||||
if lxc-info -n "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
lxc-stop -n "$CONTAINER_NAME" -k >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
lxc_install() {
|
||||
log_info "Installing LocalAI using LXC..."
|
||||
|
||||
# Check LXC packages
|
||||
if ! command -v lxc-start >/dev/null 2>&1; then
|
||||
log_error "LXC not installed. Install with: opkg install lxc lxc-common"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local rootfs="$lxc_path/$CONTAINER_NAME/rootfs"
|
||||
local config="$lxc_path/$CONTAINER_NAME/config"
|
||||
|
||||
# Extract Docker image via Registry API (no daemon needed)
|
||||
lxc_create_docker_rootfs "$rootfs" || return 1
|
||||
|
||||
# Create LXC config
|
||||
lxc_create_config "$config" "$rootfs"
|
||||
|
||||
log_info "LXC container configured at $lxc_path/$CONTAINER_NAME"
|
||||
uci_set main.runtime 'lxc'
|
||||
return 0
|
||||
}
|
||||
|
||||
# Extract Docker image via Registry API (no daemon required!)
|
||||
lxc_create_docker_rootfs() {
|
||||
local rootfs="$1"
|
||||
local image="localai/localai"
|
||||
local tag="${LOCALAI_VERSION}-ffmpeg"
|
||||
local registry="registry-1.docker.io"
|
||||
local arch
|
||||
|
||||
# Detect architecture for Docker manifest
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="amd64" ;;
|
||||
aarch64) arch="arm64" ;;
|
||||
armv7l) arch="arm" ;;
|
||||
*) arch="amd64" ;;
|
||||
esac
|
||||
|
||||
log_info "Extracting LocalAI Docker image ($arch)..."
|
||||
log_info "Image: $image:$tag"
|
||||
log_info "This includes ALL backends (llama-cpp, whisper, etc.)"
|
||||
mkdir -p "$rootfs"
|
||||
|
||||
# Get Docker Hub token
|
||||
log_info "Authenticating with Docker Hub..."
|
||||
local token=$(wget -q -O - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jsonfilter -e '@.token')
|
||||
[ -z "$token" ] && { log_error "Failed to get Docker Hub token"; return 1; }
|
||||
|
||||
# Get manifest list (multi-arch)
|
||||
log_info "Fetching manifest..."
|
||||
local manifest=$(wget -q -O - --header="Authorization: Bearer $token" \
|
||||
--header="Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
|
||||
"https://$registry/v2/$image/manifests/$tag")
|
||||
|
||||
# Find digest for our architecture
|
||||
local digest=$(echo "$manifest" | jsonfilter -e "@.manifests[@.platform.architecture='$arch'].digest")
|
||||
[ -z "$digest" ] && { log_error "No manifest found for $arch"; return 1; }
|
||||
|
||||
# Get image manifest with layer digests
|
||||
local img_manifest=$(wget -q -O - --header="Authorization: Bearer $token" \
|
||||
--header="Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
||||
"https://$registry/v2/$image/manifests/$digest")
|
||||
|
||||
# Extract layer digests
|
||||
local layers=$(echo "$img_manifest" | jsonfilter -e '@.layers[*].digest')
|
||||
local layer_count=$(echo "$layers" | wc -w)
|
||||
log_info "Downloading $layer_count layers (this will take a while, ~4GB)..."
|
||||
|
||||
local i=0
|
||||
for layer_digest in $layers; do
|
||||
i=$((i + 1))
|
||||
log_info " Layer $i/$layer_count: ${layer_digest:7:12}..."
|
||||
wget -q -O - --header="Authorization: Bearer $token" \
|
||||
"https://$registry/v2/$image/blobs/$layer_digest" | \
|
||||
tar xzf - -C "$rootfs" 2>&1 | grep -v "Cannot change ownership" || true
|
||||
done
|
||||
|
||||
# Configure container
|
||||
echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf"
|
||||
mkdir -p "$rootfs/models" "$rootfs/build" "$rootfs/tmp"
|
||||
|
||||
# Ensure /bin/sh exists
|
||||
if [ ! -x "$rootfs/bin/sh" ]; then
|
||||
log_warn "/bin/sh not found, attempting to fix..."
|
||||
if [ -x "$rootfs/bin/bash" ]; then
|
||||
ln -sf bash "$rootfs/bin/sh"
|
||||
elif [ -x "$rootfs/bin/dash" ]; then
|
||||
ln -sf dash "$rootfs/bin/sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
local rootfs_size=$(du -sh "$rootfs" 2>/dev/null | cut -f1)
|
||||
log_info "Rootfs size: $rootfs_size"
|
||||
log_info "LocalAI Docker image extracted successfully"
|
||||
log_info "All backends available: llama-cpp, whisper, stablediffusion, etc."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Create LXC configuration file
|
||||
lxc_create_config() {
|
||||
local config="$1"
|
||||
local rootfs="$2"
|
||||
|
||||
# Build command flags
|
||||
local cors_flag="" debug_flag=""
|
||||
[ "$cors" = "1" ] && cors_flag=" --cors"
|
||||
[ "$debug" = "1" ] && debug_flag=" --debug"
|
||||
|
||||
# Detect init command based on rootfs type
|
||||
local init_cmd="/usr/bin/local-ai"
|
||||
if [ -f "$rootfs/build/entrypoint.sh" ]; then
|
||||
# Docker image has entrypoint script
|
||||
init_cmd="/build/entrypoint.sh"
|
||||
fi
|
||||
|
||||
cat > "$config" << EOF
|
||||
# LocalAI LXC Configuration
|
||||
lxc.uts.name = $CONTAINER_NAME
|
||||
lxc.rootfs.path = dir:$rootfs
|
||||
|
||||
# Network - use host network
|
||||
lxc.net.0.type = none
|
||||
|
||||
# Mount points
|
||||
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||
lxc.mount.entry = $models_path models none bind,create=dir 0 0
|
||||
lxc.mount.entry = $data_path build none bind,create=dir 0 0
|
||||
lxc.mount.entry = /dev/null dev/null none bind,create=file 0 0
|
||||
lxc.mount.entry = /dev/zero dev/zero none bind,create=file 0 0
|
||||
lxc.mount.entry = /dev/urandom dev/urandom none bind,create=file 0 0
|
||||
|
||||
# Environment variables
|
||||
lxc.environment = LOCALAI_THREADS=$threads
|
||||
lxc.environment = LOCALAI_CONTEXT_SIZE=$context_size
|
||||
lxc.environment = LOCALAI_ADDRESS=${api_host}:${api_port}
|
||||
lxc.environment = LOCALAI_MODELS_PATH=/models
|
||||
lxc.environment = LOCALAI_DEBUG=$debug
|
||||
lxc.environment = LOCALAI_CORS=$cors
|
||||
lxc.environment = PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
# Security
|
||||
lxc.cap.drop = sys_admin sys_module mac_admin mac_override
|
||||
|
||||
# Resources
|
||||
lxc.cgroup.memory.limit_in_bytes = $memory_limit
|
||||
|
||||
# Init command
|
||||
lxc.init.cmd = $init_cmd --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
|
||||
}
|
||||
|
||||
lxc_run() {
|
||||
load_config
|
||||
lxc_stop
|
||||
|
||||
local config="$lxc_path/$CONTAINER_NAME/config"
|
||||
if [ ! -f "$config" ]; then
|
||||
log_error "LXC not configured. Run 'localaictl install --lxc' first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Starting LocalAI LXC container..."
|
||||
log_info "API: http://${api_host}:${api_port}"
|
||||
exec lxc-start -n "$CONTAINER_NAME" -F -f "$config"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# DOCKER/PODMAN FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
docker_stop() {
|
||||
local rt="$1"
|
||||
if $rt ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then
|
||||
$rt stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if $rt ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then
|
||||
$rt rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
docker_install() {
|
||||
local rt="$1"
|
||||
|
||||
log_info "Installing LocalAI using $rt..."
|
||||
log_info "Image: $docker_image"
|
||||
log_info "This includes all backends (llama-cpp, whisper, etc.)"
|
||||
|
||||
if ! $rt pull "$docker_image"; then
|
||||
log_error "Failed to pull image"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Image pulled successfully"
|
||||
uci_set main.runtime "$rt"
|
||||
return 0
|
||||
}
|
||||
|
||||
docker_run() {
|
||||
local rt="$1"
|
||||
load_config
|
||||
docker_stop "$rt"
|
||||
|
||||
log_info "Starting LocalAI container ($rt)..."
|
||||
log_info "Image: $docker_image"
|
||||
log_info "API: http://${api_host}:${api_port}"
|
||||
|
||||
local env_args="-e LOCALAI_THREADS=$threads -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"
|
||||
|
||||
exec $rt 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"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# UNIFIED COMMANDS
|
||||
# =============================================================================
|
||||
|
||||
cmd_install() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
local force_runtime=""
|
||||
case "$1" in
|
||||
--lxc) force_runtime="lxc" ;;
|
||||
--docker) force_runtime="docker" ;;
|
||||
--podman) force_runtime="podman" ;;
|
||||
esac
|
||||
|
||||
local rt="${force_runtime:-$(detect_runtime)}"
|
||||
|
||||
if [ -z "$rt" ]; then
|
||||
log_error "No container runtime found!"
|
||||
log_error "Install one of:"
|
||||
log_error " opkg install lxc lxc-common # For LXC"
|
||||
log_error " opkg install podman # For Podman"
|
||||
log_error " opkg install docker # For Docker"
|
||||
local arch=$(get_arch)
|
||||
if [ -z "$arch" ]; then
|
||||
log_error "Unsupported architecture: $(uname -m)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$data_path" "$models_path"
|
||||
if [ -x "$BINARY" ]; then
|
||||
log_warn "LocalAI already installed at $BINARY"
|
||||
local ver=$("$BINARY" run --help 2>&1 | grep -i version | head -1 || echo "installed")
|
||||
log_info "Status: $ver"
|
||||
echo ""
|
||||
echo "To reinstall, run: localaictl uninstall && localaictl install"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$rt" in
|
||||
lxc)
|
||||
lxc_install || return 1
|
||||
;;
|
||||
podman|docker)
|
||||
if ! command -v $rt >/dev/null 2>&1; then
|
||||
log_error "$rt not installed"
|
||||
# LocalAI v2.x binary URL format
|
||||
local url="https://github.com/mudler/LocalAI/releases/download/v${LOCALAI_VERSION}/local-ai-Linux-${arch}"
|
||||
|
||||
log_info "Downloading LocalAI v${LOCALAI_VERSION} for ${arch}..."
|
||||
log_info "URL: $url"
|
||||
echo ""
|
||||
|
||||
# Create temp file
|
||||
local tmp_file="/tmp/local-ai-download"
|
||||
rm -f "$tmp_file"
|
||||
|
||||
# Use -L to follow redirects (GitHub uses redirects)
|
||||
if wget -L --show-progress -O "$tmp_file" "$url" 2>&1; then
|
||||
# Verify it's an ELF binary by checking magic bytes
|
||||
local magic=$(head -c 4 "$tmp_file" | hexdump -e '4/1 "%02x"' 2>/dev/null)
|
||||
if [ "$magic" = "7f454c46" ]; then
|
||||
mv "$tmp_file" "$BINARY"
|
||||
chmod +x "$BINARY"
|
||||
log_info "LocalAI installed: $BINARY"
|
||||
|
||||
# Mark as installed in config
|
||||
uci_set main.installed 1
|
||||
|
||||
echo ""
|
||||
log_info "Binary downloaded successfully!"
|
||||
echo ""
|
||||
echo "To start the service:"
|
||||
echo " uci set localai.main.enabled=1"
|
||||
echo " uci commit localai"
|
||||
echo " /etc/init.d/localai enable"
|
||||
echo " /etc/init.d/localai start"
|
||||
echo ""
|
||||
echo "To download a model:"
|
||||
echo " localaictl model-install tinyllama"
|
||||
else
|
||||
log_error "Downloaded file is not a valid ELF binary"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
docker_install "$rt" || return 1
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown runtime: $rt"
|
||||
else
|
||||
log_error "Failed to download LocalAI"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
uci_set main.enabled '1'
|
||||
/etc/init.d/localai enable
|
||||
cmd_uninstall() {
|
||||
require_root
|
||||
|
||||
log_info ""
|
||||
log_info "LocalAI installed successfully! (runtime: $rt)"
|
||||
log_info ""
|
||||
log_info "Start with: /etc/init.d/localai start"
|
||||
log_info "API: http://<router-ip>:$api_port/v1"
|
||||
log_info ""
|
||||
log_info "Install a model:"
|
||||
log_info " localaictl model-install tinyllama"
|
||||
if [ -x "$BINARY" ]; then
|
||||
# Stop service first
|
||||
/etc/init.d/localai stop 2>/dev/null
|
||||
|
||||
rm -f "$BINARY"
|
||||
uci_set main.installed 0
|
||||
uci_set main.enabled 0
|
||||
log_info "LocalAI binary removed"
|
||||
else
|
||||
log_warn "LocalAI not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SERVICE MANAGEMENT
|
||||
# =============================================================================
|
||||
|
||||
is_running() {
|
||||
pgrep -f "$BINARY" >/dev/null 2>&1 || pgrep -f "local-ai" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
if ! [ -x "$BINARY" ]; then
|
||||
log_error "LocalAI binary not found: $BINARY"
|
||||
log_error "Run: localaictl install"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if is_running; then
|
||||
log_warn "Already running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Starting LocalAI..."
|
||||
/etc/init.d/localai start
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
require_root
|
||||
load_config
|
||||
local rt=$(detect_runtime)
|
||||
|
||||
case "$rt" in
|
||||
lxc) lxc_stop ;;
|
||||
podman) docker_stop podman ;;
|
||||
docker) docker_stop docker ;;
|
||||
esac
|
||||
/etc/init.d/localai stop
|
||||
}
|
||||
|
||||
cmd_run() {
|
||||
cmd_restart() {
|
||||
require_root
|
||||
load_config
|
||||
local rt=$(detect_runtime)
|
||||
|
||||
if [ -z "$rt" ]; then
|
||||
log_error "No runtime configured. Run 'localaictl install' first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$rt" in
|
||||
lxc) lxc_run ;;
|
||||
podman) docker_run podman ;;
|
||||
docker) docker_run docker ;;
|
||||
*)
|
||||
log_error "Unknown runtime: $rt"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
/etc/init.d/localai restart
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
load_config
|
||||
local rt=$(detect_runtime)
|
||||
|
||||
echo "=== LocalAI Status ==="
|
||||
echo ""
|
||||
echo "Runtime: ${rt:-NOT CONFIGURED}"
|
||||
echo ""
|
||||
|
||||
if is_running; then
|
||||
echo "Status: RUNNING"
|
||||
elif container_exists; then
|
||||
echo "Status: STOPPED"
|
||||
if [ -x "$BINARY" ]; then
|
||||
echo "Binary: $BINARY"
|
||||
local size=$(ls -lh "$BINARY" 2>/dev/null | awk '{print $5}')
|
||||
echo "Size: $size"
|
||||
else
|
||||
echo "Status: NOT INSTALLED"
|
||||
echo "Binary: NOT FOUND"
|
||||
echo ""
|
||||
echo "Run: localaictl install"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Configuration ==="
|
||||
echo "API port: $api_port"
|
||||
echo "Data path: $data_path"
|
||||
echo "Models path: $models_path"
|
||||
echo "Memory limit: $memory_limit"
|
||||
|
||||
if is_running; then
|
||||
echo "Service: RUNNING"
|
||||
local pid=$(pgrep -f "$BINARY" | head -1)
|
||||
echo "PID: $pid"
|
||||
else
|
||||
echo "Service: STOPPED"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Configuration:"
|
||||
echo " API: http://${api_host}:${api_port}"
|
||||
echo " Models: $models_path"
|
||||
echo " Threads: $threads"
|
||||
echo " Context: $context_size"
|
||||
|
||||
echo ""
|
||||
|
||||
if wget -q -O - "http://127.0.0.1:$api_port/readyz" 2>/dev/null | grep -q "ok"; then
|
||||
# Check backend assets
|
||||
echo "Backends:"
|
||||
if [ -d "$BACKEND_ASSETS/grpc" ]; then
|
||||
local backend_count=0
|
||||
for b in "$BACKEND_ASSETS/grpc"/*; do
|
||||
[ -x "$b" ] && backend_count=$((backend_count + 1))
|
||||
done
|
||||
echo " GRPC backends: $backend_count installed"
|
||||
else
|
||||
echo " GRPC backends: none (using built-in)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check API health
|
||||
if is_running; then
|
||||
if wget -q -O /dev/null "http://127.0.0.1:$api_port/readyz" 2>/dev/null; then
|
||||
echo "API Status: HEALTHY"
|
||||
else
|
||||
echo "API Status: NOT RESPONDING"
|
||||
echo "API Status: NOT RESPONDING (may be loading)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
load_config
|
||||
local rt=$(detect_runtime)
|
||||
|
||||
case "$rt" in
|
||||
lxc)
|
||||
if [ "$1" = "-f" ]; then
|
||||
logread -f -e localai
|
||||
else
|
||||
logread -e localai | tail -100
|
||||
fi
|
||||
;;
|
||||
podman|docker)
|
||||
if [ "$1" = "-f" ]; then
|
||||
$rt logs -f "$CONTAINER_NAME"
|
||||
else
|
||||
$rt logs --tail 100 "$CONTAINER_NAME"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
logread -e localai | tail -100
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_shell() {
|
||||
load_config
|
||||
local rt=$(detect_runtime)
|
||||
# =============================================================================
|
||||
# BACKEND MANAGEMENT
|
||||
# =============================================================================
|
||||
|
||||
if ! is_running; then
|
||||
log_error "Container not running"
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$rt" in
|
||||
lxc) lxc-attach -n "$CONTAINER_NAME" -- /bin/sh ;;
|
||||
podman|docker) $rt exec -it "$CONTAINER_NAME" /bin/sh ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_check() {
|
||||
load_config
|
||||
|
||||
echo "=== Prerequisite Check ==="
|
||||
cmd_backends() {
|
||||
echo "=== Available Backends ==="
|
||||
echo ""
|
||||
|
||||
# LXC
|
||||
if command -v lxc-start >/dev/null 2>&1; then
|
||||
echo "[OK] LXC available"
|
||||
# Check installed backend binaries
|
||||
if [ -d "$BACKEND_ASSETS/grpc" ]; then
|
||||
echo "Installed GRPC backends:"
|
||||
for backend in "$BACKEND_ASSETS/grpc"/*; do
|
||||
[ -x "$backend" ] || continue
|
||||
local name=$(basename "$backend")
|
||||
echo " - $name"
|
||||
done
|
||||
else
|
||||
echo "[--] LXC not installed"
|
||||
fi
|
||||
|
||||
# Podman
|
||||
if command -v podman >/dev/null 2>&1; then
|
||||
echo "[OK] Podman available"
|
||||
else
|
||||
echo "[--] Podman not installed"
|
||||
fi
|
||||
|
||||
# Docker
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
echo "[OK] Docker available"
|
||||
else
|
||||
echo "[--] Docker not installed"
|
||||
echo "No external GRPC backends installed"
|
||||
echo "Using built-in backends from binary"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Configured runtime: $runtime"
|
||||
echo "Detected runtime: $(detect_runtime)"
|
||||
echo ""
|
||||
|
||||
# Memory
|
||||
local mem_total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
||||
local mem_gb=$((mem_total / 1024 / 1024))
|
||||
echo "System memory: ${mem_gb}GB"
|
||||
[ "$mem_gb" -lt 2 ] && echo "[WARN] Low memory - need at least 2GB"
|
||||
|
||||
# Storage
|
||||
local storage=$(df -h "$data_path" 2>/dev/null | tail -1 | awk '{print $4}')
|
||||
echo "Storage available: $storage"
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
require_root
|
||||
load_config
|
||||
local rt=$(detect_runtime)
|
||||
|
||||
log_info "Updating LocalAI..."
|
||||
cmd_stop
|
||||
|
||||
case "$rt" in
|
||||
lxc)
|
||||
rm -rf "$lxc_path/$CONTAINER_NAME"
|
||||
lxc_install
|
||||
;;
|
||||
podman|docker)
|
||||
docker_install "$rt"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$(uci_get main.enabled)" = "1" ]; then
|
||||
/etc/init.d/localai restart
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
@ -640,26 +297,23 @@ cmd_models() {
|
||||
|
||||
if [ -d "$models_path" ]; then
|
||||
local count=0
|
||||
for model in "$models_path"/*.gguf "$models_path"/*.bin "$models_path"/*.onnx; do
|
||||
for model in "$models_path"/*.gguf "$models_path"/*.bin; do
|
||||
[ -f "$model" ] || continue
|
||||
count=$((count + 1))
|
||||
local name=$(basename "$model")
|
||||
local size=$(ls -lh "$model" | awk '{print $5}')
|
||||
echo " $count. $name ($size)"
|
||||
done
|
||||
|
||||
[ "$count" -eq 0 ] && echo " No models installed"
|
||||
fi
|
||||
|
||||
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 name=$(uci_get "$section.name")
|
||||
local desc=$(uci_get "$section.description")
|
||||
local size=$(uci_get "$section.size")
|
||||
[ -n "$name" ] && echo " $name - $desc ($size)"
|
||||
done
|
||||
echo " tinyllama - 669MB - TinyLlama 1.1B"
|
||||
echo " phi2 - 1.6GB - Microsoft Phi-2"
|
||||
echo " mistral - 4.1GB - Mistral 7B Instruct"
|
||||
echo ""
|
||||
echo "Install: localaictl model-install <name>"
|
||||
}
|
||||
|
||||
cmd_model_install() {
|
||||
@ -667,46 +321,56 @@ cmd_model_install() {
|
||||
require_root
|
||||
|
||||
local model_name="$1"
|
||||
[ -z "$model_name" ] && { echo "Usage: localaictl model-install <name>"; return 1; }
|
||||
[ -z "$model_name" ] && { echo "Usage: localaictl model-install <name|url>"; return 1; }
|
||||
|
||||
mkdir -p "$models_path"
|
||||
|
||||
# Find preset
|
||||
local preset_url="" preset_file=""
|
||||
for section in $(uci show localai 2>/dev/null | grep "=preset" | cut -d. -f2 | cut -d= -f1); do
|
||||
if [ "$(uci_get "$section.name")" = "$model_name" ]; then
|
||||
preset_url=$(uci_get "$section.url")
|
||||
preset_file=$(basename "$preset_url")
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Preset URLs
|
||||
local url="" filename=""
|
||||
case "$model_name" in
|
||||
tinyllama)
|
||||
url="https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
|
||||
filename="tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
|
||||
;;
|
||||
phi2|phi-2)
|
||||
url="https://huggingface.co/TheBloke/phi-2-GGUF/resolve/main/phi-2.Q4_K_M.gguf"
|
||||
filename="phi-2.Q4_K_M.gguf"
|
||||
;;
|
||||
mistral)
|
||||
url="https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf"
|
||||
filename="mistral-7b-instruct-v0.2.Q4_K_M.gguf"
|
||||
;;
|
||||
http*)
|
||||
url="$model_name"
|
||||
filename=$(basename "$url")
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown model: $model_name"
|
||||
log_error "Use preset name (tinyllama, phi2, mistral) or full URL"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$preset_url" ]; then
|
||||
log_info "Installing model: $model_name"
|
||||
log_info "URL: $preset_url"
|
||||
log_info "Downloading: $filename"
|
||||
log_info "URL: $url"
|
||||
log_info "This may take several minutes..."
|
||||
|
||||
if wget --show-progress -O "$models_path/$preset_file" "$preset_url"; then
|
||||
cat > "$models_path/$model_name.yaml" << EOF
|
||||
name: $model_name
|
||||
if wget -L --show-progress -O "$models_path/$filename" "$url"; then
|
||||
# Create YAML config for the model
|
||||
local model_id="${filename%.*}"
|
||||
cat > "$models_path/$model_id.yaml" << EOF
|
||||
name: $model_id
|
||||
backend: llama-cpp
|
||||
parameters:
|
||||
model: $preset_file
|
||||
model: $filename
|
||||
context_size: $context_size
|
||||
threads: $threads
|
||||
EOF
|
||||
log_info "Model installed: $model_name"
|
||||
log_info "Restart LocalAI to load: /etc/init.d/localai restart"
|
||||
log_info "Model installed: $model_id"
|
||||
log_info "Restart service to load: /etc/init.d/localai restart"
|
||||
else
|
||||
log_error "Download failed"
|
||||
return 1
|
||||
fi
|
||||
elif echo "$model_name" | grep -q "^http"; then
|
||||
local filename=$(basename "$model_name")
|
||||
log_info "Downloading: $model_name"
|
||||
wget --show-progress -O "$models_path/$filename" "$model_name" || return 1
|
||||
log_info "Model installed: $filename"
|
||||
else
|
||||
log_error "Unknown model: $model_name"
|
||||
rm -f "$models_path/$filename"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@ -719,15 +383,19 @@ cmd_model_remove() {
|
||||
[ -z "$model_name" ] && { echo "Usage: localaictl model-remove <name>"; return 1; }
|
||||
|
||||
local found=0
|
||||
for ext in gguf bin onnx yaml; do
|
||||
[ -f "$models_path/$model_name.$ext" ] && rm -f "$models_path/$model_name.$ext" && found=1
|
||||
for ext in gguf bin yaml yml; do
|
||||
if [ -f "$models_path/$model_name.$ext" ]; then
|
||||
rm -f "$models_path/$model_name.$ext"
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Also try to match partial names
|
||||
for file in "$models_path"/*"$model_name"*; do
|
||||
[ -f "$file" ] && rm -f "$file" && found=1
|
||||
done
|
||||
|
||||
[ "$found" -eq 1 ] && log_info "Model removed: $model_name" || log_warn "Model not found: $model_name"
|
||||
[ $found -eq 1 ] && log_info "Model removed: $model_name" || log_warn "Model not found: $model_name"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
@ -735,17 +403,17 @@ cmd_model_remove() {
|
||||
# =============================================================================
|
||||
|
||||
case "${1:-}" in
|
||||
install) shift; cmd_install "$@" ;;
|
||||
check) cmd_check ;;
|
||||
update) cmd_update ;;
|
||||
install) cmd_install ;;
|
||||
uninstall) cmd_uninstall ;;
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
status) cmd_status ;;
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
shell) cmd_shell ;;
|
||||
backends) cmd_backends ;;
|
||||
models) cmd_models ;;
|
||||
model-install) shift; cmd_model_install "$@" ;;
|
||||
model-remove) shift; cmd_model_remove "$@" ;;
|
||||
service-run) cmd_run ;;
|
||||
service-stop) cmd_stop ;;
|
||||
help|--help|-h|'') usage ;;
|
||||
*) echo "Unknown: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
Loading…
Reference in New Issue
Block a user