Two llama-server.exe router instances behind small Python proxies. proxy.py
listens on 0.0.0.0:8001 and reverse-proxies /embedding/* to the embed stack.
No tunnel runs on this box — the ZBOX's Caddy is the entry point and forwards
to :8001. Each proxy loads its model on first request and unloads after
10 minutes of inactivity — the router process stays up so the front-end
connection never breaks.
:8001 proxy.py → :8002 router (chat)
:8001 /embedding/* → :8003 embed_proxy.py → :8004 router (embeddings)
Bare /v1/... at the root also still hits the chat router
(backwards compat); the /chat prefix is the preferred alias.
Both stacks run side-by-side as independent processes; both models can be loaded
concurrently.
| File | Role |
|---|---|
proxy.py |
Chat proxy + router supervisor. Generates models-preset.ini at startup. Also reverse-proxies /embedding/* to the embed stack. |
embed_proxy.py |
Embedding/re-ranking proxy + router supervisor. Generates embed-preset.ini. |
watchdog.ps1 |
Restart-on-crash supervisor for proxy.py. |
watchdog-embed.ps1 |
Restart-on-crash supervisor for embed_proxy.py. |
restart-watchdog.ps1 |
Graceful midnight restart for chat watchdog (WM_CLOSE → cascade shutdown → relaunch). |
restart-watchdog-embed.ps1 |
Same for embed watchdog, staggered 1 min after chat. |
create-scheduler-tasks.ps1 |
Creates daily Task Scheduler entries (run once as Administrator). |
log_paths.py |
Shared log path resolution and formatting utilities. |
log_paths.ps1 |
PowerShell helper for inspecting log paths. |
chat_template.jinja |
Custom Jinja chat template passed to llama-server. |
models-preset.ini |
Auto-generated (don't edit). Every MODELS × CTX_CHOICES combo as its own preset. |
embed-preset.ini |
Auto-generated (don't edit). Single preset for Qwen3-Embedding-4B. |
models/ |
GGUF weights. models/_aux/ holds mmproj projectors. |
llama.cpp_latest/llama-server.exe |
The router binary. |
- Python 3.x — create and activate a virtualenv:
python -m venv .venv .venv\Scripts\activate - Install dependencies — only
aiohttpis required:pip install aiohttp
- Download
llama-server.exe— get a Windows build from llama.cpp releases (b9209+ for MTP support) and place it inllama.cpp_latest/. - Download GGUF models — place your model files in the
models/directory. - Set your API key — export the environment variable (overrides the hardcoded fallback in the proxy code):
$env:LLAMA_API_KEY = "your-secret-key"
Two terminals for the proxies (service them however you like — they're independent):
H:\llama.cpp\watchdog.ps1 # chat stack on :8001
H:\llama.cpp\watchdog-embed.ps1 # embed stack on :8003Daily restart — create Task Scheduler entries so watchdogs restart cleanly at midnight:
H:\llama.cpp\create-scheduler-tasks.ps1 # run once as AdministratorEntry point — the ZBOX (htk-ZBOX-PI336, LAN 192.168.178.43 / Tailscale
100.80.201.44) runs Caddy, which forwards to :8001 on this box. No tunnel
runs on this machine.
Headless chat variant (skip the interactive picker — sets the fallback model
when a client request omits the model field; all presets are still routable):
H:\llama.cpp\watchdog.ps1 --model "Nail-Qwen3.6-35B-A3B Q4"- Chat:
/chat/v1/chat/completions(via the ZBOX entry point) — model field selects preset (e.g.nail-35b-a3b-q4-262k). - Embeddings:
/embedding/v1/embeddings— modelqwen3-embedding-4b-8k. - Re-ranking:
/embedding/v1/rerank— same embed router. - Both require
Authorization: Bearer <key>. Set via$env:LLAMA_API_KEY(overrides the hardcoded fallback in the proxy code).
/health on either port reports proxy + router state and which model is currently loaded.
- Watchdog starts the proxy. Proxy spawns the router with
--no-models-autoloadand--models-max 1. GPU is idle, router is healthy. - First chat/embedding request arrives. Proxy POSTs
/models/loadto the router, polls/v1/modelsuntilstatus.value == "loaded", then forwards. - Idle watchdog (inside each proxy) checks every 30s. After 10 minutes of no activity it POSTs
/models/unload. VRAM frees; router stays running; front-end connection stays alive. - Next request reloads on demand.
Cold-load latency: ~15–20s for the 35B chat model on a 3090 Ti; ~2–3s for the 4B embedder.
Chat side — append a ModelChoice(...) to MODELS in proxy_config.py. Every entry is crossed with CTX_CHOICES (currently a single 262k) to produce one preset per (model × ctx) pair; a model can pin its own ctx_choices to override. No INI editing.
Each preset uses KV cache quantization (cache-type-k = q4_0, cache-type-v = q4_0), flash attention, and a custom Jinja chat template (chat_template.jinja with preserve_thinking kwarg).
MTP (speculative decoding) — set spec_mtp=True on a ModelChoice to enable built-in MTP speculative decoding. This emits spec-type=draft-mtp, spec-draft-n-max=2, spec-draft-p-min=0.0 in the preset. Requires the b9209+ router binary (PR ggml-org/llama.cpp#22673). The 27B model (qwen3.8-27b-q4-mtp) uses models/Qwen3.8-27B-UD-Q4_K_XL.gguf, which ships the MTP draft head baked in.
Embed side — embed_proxy.py is hardcoded to a single model (MODEL_FILE, MODEL_ID, CTX_SIZE constants at the top). Change those if you swap the embedder.
logs/<week>/proxy-<date>.log— chat proxy eventslogs/<week>/embed-proxy-<date>.log— embed proxy eventslogs/<week>/llama-server-<date>.log— chat router outputlogs/<week>/embed-server-<date>.log— embed router outputlogs/<week>/chat-<date>.logandchat-<date>.raw.jsonl— per-request chat traces (proxy.py only)logs/<week>/watchdog-restart-<date>.log— daily restart logs- All logs are bucketed by ISO week folder.