Two llama-server.exe router instances behind small Python proxies, exposed over a
cloudflared tunnel under a single hostname. proxy.py reverse-proxies /embedding/*
to the embed stack. Each proxy loads its model on first request and unloads after
10 minutes of inactivity — the router process stays up so the tunnel never breaks.
ai.example.com/chat/* → :8001 proxy.py → :8002 router (chat)
ai.example.com/embedding/* → :8001 proxy.py → :8003 embed_proxy.py → :8004 router (embeddings)
Bare ai.example.com/v1/... at the root also still hits the chat router
(backwards compat); the /chat prefix is the preferred public 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 AdministratorPublic tunnel — cloudflared is set up separately (outside this repo). Install cloudflared.exe, configure your tunnel in ~/.cloudflared/config.yml, then run it however you prefer.
Headless chat variant (skip the interactive picker — sets the fallback model
when a client request omits the model field; all combos are still routable):
H:\llama.cpp\watchdog.ps1 --model "Qwen3.6-35B-A3B Q3" --ctx-size 32768- Chat:
https://ai.example.com/chat/v1/chat/completions— model field selects preset (e.g.qwen3.6-35b-q3-32k). - Embeddings:
https://ai.example.com/embedding/v1/embeddings— modelqwen3-embedding-4b-8k. - Re-ranking:
https://ai.example.com/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; tunnel socket 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.py. Every entry is automatically crossed with CTX_CHOICES (32k, 64k, 96k, 128k) to produce one preset per (model × ctx) pair. 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 has an MTP variant (qwen3.6-27b-q4-mtp) that uses models/Qwen3.6-27B-UD-Q4_K_XL.mtp.gguf.
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.