STREAM_IDLE_TIMEOUT_MS was raised to 180s on 2026-09-09 to give contended
llama-server prefill room to produce a first token, but qwen-code sessions
kept hitting "Stream produced no non-ping SSE event within 95000ms" the very
next morning. Per OmniRoute's own docs, that's the wrong timer: the first
non-ping SSE event's deadline inherits REQUEST_TIMEOUT_MS (default 10 min,
computed as remaining budget after retries/cooldowns), not
STREAM_IDLE_TIMEOUT_MS (which only bounds gaps between chunks once streaming
has already started).
Two changes:
- Add REQUEST_TIMEOUT_MS=1800000 (30 min) on the omniroute service, exposed
as OMNIROUTE_REQUEST_TIMEOUT_MS like the existing stream-idle var. Safety
margin, not the root-cause fix.
- Add --cache-reuse 256 to llama-server: it had no KV-cache reuse configured,
so every request reprefilled its full prompt from scratch even when most
of a conversation's prefix was unchanged. This is the actual fix for why
compact-prompt prefill was slow enough to hit the timeout in the first
place.
Documents the distinction and root cause in docs/research/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqWBahogLCkrXNfzCSvcVc
- docker-compose.yml: add --reasoning off to qwen-classifier per
fast-model-choice.md's own recommendation (ggml-org/llama.cpp#20809
safety net) — missed in the original rollout, caught while writing
this up.
- docs/coding-cli-setup/qwen-code.md: rewritten to match what's actually
deployed (qwen-classifier, partial GPU offload, 65536 ctx, Q4_K_XL) —
previously described an unimplemented llama-server-fast/8192-ctx plan.
Documents the non-interactive MCP tool allow-list gap found live-testing.
- docs/coding-cli-setup/opencode.md: fix stale 65536 example that didn't
match its own documented LLAMA_CTX_SIZE/LLAMA_PARALLEL formula (131072).
- docs/research/fast-model-choice.md: implementation note recording where
the actual rollout diverged from this doc's original recommendations
(service name, quant, context size, CPU-first-then-GPU path).
- docs/research/omniroute-account-semaphore-timeout.md: new — the
hardcoded 30s per-connection semaphore timeout found during the
pr-agent investigation, root-caused against OmniRoute's own source,
and the maxConcurrent:null + providerSpecificData.timeoutMs fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full offload (999 layers) left only ~768MB free VRAM regardless of
batch-size/flash-attn tuning — that gap tracks roughly fixed regardless
of those knobs, most likely ROCm's own per-process HIP context overhead
(same ROCm#5706 quirk already noted for two HIP contexts sharing this
card, see llama-server's GPU_MAX_HW_QUEUES comment above). Dropping to
28/36 layers on GPU (6 layers + their KV on CPU) trades a slice of
speed for real freed VRAM — still >80% of layers on GPU, nowhere near
CPU-only's unusable latency. Verifying live before locking this number in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Batch/ubatch reduction barely moved measured VRAM (~768MB free, same as
before) — wrong lever. llama-server runs with --flash-attn on; this
service didn't. Without it, the unfused attention compute buffer at
65536 ctx is far larger than flash-attn's fused workspace, which is
what the naive weights+KV estimate missed. Matches llama-server's flag.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Measured live: full-offload weights+KV (~4.9GiB estimate) actually used
~5.85GiB, leaving only ~700MB free on the R9700 — too tight, real OOM
risk for either GPU process. The gap was compute-buffer/graph overhead
the naive estimate didn't account for. Drop --batch-size/--ubatch-size
well below llama-server's defaults (2048/512) to shrink it — a
single-request classifier has no batching throughput to lose.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CPU-only was too slow in practice: real classification calls blew past
OmniRoute's 60s timeout and retry-looped (504/499). Moved to GPU.
Also traced qwen-code's actual classifier transcript cap in its source
(MAX_TRANSCRIPT_MESSAGES=40, MAX_HISTORICAL_ACTION_CHARS=4000/message) —
worst case is ~40-50K tokens, not the 131072 originally set in
settings.json (copied from the main model's entry, not a real qwen-code
requirement). Dropped ctx-size to 65536 (~1.5x margin) so Q4_K_XL
weights + q4_0/q4_0 KV fit fully on GPU (~4.9GiB) inside the ~6.1GiB
free on the R9700, instead of needing partial CPU/GPU offload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Q4_K_M-class file this originally specced didn't exist yet on
gameserver (classifier crash-looped: "No such file or directory").
A Q8_K_XL GGUF for the same model was already sitting in the models
volume from something earlier — point at that instead of downloading a
new file, and drop the KV cache quant to q4_0/q4_0 to keep total RAM
comfortable now that the weights are the larger Q8 variant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fastModel in ~/.qwen/settings.json (permissions.autoMode.classifier) was
aliased onto llama-server's own 27B connection, so every tool-call safety
check queued behind whatever heavy generation was already running on that
model's 2 GPU slots.
Add qwen-classifier: a separate llama.cpp instance, CPU-only, running
Qwen3-4B-Instruct-2507 (the smallest Qwen3 with native >=131072 context,
qwen-code's requirement, without lossy RoPE scaling). Structurally isolated
from llama-server's queue instead of sharing it. Sized for gameserver's
~17GiB free system RAM: q8_0/q8_0 KV at full 131072 ctx (~9.8GiB) + Q4_K_M-
class weights (~2.3GiB) fits comfortably, with better KV quality than the
q4_0 that would've been needed to fit this on the GPU's ~6GiB free VRAM.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Assess whether the Colibrì disk-streaming MoE inference engine fits the AMD ROCm single-GPU setup or fills a gap beyond the existing llama.cpp/OmniRoute/Qdrant/Neo4j/ComfyUI stack. Conclusion: worth a passing watch, not worth integrating today.
Move docs/coding-cli-setup.md to docs/coding-cli-setup/ with one file
per CLI (claude-code, kimi-cli, opencode, qwen-code) plus a shared
index.md for the gateway intro, tool-calling risk note, and summary
table.
Also fixes the qwen-code doc: context sizes are per-slot
(LLAMA_CTX_SIZE / LLAMA_PARALLEL), not raw LLAMA_CTX_SIZE (same fix
applied to OpenCode's limit.context); documents the fastModel
classifier provider and its own context math; adds the omniroute-search
MCP server (SearXNG web search) and Auto Mode permissions tuning that
were missing from the original qwen-code section.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
current=$(grep ...) ran before checking whether the key existed in .env
at all. Under set -euo pipefail, a key missing from .env (e.g. a new
tunable this branch just added to .env.example) made that grep exit 1,
pipefail propagated it, and set -e killed the script instantly with no
output - looked exactly like a gum hang but never reached ensure_gum.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bs8xf8Dt8X6tRBvBiLCXYc
The server has no outbound internet access, so the curl download in
ensure_gum always failed silently and fell back to plain prompts.
Vendor the x86_64 release tarball under scripts/vendor/ and check it
before attempting a download - download stays as a fallback for other
archs or a version bump without a re-vendor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bs8xf8Dt8X6tRBvBiLCXYc
curl -fsSL | tar swallowed curl errors entirely, so a network failure
fetching gum looked identical to a successful skip - just silently
dropped into the plain-prompt fallback with no explanation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bs8xf8Dt8X6tRBvBiLCXYc
Documents the NPM route for the RAG knowledge graph alongside the
existing proxy-ai.home/search.home entries. Neo4j (not Qdrant) gets
the hostname — it's the human-facing graph browser; Qdrant's
dashboard stays on its raw port for now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjhxWirQepKFEQj1huXNJR
Drops the second always-resident llama.cpp instance, its downloader,
and the omniroute depends_on entry. Also strips the now-dead
LLAMA_FAST_* block from .env.example and the stale VRAM-budget comment
in scripts/switch-model.sh that assumed this service was always up.
Note: this was qwen-code's Auto Mode Stage 1 classifier (fastModel) —
see docs/research/fast-model-choice.md and issue #44. Auto Mode will
lose that classifier until/unless it's reconfigured to route
elsewhere or fall back to prompt-only classification.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjhxWirQepKFEQj1huXNJR
Adds the two databases as infra services (own volume, ai-stack
network, host-published UI ports like comfyui) for an upcoming RAG
pipeline. Extraction/chunking/orchestration code is out of scope for
this repo — it's app logic that calls into these DBs and llama-server,
not compose infra.
Neo4j password follows the omniroute secret pattern: blank in
.env.example, auto-generated by scripts/update.sh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjhxWirQepKFEQj1huXNJR
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
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
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
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.
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
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
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
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
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
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>
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>