diff --git a/package/secubox/secubox-app-localai-wb/Makefile b/package/secubox/secubox-app-localai-wb/Makefile deleted file mode 100644 index 0565dad..0000000 --- a/package/secubox/secubox-app-localai-wb/Makefile +++ /dev/null @@ -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 - -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)) diff --git a/package/secubox/secubox-app-localai-wb/files/etc/config/localai-wb b/package/secubox/secubox-app-localai-wb/files/etc/config/localai-wb deleted file mode 100644 index 661ecc5..0000000 --- a/package/secubox/secubox-app-localai-wb/files/etc/config/localai-wb +++ /dev/null @@ -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' diff --git a/package/secubox/secubox-app-localai-wb/files/etc/init.d/localai-wb b/package/secubox/secubox-app-localai-wb/files/etc/init.d/localai-wb deleted file mode 100644 index 464147c..0000000 --- a/package/secubox/secubox-app-localai-wb/files/etc/init.d/localai-wb +++ /dev/null @@ -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 -} diff --git a/package/secubox/secubox-app-localai-wb/files/usr/sbin/localai-wb-ctl b/package/secubox/secubox-app-localai-wb/files/usr/sbin/localai-wb-ctl deleted file mode 100644 index a2bc24b..0000000 --- a/package/secubox/secubox-app-localai-wb/files/usr/sbin/localai-wb-ctl +++ /dev/null @@ -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 - -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 Install model from preset or URL - model-remove Remove installed model - -Backend Commands: - backends List available backends - backend-install 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 "; 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 " -} - -cmd_model_install() { - load_config - require_root - - local model_name="$1" - [ -z "$model_name" ] && { echo "Usage: localai-wb-ctl model-install "; 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 "; 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 diff --git a/package/secubox/secubox-app-localai-wb/files/usr/share/localai-wb/build-sdk.sh b/package/secubox/secubox-app-localai-wb/files/usr/share/localai-wb/build-sdk.sh deleted file mode 100644 index 64ddc98..0000000 --- a/package/secubox/secubox-app-localai-wb/files/usr/share/localai-wb/build-sdk.sh +++ /dev/null @@ -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 </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@:/opt/localai/bin/local-ai" - log_info " ssh root@ 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!" diff --git a/package/secubox/secubox-app-localai/Makefile b/package/secubox/secubox-app-localai/Makefile index 974aee8..628d815 100644 --- a/package/secubox/secubox-app-localai/Makefile +++ b/package/secubox/secubox-app-localai/Makefile @@ -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 -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://: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://:8080/v1" - echo "Web UI: http://:8080" - echo "" - echo "Download models with:" - echo " localaictl model-install tinyllama" - echo "" + echo "API: http://:8081/v1" } exit 0 endef diff --git a/package/secubox/secubox-app-localai/files/etc/config/localai b/package/secubox/secubox-app-localai/files/etc/config/localai index 986e739..43848ed 100644 --- a/package/secubox/secubox-app-localai/files/etc/config/localai +++ b/package/secubox/secubox-app-localai/files/etc/config/localai @@ -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' diff --git a/package/secubox/secubox-app-localai/files/etc/init.d/localai b/package/secubox/secubox-app-localai/files/etc/init.d/localai index 239c00a..99f7d6d 100644 --- a/package/secubox/secubox-app-localai/files/etc/init.d/localai +++ b/package/secubox/secubox-app-localai/files/etc/init.d/localai @@ -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() { diff --git a/package/secubox/secubox-app-localai/files/usr/sbin/localaictl b/package/secubox/secubox-app-localai/files/usr/sbin/localaictl index ca379f9..368872b 100644 --- a/package/secubox/secubox-app-localai/files/usr/sbin/localaictl +++ b/package/secubox/secubox-app-localai/files/usr/sbin/localaictl @@ -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 -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 Install model from preset or URL model-remove 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= 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" - return 1 - fi - docker_install "$rt" || return 1 - ;; - *) - log_error "Unknown runtime: $rt" + # 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 - ;; - esac + fi + else + log_error "Failed to download LocalAI" + rm -f "$tmp_file" + return 1 + 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://:$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" - echo "Threads: $threads" + + 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 - echo "API Status: HEALTHY" + # 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 "API Status: NOT RESPONDING" + 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 (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) - - if ! is_running; then - log_error "Container not running" - return 1 + if [ "$1" = "-f" ]; then + logread -f -e localai + else + logread -e localai | tail -100 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 +# ============================================================================= +# BACKEND MANAGEMENT +# ============================================================================= - 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 " } cmd_model_install() { @@ -667,46 +321,56 @@ cmd_model_install() { require_root local model_name="$1" - [ -z "$model_name" ] && { echo "Usage: localaictl model-install "; return 1; } + [ -z "$model_name" ] && { echo "Usage: localaictl model-install "; 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" - 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" + log_info "Model installed: $model_id" + log_info "Restart service to load: /etc/init.d/localai restart" else - log_error "Unknown model: $model_name" + log_error "Download failed" + rm -f "$models_path/$filename" return 1 fi } @@ -719,15 +383,19 @@ cmd_model_remove() { [ -z "$model_name" ] && { echo "Usage: localaictl model-remove "; 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