diff --git a/.env.example b/.env.example index 619dbc4..5108412 100644 --- a/.env.example +++ b/.env.example @@ -116,11 +116,22 @@ LLAMA_FAST_MODEL_FILE=Qwen3-4B-Instruct-2507-UD-Q8_K_XL.gguf # Same reasoning as LLAMA_GPU_LAYERS above — full GPU offload, this model # is dense too. LLAMA_FAST_GPU_LAYERS=999 -# Classifier transcripts are truncated/bounded by qwen-code itself (see its -# own Auto Mode docs) — no need for anywhere near the 27B's huge context. -# 8192 keeps this instance's KV cache negligible. +# --ctx-size is the TOTAL across every LLAMA_FAST_PARALLEL slot, not per +# request — same halving already called out for the main model above. +# Was PARALLEL=2, silently halving this to 4096/slot — too small: a real +# classifier call (hints + environment + recent tool-call history) hit +# "exceeds the available context size (4096 tokens)" in practice, which +# qwen-code surfaces as "Auto Mode couldn't classify this action +# (Classifier stage 1 unavailable)" — see issue #5. Fixed by dropping to +# a single slot instead of raising ctx-size (no extra VRAM, and this +# service doesn't need concurrent classifier calls the way the main +# model needs concurrent chat sessions) — the full 8192 now goes to the +# one slot. If hints.allow/softDeny/hardDeny ever approach their +# 50-entries-each ceiling, raise LLAMA_FAST_CTX_SIZE instead — qwen-code +# caps those at 200 chars x 150 entries plus 40,000 chars of +# historical-action context, which can exceed 8192 tokens worst-case. LLAMA_FAST_CTX_SIZE=8192 -LLAMA_FAST_PARALLEL=2 +LLAMA_FAST_PARALLEL=1 # --- ComfyUI diffusion model (Qwen-Image, FP8 — see docs/research/ # image-generation-model-choice.md and issue #42) --- diff --git a/.gitignore b/.gitignore index 74c31ad..77761ae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ # to be committed to this repo. data/ .leankg/ +.cache/ diff --git a/docker-compose.yml b/docker-compose.yml index 4e18455..5c0ed1d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,7 +70,7 @@ services: --port 8080 --n-gpu-layers ${LLAMA_FAST_GPU_LAYERS:-999} --ctx-size ${LLAMA_FAST_CTX_SIZE:-8192} - --parallel ${LLAMA_FAST_PARALLEL:-2} + --parallel ${LLAMA_FAST_PARALLEL:-1} --flash-attn on --cache-type-k q8_0 --cache-type-v q8_0 diff --git a/scripts/update.sh b/scripts/update.sh index 237be03..d3c9054 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -4,6 +4,18 @@ # exist, pulls, validates, rebuilds/re-pulls images, and recreates only what # changed — safe to run any time, including with nothing to do. # +# Tunable config values (LLAMA_*, ports, timeouts — anything with a real +# default in .env.example) are synced from .env.example every run. A value +# already matching is left alone silently. A value that DIFFERS from the +# server's current .env is a conflict: interactively, you're shown every +# conflict on one screen (via gum) and choose which to accept — unpicked +# keys keep the server's current value. Non-interactively (no TTY — cron, +# CI, piped), any conflict is a hard error unless --force is passed, which +# accepts every new value automatically. Secrets and host-resolved values +# (blank in .env.example — OMNIROUTE_*_SECRET/_KEY/_SALT/_PASSWORD, +# SEARXNG_LAN_IP, COMFYUI_PUID/PGID, HOST_VIDEO_GID/RENDER_GID) are never +# touched by this — they keep going through set_if_blank as before. +# # omniroute's own routing/provider config (llama-server, search) lives in # its dashboard, not a checked-in file like the old litellm-config.yaml — # see issue #31 and docs/proxy-key-onboarding.md. @@ -14,6 +26,14 @@ set -euo pipefail cd "$(dirname "$0")/.." +FORCE=false +for arg in "$@"; do + case "$arg" in + --force) FORCE=true ;; + *) echo "Usage: $0 [--force]" >&2; exit 1 ;; + esac +done + # Must run before anything else touches a file this script itself reads # (docker-compose.yml, .env.example, this script's own remaining lines) — # a self-updating script isn't guaranteed atomic against its own file @@ -27,6 +47,106 @@ git pull --ff-only [ -f .env ] || cp .env.example .env +echo "==> syncing tracked config values from .env.example" +# ponytail: gum (charmbracelet/gum) is a single static binary, fetched as a +# release tarball — no build step, no package-manager dependency. Cached +# under .cache/gum/ (gitignored) so repeat runs don't re-download. Release +# asset naming (gum__Linux_.tar.gz) follows charm's standard +# goreleaser convention but hasn't been exercised against a real download +# on this exact host yet — if it 404s, check +# https://github.com/charmbracelet/gum/releases for the current naming. +GUM_VERSION="0.14.5" +GUM_DIR="$(pwd)/.cache/gum" +GUM_BIN="$GUM_DIR/gum" +ensure_gum() { + command -v gum >/dev/null 2>&1 && { echo "gum"; return; } + [ -x "$GUM_BIN" ] && { echo "$GUM_BIN"; return; } + mkdir -p "$GUM_DIR" + local arch tmpdir url + case "$(uname -m)" in + x86_64) arch="x86_64" ;; + aarch64|arm64) arch="arm64" ;; + *) echo "no gum build for $(uname -m), falling back to plain prompts" >&2; echo ""; return ;; + esac + url="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_${arch}.tar.gz" + tmpdir="$(mktemp -d)" + if curl -fsSL "$url" | tar -xz -C "$tmpdir" 2>/dev/null; then + find "$tmpdir" -name gum -type f -exec cp {} "$GUM_BIN" \; + chmod +x "$GUM_BIN" 2>/dev/null || true + fi + rm -rf "$tmpdir" + [ -x "$GUM_BIN" ] && echo "$GUM_BIN" || echo "" +} + +# Collect every key where .env.example has a real (non-blank) default: +# missing from .env -> just add it (no conflict, nothing to decide); +# present and identical -> leave alone silently; present and different -> +# a conflict to resolve below. +conflict_keys=() +conflict_old=() +conflict_new=() +while IFS='=' read -r key value; do + [ -n "$value" ] || continue + current="$(grep -E "^${key}=" .env | head -1 | cut -d= -f2-)" + if ! grep -qE "^${key}=" .env; then + echo "${key}=${value}" >> .env + elif [ "$current" != "$value" ]; then + conflict_keys+=("$key") + conflict_old+=("$current") + conflict_new+=("$value") + fi +done < <(grep -E '^[A-Za-z_][A-Za-z0-9_]*=.+' .env.example) + +if [ "${#conflict_keys[@]}" -gt 0 ]; then + if [ "$FORCE" = true ]; then + for i in "${!conflict_keys[@]}"; do + key="${conflict_keys[$i]}"; new="${conflict_new[$i]}" + sed -i "s|^${key}=.*|${key}=${new}|" .env + echo "${key}: ${conflict_old[$i]} -> ${new} (--force)" + done + elif [ ! -t 0 ] || [ ! -t 1 ]; then + echo "ERROR: ${#conflict_keys[@]} config value(s) in .env differ from .env.example, and this isn't an interactive terminal:" >&2 + for i in "${!conflict_keys[@]}"; do + echo " ${conflict_keys[$i]}: ${conflict_old[$i]} (current) vs ${conflict_new[$i]} (.env.example)" >&2 + done + echo "Re-run interactively to choose per-key, or pass --force to accept every new value." >&2 + exit 1 + else + gum_bin="$(ensure_gum)" + labels=() + for i in "${!conflict_keys[@]}"; do + labels+=("${conflict_keys[$i]}: ${conflict_old[$i]} -> ${conflict_new[$i]}") + done + if [ -n "$gum_bin" ]; then + selected="$(printf '%s\n' "${labels[@]}" | "$gum_bin" choose --no-limit --selected "$(printf '%s\n' "${labels[@]}" | paste -sd,)" --header "Config differs from .env.example — selected keys take the new value, unselected keep the server's current value:")" + else + # ponytail: plain-bash fallback if gum couldn't be fetched (offline, + # unsupported arch) — same one-screen-of-conflicts idea, cruder UI. + echo "Config differs from .env.example. Enter space-separated numbers to KEEP the server's current value (all others take the new value), or press enter to take every new value:" + for i in "${!conflict_keys[@]}"; do + echo " $((i+1))) ${labels[$i]}" + done + read -r -p "> " keep_nums + selected="" + for i in "${!conflict_keys[@]}"; do + case " $keep_nums " in + *" $((i+1)) "*) ;; + *) selected="${selected}${labels[$i]}"$'\n' ;; + esac + done + fi + for i in "${!conflict_keys[@]}"; do + key="${conflict_keys[$i]}"; new="${conflict_new[$i]}" + if printf '%s\n' "$selected" | grep -qxF "${labels[$i]}"; then + sed -i "s|^${key}=.*|${key}=${new}|" .env + echo "${key}: ${conflict_old[$i]} -> ${new}" + else + echo "${key}: kept ${conflict_old[$i]} (server value)" + fi + done + fi +fi + # Handles all three cases: the KEY=value line is missing entirely (.env # predates that var being added to .env.example — sed can't fix what isn't # there, so this appends it), present but blank, or already set.