commit 45ccbccaaf3af49caa61288eafd3b2e6d7112526 Author: ml Date: Wed Sep 30 07:55:26 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7af8cb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# machine-specific files generated by ./setup.sh +Dockerfile +RUN_AIDER.md +.ai-config diff --git a/Dockerfile.template b/Dockerfile.template new file mode 100644 index 0000000..5513777 --- /dev/null +++ b/Dockerfile.template @@ -0,0 +1,17 @@ +# Template - do not build directly. ./setup.sh renders this to ./Dockerfile +# by substituting the @@...@@ placeholders for the model chosen for this machine. +FROM ollama/ollama:latest + +ENV OLLAMA_HOST=0.0.0.0:11434 \ + OLLAMA_CONTEXT_LENGTH=@@CONTEXT@@ \ + OLLAMA_KEEP_ALIVE=30m + +# Bake the model into the image so the container works offline after the build. +RUN set -e; \ + ollama serve > /tmp/ollama-build.log 2>&1 & \ + pid=$!; \ + for i in $(seq 1 60); do ollama list >/dev/null 2>&1 && break; sleep 1; done; \ + ollama pull @@MODEL@@; \ + kill $pid; wait $pid || true + +EXPOSE 11434 diff --git a/README.md b/README.md new file mode 100644 index 0000000..892149e --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# local_ai + +Local coding AI for [aider](https://aider.chat): an Ollama container with a model +chosen for *this* machine. Aider runs on the host and talks to the container. + +```bash +./setup.sh # analyse machine, pick model, generate Dockerfile + RUN_AIDER.md +./build-ai-image.sh # build image (downloads the model) +./run-ai-image.sh # start container on 127.0.0.1:11434 +# then follow RUN_AIDER.md to start aider +``` + +- `models.tsv` – model catalogue (edit to add models) +- `Dockerfile.template` – container template; `setup.sh` fills in the model +- `Dockerfile`, `.ai-config`, `RUN_AIDER.md` – generated per machine (git-ignored) +- `./setup.sh --model --yes` for non-interactive use diff --git a/build-ai-image.sh b/build-ai-image.sh new file mode 100755 index 0000000..7501d86 --- /dev/null +++ b/build-ai-image.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Build the local AI container image (model is downloaded into the image). +set -euo pipefail +cd "$(dirname "$0")" + +[ -f .ai-config ] && [ -f Dockerfile ] || { echo "No Dockerfile/.ai-config yet - run ./setup.sh first." >&2; exit 1; } +# shellcheck disable=SC1091 +source .ai-config + +echo "Building $IMAGE_NAME (model: $MODEL) - this downloads the model, may take a while..." +docker build -t "$IMAGE_NAME" . +echo +echo "Done. Next: ./run-ai-image.sh" diff --git a/models.tsv b/models.tsv new file mode 100644 index 0000000..669fd25 --- /dev/null +++ b/models.tsv @@ -0,0 +1,12 @@ +# ollama_tag disk_gb mem_gb cpu_ok rank description +# disk_gb: download / image size. mem_gb: RAM (or VRAM) needed incl. context cache. +# cpu_ok: yes = usable speed without a GPU (small dense or MoE with few active params). +# rank: higher = better quality for coding; the recommender picks the highest rank that fits. +qwen2.5-coder:1.5b 1.0 2.0 yes 10 Tiny; very fast, weak edits. For very small machines. +qwen2.5-coder:3b 1.9 3.0 yes 20 Small and fast; OK for simple edits. +qwen2.5-coder:7b 4.7 6.5 yes 30 Best all-round choice for laptops / CPU-only. +deepseek-coder-v2:16b 8.9 11.5 yes 35 MoE (2.4B active): fast on CPU, needs ~12 GB RAM. +qwen2.5-coder:14b 9.0 11.5 no 40 Noticeably smarter than 7B; wants a GPU (~12 GB VRAM). +gpt-oss:20b 14 17 yes 50 MoE reasoning model; needs ~17 GB RAM or VRAM. +qwen3-coder:30b 19 23 yes 60 MoE (3B active): strong coder, fast on CPU with 24+ GB RAM. +qwen2.5-coder:32b 20 24 no 70 Top-quality dense coder; needs a 24 GB GPU. diff --git a/run-ai-image.sh b/run-ai-image.sh new file mode 100755 index 0000000..2b59137 --- /dev/null +++ b/run-ai-image.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Start / stop the local AI container. +# ./run-ai-image.sh start (default) +# ./run-ai-image.sh stop stop and remove +# ./run-ai-image.sh logs follow container logs +# ./run-ai-image.sh status show state +set -euo pipefail +cd "$(dirname "$0")" + +[ -f .ai-config ] || { echo "No .ai-config yet - run ./setup.sh first." >&2; exit 1; } +# shellcheck disable=SC1091 +source .ai-config + +running() { [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" = "true" ]; } +exists() { docker inspect "$CONTAINER_NAME" >/dev/null 2>&1; } + +case "${1:-start}" in + stop) exists && docker rm -f "$CONTAINER_NAME" >/dev/null && echo "Stopped." || echo "Not running."; exit 0 ;; + logs) exec docker logs -f "$CONTAINER_NAME" ;; + status) running && echo "Running: http://127.0.0.1:$PORT (model $MODEL)" || echo "Not running."; exit 0 ;; + start) ;; + *) echo "Usage: $0 [start|stop|logs|status]" >&2; exit 1 ;; +esac + +docker image inspect "$IMAGE_NAME" >/dev/null 2>&1 || { echo "Image missing - run ./build-ai-image.sh first." >&2; exit 1; } + +if running; then + echo "Already running on http://127.0.0.1:$PORT" +else + exists && docker rm -f "$CONTAINER_NAME" >/dev/null + args=(-d --name "$CONTAINER_NAME" -p "127.0.0.1:$PORT:11434" --restart unless-stopped) + [ "$USE_GPU" = "1" ] && args+=(--gpus all) + docker run "${args[@]}" "$IMAGE_NAME" >/dev/null + printf "Waiting for the model server" + for _ in $(seq 1 60); do + curl -fs "http://127.0.0.1:$PORT/api/tags" >/dev/null 2>&1 && break + printf "."; sleep 1 + done + echo + curl -fs "http://127.0.0.1:$PORT/api/tags" >/dev/null 2>&1 || { echo "Server did not come up; see: ./run-ai-image.sh logs" >&2; exit 1; } + echo "Running on http://127.0.0.1:$PORT" +fi + +echo +echo "Now, in your project directory:" +echo " OLLAMA_API_BASE=http://127.0.0.1:$PORT aider --model ollama_chat/$MODEL" +echo "(see RUN_AIDER.md for details)" diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..f549d04 --- /dev/null +++ b/setup.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +# Analyse this machine, recommend a local coding model, and generate: +# Dockerfile (from Dockerfile.template, with the chosen model) +# .ai-config (settings used by build-ai-image.sh / run-ai-image.sh) +# RUN_AIDER.md (how to run aider against the container on this machine) +# +# Usage: ./setup.sh [--model ] [--gpu|--no-gpu] [--yes] +set -euo pipefail +cd "$(dirname "$0")" + +FORCE_MODEL=""; FORCE_GPU=""; ASSUME_YES=0 +while [ $# -gt 0 ]; do + case "$1" in + --model) FORCE_MODEL="${2:?--model needs a value}"; shift ;; + --gpu) FORCE_GPU=1 ;; + --no-gpu) FORCE_GPU=0 ;; + --yes|-y) ASSUME_YES=1 ;; + -h|--help) sed -n '2,8p' "$0" | sed 's/^# \?//'; exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac + shift +done + +IMAGE_NAME="local-ai"; CONTAINER_NAME="local-ai"; PORT=11434 +NOTES=() # machine-specific remarks, echoed to the terminal and RUN_AIDER.md + +# ---------------------------------------------------------------- detection +cpu_model=$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2- | xargs || true) +cpu_threads=$(nproc) +ram_gb=$(awk '/MemTotal/ {printf "%.1f", $2/1024/1024}' /proc/meminfo) +disk_gb=$(df -Pk . | awk 'NR==2 {printf "%.0f", $4/1024/1024}') +grep -qw avx2 /proc/cpuinfo && avx2=yes || avx2=no + +gpu_name=""; vram_gb=0; gpu_usable=0; nodriver_gpu=0; est_vram=0 +if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then + gpu_name=$(nvidia-smi --query-gpu=name --format=csv,noheader | head -1) + vram_mb=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | head -1) + vram_gb=$(awk -v m="$vram_mb" 'BEGIN {printf "%.1f", m/1024}') + gpu_usable=1 +elif command -v lspci >/dev/null 2>&1 && lspci | grep -iE 'vga|3d' | grep -qi nvidia; then + gpu_name=$(lspci | grep -iE 'vga|3d' | grep -i nvidia | head -1 | sed 's/.*: //') + nodriver_gpu=1 + # VRAM can't be read without a driver; estimate from the card name (unknown -> 0) + case "$gpu_name" in + *"1650"*|*"1050 Ti"*|*"3050 Mobile"*|*"3050 Laptop"*) est_vram=4 ;; + *"1660"*|*"2060"*|*"1060"*|*"2070"*|*"2080"*|*"3060 Mobile"*|*"3060 Laptop"*|*"3070"*|*"3060 Ti"*) est_vram=6 ;; + *"3060"*|*"4060"*|*"2080 Ti"*|*"3080"*|*"4070"*) est_vram=12 ;; + *"3090"*|*"4090"*) est_vram=24 ;; + esac +fi + +have_docker=no; docker_ok=no; docker_nvidia=no +if command -v docker >/dev/null 2>&1; then + have_docker=yes + if docker info >/dev/null 2>&1; then + docker_ok=yes + docker info 2>/dev/null | grep -qi nvidia && docker_nvidia=yes + fi +fi +if [ "$gpu_usable" = 1 ] && [ "$docker_nvidia" = no ]; then + NOTES+=("GPU $gpu_name found but Docker has no NVIDIA runtime (install nvidia-container-toolkit). Running on CPU.") + gpu_usable=0 +fi +[ "$FORCE_GPU" = 1 ] && gpu_usable=1 +[ "$FORCE_GPU" = 0 ] && gpu_usable=0 +[ "$have_docker" = no ] && NOTES+=("Docker not found - install it before running ./build-ai-image.sh.") +[ "$have_docker" = yes ] && [ "$docker_ok" = no ] && NOTES+=("Docker is installed but not usable by this user (daemon down, or add yourself to the 'docker' group).") +[ "$avx2" = no ] && NOTES+=("CPU has no AVX2; CPU inference will be slow.") +command -v aider >/dev/null 2>&1 && aider_ok=yes || aider_ok=no + +echo "=== This machine ===" +printf " CPU : %s (%s threads, AVX2: %s)\n" "$cpu_model" "$cpu_threads" "$avx2" +printf " RAM : %s GB\n" "$ram_gb" +if [ "$gpu_usable" = 1 ]; then printf " GPU : %s (%s GB VRAM) - will be used\n" "$gpu_name" "$vram_gb" +elif [ -n "$gpu_name" ]; then printf " GPU : %s (not usable)\n" "$gpu_name" +else printf " GPU : none\n"; fi +printf " Disk : %s GB free here\n" "$disk_gb" +printf " Docker : %s aider: %s\n" "$docker_ok" "$aider_ok" +echo + +# ------------------------------------------------ memory budget / context +# What a model must fit into: VRAM when a GPU is used, else RAM minus headroom +# for the OS, aider and the editor. +budget=$(awk -v r="$ram_gb" 'BEGIN {b=r-4; if (b<1) b=1; printf "%.1f", b}') +gpu_budget=$(awk -v v="$vram_gb" 'BEGIN {printf "%.1f", v*0.9}') + +if awk -v b="$budget" 'BEGIN{exit !(b<6)}'; then CONTEXT=4096 +elif awk -v b="$budget" 'BEGIN{exit !(b>=40)}'; then CONTEXT=32768 +elif awk -v b="$budget" 'BEGIN{exit !(b>=20)}'; then CONTEXT=16384 +else CONTEXT=8192; fi + +# ---------------------------------------------------------------- catalogue +mapfile -t rows < <(grep -v '^#' models.tsv | grep -v '^[[:space:]]*$' | sort -t$'\t' -k5,5n) +tags=(); status=(); speed=(); rec=-1 +for i in "${!rows[@]}"; do + IFS=$'\t' read -r tag disk mem cpu_ok rank desc <<<"${rows[$i]}" + tags[i]=$tag + fits_ram=$(awk -v m="$mem" -v b="$budget" 'BEGIN{print (m<=b)}') + fits_gpu=$(awk -v m="$mem" -v b="$gpu_budget" -v u="$gpu_usable" 'BEGIN{print (u && m<=b)}') + if [ "$fits_gpu" = 1 ]; then status[i]="fits"; speed[i]="fast (GPU)" + elif [ "$fits_ram" = 1 ] && [ "$cpu_ok" = yes ]; then status[i]="fits"; speed[i]="ok (CPU)" + elif [ "$fits_ram" = 1 ]; then status[i]="slow"; speed[i]="slow (CPU)" + else status[i]="too big"; speed[i]="-"; fi + [ "${status[$i]}" = fits ] && rec=$i # sorted by rank, so last "fits" wins +done +[ "$rec" -ge 0 ] || rec=0 + +# ------------------------------------------- would a GPU driver help? +if [ "$nodriver_gpu" = 1 ]; then + best=-1 + if awk -v v="$est_vram" 'BEGIN{exit !(v>0)}'; then + for i in "${!rows[@]}"; do + mem=$(cut -f3 <<<"${rows[$i]}") + awk -v m="$mem" -v v="$est_vram" 'BEGIN{exit !(m<=v*0.9)}' && best=$i + done + fi + drv="install the NVIDIA driver and nvidia-container-toolkit, then re-run ./setup.sh" + if [ "$est_vram" = 0 ]; then + NOTES+=("NVIDIA GPU ($gpu_name) found but not usable (no driver). Installing the driver may speed things up a lot if it has enough VRAM: $drv.") + elif [ "$best" -gt "$rec" ]; then + NOTES+=("Worth installing the NVIDIA driver: with ~${est_vram} GB VRAM, ${tags[$best]} would run fully on the GPU (better than the current CPU pick ${tags[$rec]}). $drv.") + elif [ "$best" -ge 0 ]; then + NOTES+=("Optional: an NVIDIA driver would make models up to ${tags[$best]} run fully on the ~${est_vram} GB GPU (much faster) and partly speed up larger ones such as ${tags[$rec]} via partial offload, but ${est_vram} GB VRAM does not unlock bigger models than ${tags[$rec]}. Worth it for speed: $drv.") + else + NOTES+=("A driver would not help much: ~${est_vram} GB VRAM is too small to hold even the smallest model.") + fi +fi + +# --------------------------------------------------------------------- menu +if [ -z "$FORCE_MODEL" ]; then + echo "=== Models (budget: ${budget} GB RAM$([ "$gpu_usable" = 1 ] && echo ", ${gpu_budget} GB VRAM")) ===" + printf " %-3s %-24s %7s %7s %-9s %-11s %s\n" "#" "model" "disk" "memory" "fit" "speed" "" + for i in "${!rows[@]}"; do + IFS=$'\t' read -r tag disk mem cpu_ok rank desc <<<"${rows[$i]}" + mark=" "; [ "$i" = "$rec" ] && mark="*" + printf " %s%-3s %-24s %5s GB %5s GB %-9s %-11s %s\n" "$mark" "$((i+1))" "$tag" "$disk" "$mem" "${status[$i]}" "${speed[$i]}" "$desc" + done + echo + echo " * = recommended for this machine: ${tags[$rec]}" + echo + if [ "$ASSUME_YES" = 1 ]; then choice=""; else read -r -p "Select model number [$((rec+1))]: " choice; fi + choice=${choice:-$((rec+1))} + [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#rows[@]}" ] || { echo "Invalid choice." >&2; exit 1; } + sel=$((choice-1)) + MODEL=${tags[$sel]} + if [ "${status[$sel]}" = "too big" ] || [ "${status[$sel]}" = "slow" ]; then + echo "Warning: $MODEL is '${status[$sel]}' on this machine (may swap, crawl, or crash)." + if [ "$ASSUME_YES" != 1 ]; then read -r -p "Continue anyway? [y/N] " a; [[ "$a" =~ ^[Yy] ]] || exit 1; fi + fi + sel_disk=$(cut -f2 <<<"${rows[$sel]}") +else + MODEL=$FORCE_MODEL; sel_disk=0 + for r in "${rows[@]}"; do [ "$(cut -f1 <<<"$r")" = "$MODEL" ] && sel_disk=$(cut -f2 <<<"$r"); done +fi +if awk -v d="$sel_disk" -v f="$disk_gb" 'BEGIN{exit !(d*2>f)}'; then + NOTES+=("Low disk space: ${disk_gb} GB free, the build needs roughly 2x the model size (${sel_disk} GB).") +fi + +# ------------------------------------------------------------------- output +USE_GPU=$gpu_usable +sed -e "s|@@MODEL@@|$MODEL|g" -e "s|@@CONTEXT@@|$CONTEXT|g" Dockerfile.template > Dockerfile +cat > .ai-config < RUN_AIDER.md + +echo +echo "Selected: $MODEL (context $CONTEXT, GPU: $([ "$USE_GPU" = 1 ] && echo yes || echo no))" +for n in "${NOTES[@]}"; do echo "Note: $n"; done +echo "Generated: Dockerfile .ai-config RUN_AIDER.md" +echo +echo "Next steps:" +echo " ./build-ai-image.sh" +echo " ./run-ai-image.sh" +echo " then follow RUN_AIDER.md to start aider"