Commit Graph
89 Commits
Author SHA1 Message Date
haylan 5b7548dc7c Merge pull request 'Fix llama-server-fast context-size exhaustion breaking Auto Mode' (#49) from fix-fastmodel-context-size into main
Reviewed-on: #49
2026-09-06 20:21:53 +00:00
haylan ea7b05fb99 feat: interactive per-key conflict resolution in update.sh's config sync
Replaces the previous commit's blind force-overwrite with a real
choice, per user feedback: force-overwriting server config without
asking was the wrong default.

- A tracked config value (real default in .env.example) that already
  matches .env is left alone silently — no prompt, no noise.
- A value that DIFFERS is a conflict, shown on one screen (all
  conflicts together, not one prompt per key) via gum
  (charmbracelet/gum) — single static binary fetched as a release
  tarball into .cache/gum/ (gitignored), no build step, no package
  manager dependency. Falls back to a plain read-based prompt if gum
  can't be fetched (offline, unsupported arch).
- Non-interactive (no TTY — cron, CI, piped): any conflict is a hard
  error (exit 1, lists every conflicting key) unless --force is
  passed, which accepts every new value automatically — matches how
  this PR's own fix needs to land unattended.
- Secrets and host-resolved values are completely unaffected either
  way — untouched by this loop, same as before.

Verified in an isolated sandbox against the exact scenario from this
PR (stale LLAMA_FAST_PARALLEL=2 vs git's 1):
- no TTY, no --force: exits 1, prints the diff, doesn't touch .env
- no TTY, --force: LLAMA_FAST_PARALLEL corrected 2 -> 1, an
  OMNIROUTE secret confirmed untouched (not regenerated)
docker compose config -q still passes.

Not verified: the interactive gum path itself (needs a real TTY,
couldn't allocate a pty in this sandbox) — worth confirming for real
on the server, including that gum's release asset naming actually
matches what ensure_gum() expects.

Refs #5
2026-09-06 22:20:27 +02:00
haylan 9def240a8e feat: update.sh force-syncs tracked config from .env.example
Follows directly from the previous commit's caveat: this PR's own fix
(LLAMA_FAST_PARALLEL=2 -> 1) wouldn't have taken effect on the server
without a manual .env edit, because set_if_blank never touches an
already-set value — by design, for secrets, but the same logic was
silently protecting stale copies of ordinary tunable config too.

Every KEY=VALUE line in .env.example with a real (non-blank) default
is now force-synced into .env on every run. Secrets and host-resolved
values are unaffected — .env.example already leaves those blank on
purpose, so the sync loop naturally skips them and they keep going
through set_if_blank as before.

Trade-off, called out in both the script's header and the sync loop's
own comment: there's no such thing as a persistent server-only
override for these keys anymore — a hand-edited value not reflected
in git gets reverted on the next run. That's the intended behavior.

Verified against a simulated stale .env matching the real scenario
from this PR: LLAMA_FAST_PARALLEL correctly overwritten 2 -> 1, an
OMNIROUTE secret left untouched. bash -n and docker compose config -q
both pass.

Refs #5
2026-09-06 22:10:51 +02:00
haylanandClaude-Bot 52a92f6508 fix: llama-server-fast context-size exhaustion breaking Auto Mode classifier
Real failure: "Auto Mode couldn't classify this action (Classifier
stage 1 unavailable)". Reproduced directly against the server:

  {"error":{"message":"[400]: request (6186 tokens) exceeds the
  available context size (4096 tokens)"...

LLAMA_FAST_CTX_SIZE=8192 is the TOTAL across every LLAMA_FAST_PARALLEL
slot, not per-request — the main model's own .env.example comment
already calls this out, missed it when llama-server-fast was set up
(#44). With PARALLEL=2 that's 4096/slot, too small for a real
classifier call (hints + environment + recent tool-call history).

Fixed by dropping to a single slot (LLAMA_FAST_PARALLEL=1) rather than
raising ctx-size — this service doesn't need concurrent classifier
calls the way the main model needs concurrent chat sessions, so this
costs no extra VRAM. The full 8192 now goes to the one slot.

docker compose config -q validated.

Refs #5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnMEdzeQzqZE5soVEXPCx
2026-09-06 22:06:59 +02:00
haylan 63938e95c9 Merge pull request 'Run git pull first in update.sh, not mid-script' (#48) from fix-update-sh-pull-order into main
Reviewed-on: #48
2026-09-06 19:40:10 +00:00
haylan 7b6d3f5802 fix: run git pull first in update.sh, not mid-script
Real failure on the server after #47 merged: the old GID-resolution
code ran (found the old COMFYUI_VIDEO_GID/COMFYUI_RENDER_GID vars
"already set"), then git pull swapped every file on disk out from
under the still-running script — including docker-compose.yml, now
requiring HOST_VIDEO_GID/HOST_RENDER_GID — but the resolution step
that would populate those had already run under the old code and
never re-ran. compose validation then failed on the new required
vars that were never set.

A self-updating script isn't atomic against its own file changing
mid-run. Move git pull to the very first thing the script does, so
every run is consistently either fully old or fully new code, never
a mix.

bash -n and docker compose config -q both validated.
2026-09-06 21:38:18 +02:00
haylan 386a41200f Merge pull request 'Fix GPU pinned at 100% with two containers, flaky render group' (#47) from fix-gpu-pin-and-render-group into main
Reviewed-on: #47
2026-09-06 19:35:31 +00:00
haylanandClaude-Bot 75033dacd7 fix: GPU pinned at 100% with two llama.cpp containers, flaky render group
Two real-hardware findings from issue #5, both researched and fixed
together (docs/research/rocm-gpu-pin-and-render-group.md):

- GPU_MAX_HW_QUEUES=1 on llama-server and llama-server-fast. Confirmed
  on real hardware: either container alone is fine (3% GPU, low
  power), only two concurrent HIP contexts pin the R9700 at 100%/
  boost-clock (ROCm/ROCm#5706, an MES firmware bug). The env var is
  validated on the exact image this stack uses, per-process by design
  — applying it to both containers is the correct scope.

- group_add switched from plain names (video/render) to resolved
  numeric GIDs (HOST_VIDEO_GID/HOST_RENDER_GID) on all three GPU
  services. The "unable to find group render: no matching entries in
  group file" error confirmed new since the second GPU service was
  added is a known Docker bug (docker/cli#4714): group_add by name
  resolves against the container's own /etc/group, not the host's,
  and multiple GPU services starting concurrently race on that lookup.
  Numeric GIDs skip resolution entirely. scripts/update.sh's existing
  comfyui-only GID resolution is generalized to resolve these once for
  all three services.

docker compose config -q validated (fails fast with a clear error if
the GIDs aren't resolved yet, passes once they are).

Refs #5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnMEdzeQzqZE5soVEXPCx
2026-09-06 21:33:40 +02:00
haylan 1ee2e76033 Merge pull request 'Downloader for Qwen-Image weights, switch-model.sh' (#46) from comfyui-model-and-switch-script into main
Reviewed-on: #46
2026-09-06 19:01:02 +00:00
haylanandClaude-Bot 4b47a1769d feat: downloader for Qwen-Image weights, switch-model.sh script
Two frontier tickets from the ComfyUI map (#38), both unblocked now
that their blockers (#39 model choice, #40 lazytainer research) are
resolved.

- downloader-comfyui service (docker-compose.yml) + COMFYUI_* vars
  (.env.example): fetches Qwen-Image FP8 diffusion/text-encoder/VAE
  weights from Comfy-Org/Qwen-Image_ComfyUI, same test -f guard
  pattern as the existing downloaders. Closes #42.
- scripts/switch-model.sh: swaps GPU residency between llama-server
  and comfyui via direct `docker compose stop`/`up -d`, bypassing
  lazytainer per docs/research/lazytainer-omniroute-idle-stop.md
  (its packet-threshold detector can't distinguish OmniRoute's health
  checks from real traffic, so idle-stop can't be relied on for a
  deliberate swap). llama-server-fast stays resident throughout —
  not part of this swap. Closes #43.
- scripts/update.sh: runs the new downloader profile.

docker compose config -q validated clean.

Refs #38, #42, #43

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnMEdzeQzqZE5soVEXPCx
2026-09-06 20:57:38 +02:00
haylan 1932981f09 Merge pull request 'Add llama-server-fast: small non-thinking classifier/fast model' (#45) from add-fast-model into main
Reviewed-on: #45
2026-09-06 18:39:30 +00:00
haylanandClaude-Bot d984c10835 feat: add llama-server-fast, a small non-thinking classifier model
Second, always-resident llama.cpp instance (Qwen3-4B-Instruct-2507,
Q8_0 GGUF, ~5GB VRAM) alongside the existing Qwen3.8-27B instance, for
use as qwen-code CLI's Auto Mode classifier fastModel. Model choice
researched in docs/research/fast-model-choice.md: architecturally
non-thinking (unlike Qwen3-1.7B/0.6B), --reasoning off added
defensively per a known (closed) llama.cpp misdetection bug.

- docker-compose.yml: llama-server-fast + downloader-fast services,
  omniroute depends_on updated
- .env.example: LLAMA_FAST_* vars
- scripts/update.sh: runs the new downloader profile

Refs #44

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnMEdzeQzqZE5soVEXPCx
2026-09-06 20:24:59 +02:00
haylan 71c9003bd8 Create dashscope-websearch-selfhost-options.md 2026-09-06 16:10:26 +02:00
haylanandClaude-Bot ed83fca05c feat(comfyui): add local image-gen service (#41)
Adds the comfyui service (yurisasc/comfyui-rocm7.1, gfx1201-tuned for the
R9700) on the ai-stack network, published on host port 8138 for a planned
external nginx route to comfy.home. Resolves PUID/PGID/VIDEO_GID/RENDER_GID
from the host in scripts/update.sh, same pattern as SEARXNG_LAN_IP.

OmniRoute provider registration (http://comfyui:8188) is still the same
manual dashboard/POST-/api/providers flow already used for llama-server —
not scripted, per docs/proxy-key-onboarding.md.

Part of wayfinder map #38.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zCwaWJQuKgXDUfRPBqDS7
2026-09-05 22:40:07 +02:00
haylanandClaude-Bot 7d1ff2f54f docs(research): add image-gen backend survey and omniroute/qwen websearch notes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zCwaWJQuKgXDUfRPBqDS7
2026-09-05 21:24:23 +02:00
haylanandClaude-Bot ac3f730f83 docs(research): recommend Qwen-Image FP8 for full-VRAM diffusion build
Closes issue #39 — with llama-server stopped and the full ~32GB R9700
available, Qwen-Image (Apache-2.0, 20B MMDiT) at FP8 precision (~25GB)
is the recommended upgrade from FLUX.1-schnell: it has the cleanest
license of the candidates and is the only one with a ComfyUI workflow
pre-validated specifically on this GPU architecture (gfx1201/R9700),
per kyuz0/amd-r9700-comfy. HunyuanImage-3.0 is ruled out (CUDA-only,
multi-GPU datacenter VRAM floor); Krea-2 flagged as promising but too
new for R9700-specific field evidence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zCwaWJQuKgXDUfRPBqDS7
2026-09-05 21:03:13 +02:00
haylanandClaude-Bot 451d5c7b28 docs(research): confirm lazytainer/omniroute idle-stop conflict (#40)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zCwaWJQuKgXDUfRPBqDS7
2026-09-05 20:57:24 +02:00
haylanandClaude-Bot d8736b6dd7 fix(llama-server): quantize KV cache, restore full 262144 context
A real session hit 'request (66192 tokens) exceeds the available
context size (65536 tokens)' — --parallel 2 was splitting the
131072 total into 65536/slot, too small for actual usage.

Add --cache-type-k/v q8_0 (roughly halves KV memory) so the model's
true max context (262144, already the documented .env.example goal)
fits in the same ~25.6GB/6GB-headroom footprint the old 131072 fp16
setting used, instead of shrinking per-slot context to fit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 12:00:07 +02:00
haylanandClaude-Bot 5767f548c3 perf(llama-server): enable flash attention
Cuts prefill time with no accuracy cost, per markaicode.com's
llama.cpp timeout writeup — directly helps the prefill-vs-stream-idle
contention issue fixed in the prior two commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 11:32:17 +02:00
haylanandClaude-Bot 633292b291 fix(omniroute): raise stream-idle timeout to 180s
Contended prefill (see LLAMA_PARALLEL) can outrun the ~95s dashboard
value before first token, causing OmniRoute to cancel still-working
requests. Track it in git via STREAM_IDLE_TIMEOUT_MS instead of a
dashboard-only setting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 11:29:12 +02:00
haylanandClaude-Bot 23e90fe8fb fix(llama-server): cap concurrent slots at 2 to curb prefill contention
Default --parallel of 4 let concurrent subagent requests split GPU
compute, pushing large-context prefill past OmniRoute's stream-idle
timeout and triggering cancel-on-both-sides.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 11:26:30 +02:00
haylan ae812cd9e0 feat(ctx): larger context size 2026-09-04 21:47:38 +02:00
haylan 9e9cac254b feat(stack): drop qdrant and embedding-server, use OmniRoute's built-in memory
OmniRoute's memory feature is self-contained: its bundled sqlite-vec
vector store plus a local ONNX embedding model (Transformers.js,
~400MB, fetched into the omniroute-data volume on first use) replace
the external qdrant + bge-small-en-v1.5 embedding-server pair, which
was never wired up in the dashboard. Two fewer containers, no
second GGUF download, no EMBEDDING_MODEL_FILE var. Memory stays
opt-in via the dashboard (Settings -> Memory, transformers source);
nothing here changes the gateway's static config.
2026-09-03 22:19:21 +02:00
haylanandClaude-Bot 9e1362c22c feat(omniroute): add dedicated embedding-server for memory feature
llama.cpp loads one model per process and the running Qwen3.8-27B chat
model isn't embedding-trained, so this is a second, CPU-only
llama-server instance (bge-small-en-v1.5, 384-dim) rather than adding
--embeddings to the chat one — see docs/research/litellm-knowledgebase.md
#3.

Downloader extended to fetch both GGUFs into the shared models volume.
No host port published — OmniRoute reaches it via the ai-stack network
DNS name (embedding-server:8081).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qx22CV9EUS3hGATucQav13
2026-09-03 21:43:20 +02:00
haylanandClaude-Bot 885de477ba feat(stack): bring back qdrant as OmniRoute's memory vector store
Bare service, no static config — wired up as a memory provider by
hand in the OmniRoute dashboard. Not published to the host; only
OmniRoute (same ai-stack network) talks to it. Also drops two stale
comments left over from the OMNIROUTE_PORT:4000 removal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qx22CV9EUS3hGATucQav13
2026-09-03 21:19:06 +02:00
haylanandClaude-Bot c795993a64 fix(omniroute): publish API and dashboard ports directly, drop OMNIROUTE_PORT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qx22CV9EUS3hGATucQav13
2026-09-03 20:49:25 +02:00
haylanandClaude-Bot c38375c0f4 fix(omniroute): add required WS bridge secret, memory ceiling, shutdown grace period
Cross-checked the deployment against OmniRoute's own docs
(docs/reference/ENVIRONMENT.md, docs/guides/DOCKER_GUIDE.md) and found
three gaps from the original migration:

- OMNIROUTE_WS_BRIDGE_SECRET was entirely missing - ENVIRONMENT.md marks
  it REQUIRED (production), for the internal Codex Responses WebSocket
  bridge. docker compose config validated fine without it (compose
  doesn't know omniroute's own required-var list), so this went
  unnoticed until checking the docs directly.
- No mem_limit/OMNIROUTE_MEMORY_MB - the Docker guide is explicit that
  the 1024MB default heap is dashboard-only sized; coding-agent workloads
  (every client this stack has) need OMNIROUTE_MEMORY_MB=8192 and a
  10+ GiB container ceiling. Set both.
- No stop_grace_period - the guide's --stop-timeout 40 equivalent, so
  SQLite WAL changes checkpoint back into the main DB file on shutdown
  instead of getting killed mid-write.

Redis checked and confirmed correctly absent - OmniRoute uses SQLite
only, no Redis anywhere in its docs.

Still open: whether API_PORT actually isolates /dashboard and /api/*
from the published port, or bridges everything through (see issue #31)
- OmniRoute's own ARCHITECTURE.md doesn't document split-port mode as a
real security boundary, and the live "[API Bridge] ... -> dashboard"
log line is ambiguous. Waiting on a live curl test against
proxy-ai.home before treating that as resolved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPZ6TogJiYxG8E4EQBB197
2026-09-03 20:37:49 +02:00
haylanandClaude-Bot 6132e6263e docs: fix gateway hostname to proxy-ai.home/proxy-ai.haylan.ch
Docs said proxy.ai.home (dot) throughout, but the actual NPM Proxy Host
is proxy-ai.home (hyphen) - confirmed with the user, who already has the
reverse proxy pointing :4000 at http://proxy-ai.home/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPZ6TogJiYxG8E4EQBB197
2026-09-03 20:28:21 +02:00
haylanandClaude-Bot 90ef1a1061 fix(omniroute): keep the gateway published on host port 4000
Not omniroute's own internal port (API_PORT stays at its default 20129,
unreconfigured) - just the Docker port mapping, so existing NPM/firewall
config pointed at :4000 keeps working without changes on that end. New
OMNIROUTE_PORT env var is the host side of "OMNIROUTE_PORT:API_PORT" in
docker-compose.yml's ports: entry.

Also corrected docs/proxy-key-onboarding.md's dashboard-access
instructions - DASHBOARD_PORT was never published to the host in the
first place, so "http://<host>:20128" was never actually reachable as
written; documented reaching it via the container's own bridge-network IP
or an SSH port-forward instead.

llama-server remains unexposed (no ports: entry, only expose:) -
unaffected by this change, confirming it stays that way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPZ6TogJiYxG8E4EQBB197
2026-09-03 20:17:08 +02:00
haylanandClaude-Bot 977e9d3dd7 fix(omniroute): healthcheck used python3, which the image doesn't have
Confirmed live on the R9700: omniroute starts up fine ("[API Bridge]
Listening on 0.0.0.0:20129") but docker reported it unhealthy forever -
the healthcheck's python3 -c "..." command can never run (which python3
wget curl node found only node in the image), so it failed every single
check regardless of actual app health.

Switched to a node-based TCP-connect check on the same port instead of an
HTTP GET against /healthz - OmniRoute's own Docker guide already treats a
bare TCP probe as an acceptable liveness check, and this sidesteps needing
to confirm /healthz's exact path/response shape on this image.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPZ6TogJiYxG8E4EQBB197
2026-09-03 20:08:58 +02:00
haylanandClaude-Bot 3bbda098b3 feat(stack): remove Open WebUI and Qdrant
No longer needed - every client is a coding CLI behind the OmniRoute
gateway, not a chat UI. Drops the open-webui and qdrant services,
WEBUI_PORT/OPENWEBUI_OMNIROUTE_KEY env vars, and the openwebui-data/
qdrant-data volumes. Qdrant only ever served Open WebUI's own built-in
memory/RAG (unrelated to the gateway-level knowledgebase removed in
472e3a4), so it goes too rather than sit unused.

Docs updated: README, docs/network-access.md (ai.home/ai.haylan.ch
section was entirely about Open WebUI, rewritten around the gateway),
docs/proxy-key-onboarding.md, docs/proxy-request-priority.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPZ6TogJiYxG8E4EQBB197
2026-09-03 19:55:31 +02:00
haylanandClaude-Bot 472e3a4738 feat(gateway): migrate LiteLLM to OmniRoute, drop the memory/knowledgebase feature
LiteLLM -> OmniRoute (issue #31, wayfinder map + research tickets #32-37):
replace the litellm/litellm-db services with omniroute, split-port mode
(API_PORT published/reverse-proxied, DASHBOARD_PORT never published -
tighter than litellm's old /ui NPM path-deny rule), 5 new secrets in place
of LITELLM_MASTER_KEY/LITELLM_SALT_KEY, llama-server/searxng registered as
omniroute providers post-boot (no static config.yaml equivalent). No
scripted per-workload key minting yet - omniroute's POST /api/keys needs a
dashboard session, not a static bearer key - so OPENWEBUI_OMNIROUTE_KEY is
a manual step for now (docs/proxy-key-onboarding.md).

Caveat carried into the map and README: OmniRoute's own docs
(docs/security/STEALTH_GUIDE.md, MITM-TPROXY-DECRYPT.md, PUBLIC_CREDS.md
on its release/v3.8.51 branch) describe shipped features for AI-provider
client-detection evasion, system-wide HTTPS interception via a locally
installed root CA, and hiding credentials from secret scanners. Proceeding
anyway was an explicit, informed user decision.

Also drops the gateway-level memory/knowledgebase feature entirely (user:
"I don't need it") - litellm-pgvector, pgvector-db, embedding-server,
scripts/ingest-memory.sh, vendor/litellm-pgvector/, docs/memory-
knowledgebase.md. Open WebUI's own qdrant-backed memory/RAG is unrelated
and untouched. litellm-config.yaml deleted (was kept as a rollback
reference, but there's no rollback path to a feature being deliberately
removed).

Not yet verified against real hardware - see issue #31's open tickets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPZ6TogJiYxG8E4EQBB197
2026-09-03 19:49:35 +02:00
haylanandClaude-Bot 4fe910a5f3 docs(agents): fix tea comment syntax, note map-edit race condition
tea comments create doesn't exist (add/a is the subcommand); a wayfinder
session hit this live resolving OmniRoute-migration tickets. Also note
that concurrent ticket resolutions racing to edit the same map issue
(a full-body replace, no append) can clobber each other's Decisions-so-far
lines -- observed the same session across 5 parallel research tickets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPZ6TogJiYxG8E4EQBB197
2026-09-03 19:29:10 +02:00
haylan 6b06d6001f Merge pull request 'feat(llama.cpp): raise default context to 128K, document RAM/SSD offload knobs' (#30) from ctx-size-128k into main
Reviewed-on: #30
2026-09-03 04:44:34 +00:00
haylanandClaude-Bot c5864beec9 feat(llama.cpp): raise default context to 128K, document RAM/SSD offload knobs
LLAMA_CTX_SIZE default 65536 -> 131072: real usage was burning through 64K
fast. ~25.6GB VRAM (17.6GB weights + ~8GB KV cache) on the 32GB R9700,
~6GB headroom — see docs/research/qwen3.8-27b-quant.md for the full table.

Also documents LLAMA_GPU_LAYERS as the RAM-offload knob for this dense
model (no separate RAM-offload flag exists in llama.cpp, and --n-cpu-moe/
--cpu-moe/--override-tensor "exps" are MoE-only, no-ops here), and that
there's no explicit SSD offload tier to enable — llama.cpp's default mmap
already falls back to disk implicitly if GPU+RAM run out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCAUsjGNSoJTtK8hyLKg5m
2026-09-03 06:35:48 +02:00
haylan 124053cf89 feat(scripts): auto-register memory-and-notes in litellm's DB for the Admin UI
The vector_store_registry block in litellm-config.yaml only seeds the
store into litellm's in-memory registry at boot — the Admin UI's Vector
Stores page (/ui/vector-stores) reads litellm's own DB
(LiteLLM_ManagedVectorStoresTable) instead, via /vector_store/list. A
config-only entry there gets silently deleted from memory the first time
anyone loads that page, since /vector_store/list treats the DB as the
source of truth and removes anything not also present there.

update.sh now registers it in the DB too, via POST /vector_store/new
(idempotent, same pattern as the existing virtual-key minting) — with the
literal resolved key, not the os.environ/... form used in
litellm-config.yaml, since the management API doesn't do config.yaml-style
substitution on request bodies.

Found in the process: /vector_store/update in this litellm version can't
touch litellm_params at all (VectorStoreUpdateRequest has no such field,
so PGVECTOR_API_KEY sent through it is silently dropped) — worth knowing
if this key ever needs rotating; documented in update.sh's comment.

Verified live: registered via the API, confirmed the row appears in
/vector_store/list (what the Admin UI page reads) with the real key, and
re-verified search still works end-to-end afterward.
2026-09-02 21:51:02 +00:00
haylan 47bdb22457 docs: record issue #24's smoke-test findings and local litellm-pgvector patches
memory-knowledgebase.md no longer says 'not yet verified' — it's been
smoke-tested end-to-end (direct search + the file_search tool on a chat
completion) and the bugs found are fixed in the preceding commits.
VENDORED.md documents the three local patches on top of the upstream
litellm-pgvector commit so a future re-vendor doesn't silently drop them.
2026-09-02 21:32:38 +00:00
haylan 7e8b5069d4 fix(scripts): update.sh never applied litellm-pgvector's db schema
litellm-pgvector's Dockerfile only runs 'prisma generate' (codegen) at
build time — nothing ever ran 'prisma db push' against pgvector-db, so the
vector_stores/embeddings tables plain didn't exist on a fresh deploy. Every
vector store call failed with 'relation "vector_stores" does not exist'.

update.sh now runs the push itself after bringing litellm-pgvector up.
2026-09-02 21:32:33 +00:00
haylan eaf11b6d4b fix(scripts): ingest-memory.sh never actually computed embeddings
Two bugs, either one fatal:
- litellm-pgvector's embeddings endpoints take a precomputed vector per
  chunk (they don't call the embedding model themselves) — the script
  posted {content, metadata} with no embedding field, guaranteed 422.
- The batch endpoint expects {"embeddings": [...]}; the script posted a
  bare JSON array as the body.

Now embeds each file's chunks via LiteLLM's /v1/embeddings first (using
LITELLM_PGVECTOR_EMBEDDING_KEY, already minted for exactly this) before
batch-inserting. Also made both source files optional — a missing file is
skipped, not a hard failure, since neither exists in this checkout yet.
2026-09-02 21:32:29 +00:00
haylan 213550e44b fix(litellm): prefix EMBEDDING__MODEL with openai/ for litellm-pgvector
litellm.aembedding can't infer a provider from a bare model name plus a
custom api_base — litellm-pgvector's embedding_service.py was hitting
'litellm.BadRequestError: LLM Provider NOT provided' on every query-time
embedding (i.e. every search). Same openai/ prefix already used for
qwen3.8-27b-local and local-embedding in litellm-config.yaml.
2026-09-02 21:32:23 +00:00
haylan 3eda4e3ec0 fix(litellm-pgvector): honor caller-supplied id on vector store create
POST /v1/vector_stores always minted a random UUID for the new store's id,
ignoring the request entirely. litellm-config.yaml's vector_store_registry
addresses this store by a fixed id (memory-and-notes), which could never
match a real row as a result — every search/write 404'd.

Added an optional id field to VectorStoreCreateRequest; create_vector_store
uses it when given, falls back to a random UUID otherwise (unchanged
behavior for callers that don't care).
2026-09-02 21:32:18 +00:00
haylan b627edb949 fix(litellm-pgvector): correct embedding vector dimension 1536 -> 768
schema.prisma hardcoded vector(1536), OpenAI ada-002's size. This stack's
actual embedding model is nomic-embed-text-v1.5, confirmed 768-dim live
against /v1/embeddings. Left as-is, the first insert after a schema push
would fail on a pgvector dimension mismatch.
2026-09-02 21:32:13 +00:00
haylan ec476c7950 fix(litellm): add missing api_key to vector_store_registry
Without it, litellm's pg_vector provider sent Authorization: Bearer None,
which round-tripped to the real api.openai.com and came back with
'Incorrect API key provided: None' — masking the actual problem and making
every vector store call fail. Points at litellm-pgvector's own
LITELLM_PGVECTOR_API_KEY (its SERVER_API_KEY), already present in .env.

Smoke-tested end-to-end against the live deploy for issue #24, alongside
the litellm-pgvector fixes in the following commits.
2026-09-02 21:32:08 +00:00
haylanandClaude-Bot e2dd106f74 feat(scripts): fold model downloads into update.sh, skip if already present
Today's embedding-server crash-loop (missing nomic-embed-text GGUF) was a
manual step nobody ran. update.sh now runs both downloader profiles
itself, every time, before bringing services up -- no separate command to
remember.

- docker-compose.yml: downloader/downloader-embedding commands gain a
  `test -f ... && skip || curl ...` guard, so re-running update.sh never
  re-downloads an existing model file.
- scripts/update.sh: runs both profiles after image pull/build, before
  service recreation.
- scripts/download-model.sh removed -- folded in, redundant standalone
  script.
- README.md / docs/memory-knowledgebase.md updated accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WHfjWrSEcGhCoeu6dQfDa
2026-09-02 23:07:53 +02:00
haylanandClaude-Bot fb7cfc9148 fix(litellm): raise qwen3.8-27b-local max_tokens floor 4096 -> 16384
Confirmed in llama-server logs: a real request hit exactly n_gen=4096
(the old floor) and returned no answer -- reasoning_content alone ate
the whole budget before any content was written, exactly the failure
mode this config's own comment predicted. 16384 is the user-chosen
middle ground between 8192 and 32768: ~10 min worst-case at ~26.7 t/s,
well under the 65536-token context window.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WHfjWrSEcGhCoeu6dQfDa
2026-09-02 22:59:53 +02:00
haylanandClaude-Bot 30a8523433 fix(lazytainer): drop network_mode: host, join ai-stack (fixes self-ID panic)
lazytainer panics on every start with 'Could not determine container ID
of lazytainer': its self-detection (vmorganp/Lazytainer,
configureFromLabels()) matches os.Hostname() against the Docker
container-ID list, but network_mode: host makes the container inherit
the host machine's hostname instead of its own container ID, so the
match never succeeds. Upstream's own example compose file doesn't use
host networking (default bridge + published ports) — nothing about
lazytainer requires it.

It also couldn't have seen the traffic it's meant to watch: llama-server
has no published host port (issue #15) and only exists on the ai-stack
bridge network, which host networking has no visibility into. Joining
ai-stack instead fixes both problems at once.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WHfjWrSEcGhCoeu6dQfDa
2026-09-02 22:55:38 +02:00
haylanandClaude-Bot c7848f356a chore(scripts): mark update.sh executable
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WHfjWrSEcGhCoeu6dQfDa
2026-09-02 22:47:53 +02:00
haylanandClaude-Bot cb9b3a9045 fix(docs): update stale MEMORY_RETRIEVAL_EMBEDDING_KEY refs after litellm-pgvector revert
README.md and docs/proxy-key-onboarding.md were edited by later commits
(b996b1f, 24d749b) that the litellm-pgvector revert didn't touch, so they
still named the now-removed MEMORY_RETRIEVAL_EMBEDDING_KEY var. Left
docs/research/langchain-pgvector-vs-litellm-pgvector.md as-is — it's a
dated record of the decision being reversed, not live config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WHfjWrSEcGhCoeu6dQfDa
2026-09-02 22:46:10 +02:00
haylanandClaude-Bot e7983f0710 Revert "feat(knowledgebase): replace litellm-pgvector connector with memory-retrieval"
This reverts commit abeadc49c8.

Restores vendor/litellm-pgvector/ and the vector_store_registry wiring
(in-band file_search tool-call support) at the user's request, after
re-confirming against docs.litellm.ai/docs/completion/knowledgebase and
litellm-pgvector's own README that pg_vector is still not an in-process
vector_store_registry backend -- it requires this same standalone
connector service either way, so there is no simpler 'native' path that
was missed. Trading back in: 793 lines of vendored code, the untested
Prisma migration, and the git-context build risk noted in VENDORED.md
(all flagged as unverified against real hardware in issue #24), in
exchange for the file_search in-band tool call memory-retrieval did not
support.

Conflicts resolved on top of later commits (Redis, update.sh key-minting
fold-in):
- .env.example / docs/memory-knowledgebase.md: kept the auto-mint-via-
  update.sh language, renamed MEMORY_RETRIEVAL_* back to
  LITELLM_PGVECTOR_*.
- scripts/generate-secrets.sh: left deleted -- its job was folded into
  update.sh in 24d749b, unrelated to this revert.
- scripts/update.sh: renamed the MEMORY_RETRIEVAL_* secret/mint calls to
  LITELLM_PGVECTOR_* to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WHfjWrSEcGhCoeu6dQfDa
2026-09-02 22:45:08 +02:00
haylanandClaude-Bot b4dc83949e feat(litellm): add Redis for router state/rate-limits/budgets/cache
New redis service (redis:7-alpine, password-protected, no persistence
volume — everything it holds is cache/coordination state). litellm gets
REDIS_HOST/REDIS_PORT/REDIS_PASSWORD, which is all LiteLLM needs to use it
for router state, rate limits/budgets, and cache invalidation — no
litellm-config.yaml block required (docs.litellm.ai/docs/proxy/caching).
REDIS_PASSWORD added to .env.example and update.sh's auto-generated
secrets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 22:28:25 +02:00