Router principle
Deterministic, not LLM-based
Routing decisions are made by a keyword classifier scoring the prompt against a YAML policy file, plus three override mechanisms — request header, pinned model name, metadata hint.
Lab · HomeLab
A four-machine private AI cluster, one OpenAI-compatible gateway, deterministic routing, observable failure modes, and an agent that operates the lab without taking routing authority away from policy.
Four machines, one policy-controlled gateway, deterministic routing, and observable failure behavior.
Every client speaks to the gateway. No client knows which GPU is healthy, busy, or selected.
Router principle
Routing decisions are made by a keyword classifier scoring the prompt against a YAML policy file, plus three override mechanisms — request header, pinned model name, metadata hint.
Router principle
The router never generates text itself. Every decision is auditable and reproducible.
Router principle
When the classifier scores multiple buckets: vision > coding > reasoning > chat. 'Review the security of this code' lands on Spark, not on the chat model.
Router principle
Any request carrying an image_url part goes to ubuntu-4090 regardless of keywords — nothing else in the rig can see.
Router principle
Schema-validation failure on a camera event produces a synthetic escalate_to_spark envelope, never a suppression. The router fails closed.
Router principle
Routing rules live in routing_policy.yaml and can be reloaded over an admin endpoint without restarting the gateway.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/chat/completions | OpenAI Chat Completions shim — every client speaks here |
| GET | /v1/models | Friendly model catalog: homelab/auto, homelab/code, homelab/vision, homelab/agent, … |
| POST | /v1/embeddings | Embeddings shim for downstream RAG and vector stores |
| GET | /metrics | Prometheus exposition — counters, histograms, gauges |
| GET | /stats | JSON snapshot of the same counters for humans |
| GET | /admin/upstreams | Per-route health, load, saturation, drained flag |
| POST | /admin/drain/<route> | Drain a route without restarting — explicit pins still pass through |
| POST | /admin/reload-config | Hot-reload routing_policy.yaml |
| POST | /admin/recover-cuda | Authenticated one-button CUDA recovery |
Bucket selection is deterministic policy, then size promotion decides whether cheap work should move to a heavier model.
| Bucket | Default target | Example prompts |
|---|---|---|
| vision | ubuntu-4090 | image, screenshot, ring, frigate, motion |
| notification | win-5090, or ubuntu-4090 when image-bearing | sms, alert, push notification |
| fast | ubuntu-4090 | classify, json, tags, label, summarize briefly |
| general_chat | win-5090 | tell me about, ask, what is, how do I, opinion |
| utility | win-5090 | rewrite, clean up, format, paraphrase, polish, translate |
| medium_summary | win-5090 | summarize, tl;dr, condense, recap, executive summary |
| coding | ubuntu-6000 | refactor, implement, code, FastAPI, pytest |
| agentic | ubuntu-6000 | agent, multi-step, tool use, OpenCode, Hermes |
| reasoning | Spark | architecture, trade-off, review, risk, plan |
| unknown | Spark | nothing matched — route to senior model for safety |
Large prompts promote after bucket selection. Vision and coding stay pinned; general chat promotes to Spark when context gets large.
| Bucket | <= 8K tokens | 8K-24K tokens | > 24K tokens |
|---|---|---|---|
| vision | ubuntu-4090 | ubuntu-4090 | ubuntu-4090 |
| coding / agentic | ubuntu-6000 | ubuntu-6000 | ubuntu-6000 |
| reasoning / review | Spark | Spark | Spark |
| fast | ubuntu-4090 | ubuntu-6000 | Spark |
| chat / utility / summary | win-5090 | win-5090 | Spark |
The fallback policy is intentionally asymmetric: text can spill to a smaller local model, image work cannot.
| Selected | Busy / saturated | 5xx / outage | Everything down |
|---|---|---|---|
| ubuntu-4090 | Text → win-5090 • Image → Spark | Spark | 503; camera events get a synthetic escalate envelope |
| ubuntu-6000 | win-5090 | Spark | 503 |
| win-5090 | Spark | Spark | 503 |
| Spark | none | none | 503 |
Asymmetric vision fallback
When ubuntu-4090 is saturated, text-only requests can spill to win-5090. Image-bearing requests go directly to Spark because win-5090 cannot see.
Spark is terminal
The senior model never falls back. If Spark is down, the router returns 503 instead of silently downgrading a high-risk decision.
Same client contract, two operational outcomes: direct route when the coding machine is healthy, bounded spillover when it is saturated.
Cursor posts to /v1/chat/completions with model homelab/auto.
homelab-router parses metadata and carries has_image through the full route.
Keyword policy maps refactor, FastAPI, and pytest to the coding bucket.
ubuntu-6000 handles the normal path with GLM-4.5-Air-AWQ.
If ubuntu-6000 is busy, the request spills to win-5090 instead of queueing forever.
The router logs bucket, route, reason, latency, and fallback path.
Ring and Frigate events get a fast local classifier first. Ambiguous or positive events escalate; confident negatives drop locally.
Reliability
Malformed cloud payloads no longer kill the listener thread.
Reliability
A second push path keeps camera notifications independent from the cloud-side listener.
Reliability
Real-time push, parallel push, and polling converge without creating duplicate alerts.
Reliability
Camera-side REST calls bypass intermittent local DNS issues without widening network exposure.
Netdata and Home Assistant make the lab observable without presenting missing private screenshots as evidence.
Hermes is both a normal inference client and an authenticated operator. It can drain, reload, recover, and alert, but deterministic router policy still owns routing.
scheduler
Cron polls /admin/upstreams and watches GPU temps. no_agent mode runs pure script-output watchdogs without spending tokens.
"every 5m" → poll /admin/upstreamsskills
drain-route, recover-cuda, reload-policy become first-class skills the agent can invoke and improve over time.
skill: recover-cudaconductor
delegate_task spawns isolated children with their own context and toolset. Parallel log analysis and batch evals stay out of the main loop.
delegate_task → 4× parallelmemory
Every decision and outcome lives in editable markdown under ~/.hermes/. Honcho builds an evolving model of the operator over time.
~/.hermes/memory/alerts
Telegram first; Home Assistant fallback when Hermes itself is down. The lab is never silent on a real failure.
improve
Skills get better with use. Runbooks evolve as the lab evolves. The agent and the lab compound on each other.
| Schedule | Task | Mode |
|---|---|---|
| every 5m | GET /admin/upstreams → flag any not-healthy | no_agent=True |
| */15 * * * * | scrape /metrics, alert if router p95 > 4s | agent-assisted |
| 0 9 * * 1 | weekly: summarize last week's routing-decisions.jsonl | agent-assisted |
| every 2h | verify camera pipeline end-to-end on a known sample | agent-assisted |
| 30m | one-shot: re-check after an incident-response drain | agent-assisted |
These links resolve to actual deep-dive anchors below.
The Eyes
Fast vision, JSON utility, low-latency routing pre-filter.
Hardware
Production model
What it does
Latency
Optimizations
Hard rules
Recovery story
When the eGPU on the sibling machine has a PCIe link-down/up cycle, the entire NVIDIA driver enters a degraded state where nvidia-smi still works but every fresh CUDA init fails. The router detects this, fails traffic over to Spark, and a recover-cuda.sh script reloads the NVIDIA UVM module and brings everything back up automatically. The whole sequence is documented step-by-step in the runbook.
vLLM launch (excerpt)
# qwen2.5-vl-7b-router-awq on ubuntu-4090
vllm serve Qwen/Qwen2.5-VL-7B-Instruct-AWQ \
--served-model-name qwen2.5-vl-7b-router-awq \
--quantization awq_marlin \
--max-model-len 32768 \
--gpu-memory-utilization 0.78 \
--enable-prefix-caching \
--enforce-eager false \
--host 127.0.0.1The Hands
Heavy code generation, refactoring, agentic / multi-file work.
Hardware
Production model
What it does
Optimizations
Hard rules
Reliability story — why TurboQuant beat FP8
The earlier FP8 KV configuration with TRITON_ATTN was faster per token on paper, but consistently crashed the AWQ Marlin kernel mid-load with `CUDA error: unspecified launch failure`. TurboQuant k8v4 was slower in raw decode but completed every load on this Blackwell + driver combination. Reliability won. The configuration choice is documented as “the only one that survived the sustained bench.” MTP (multi-token speculative decoding) is intentionally off: the current AWQ checkpoint drops the speculative head’s weights, so draft acceptance is 0%. It will be re-enabled when the checkpoint is fixed upstream.
vLLM launch (excerpt)
# glm-4.5-air-awq on ubuntu-6000 (RTX PRO 6000 Blackwell)
vllm serve cpatonn/GLM-4.5-Air-AWQ \
--served-model-name glm-4.5-air-awq \
--quantization compressed-tensors \
--kv-cache-dtype turboquant_k8v4 \
--max-model-len 131072 \
--gpu-memory-utilization 0.92 \
--enable-prefix-caching \
--speculative-config '{"method":"none"}' \
--host 127.0.0.1The Brain
Senior reasoning, architecture, final review, safety fallback.
Hardware
Production model
What it does
What it does not do
Optimizations
Hard rules
vLLM launch (excerpt)
# qwen3.6-35b-a3b-fp8 on Spark (DGX, GB10)
vllm serve Qwen/Qwen3.6-35B-A3B-FP8 \
--served-model-name qwen3.6-35b-a3b-fp8 \
--dtype auto \
--kv-cache-dtype fp8 \
--max-model-len 131072 \
--enable-chunked-prefill \
--enable-prefix-caching \
--rope-scaling '{"type":"yarn","factor":4.0}' \
--host 127.0.0.1The Mouth
General chat, rewrites, medium-context summarization, spillover.
Hardware
Production model
What it does
What it does not do
Optimizations
Hard rules
LM Studio config (excerpt)
# gemma-4-31b on win-5090 (LM Studio, OpenAI-compatible)
{
"served_model_name": "gemma-4-31b",
"context_length": 32768,
"gpu_memory_utilization": 0.86,
"max_concurrent_slots": 3,
"bind": "private-overlay-only",
"firewall": "block all non-overlay inbound"
}Principle
ubuntu-4090 sees. ubuntu-6000 codes. Spark thinks. win-5090 chats. No co-hosting. No swapping.
Principle
Every client speaks to the gateway. The gateway hides which machine is busy, down, or saturated.
Principle
Every routing decision is made by a keyword classifier scoring against a YAML policy. No model gets to decide where requests go.
Principle
Camera events never silently produce 'ignore'. Reasoning ambiguity always lands on Spark.
Principle
Spark is the bottom of the stack. If it is down, the answer is 503 — not 'we sent it to a smaller model and hoped.'
Principle
FP8 KV on ubuntu-6000, MTP on GLM, and Gemma on ubuntu-4090 were faster on paper. All three were rejected because they were slower under real failure conditions.
Principle
Driver poisoning, eGPU PCIe link-down, and container hangs all have documented one-line fixes. Most are exposed as authenticated admin API calls.
Principle
Every model runs locally. The only external dependencies are model weights at install time and the private overlay network. Ring cloud is an input, never a destination.
Every client hash link lands on a real card; none point to missing detail sections.
Client
AI-native editor → coding bucket → ubuntu-6000
Client
Multi-file refactors → agentic bucket → ubuntu-6000
Client
Personal agent — manages the lab, also a chat client
Client
Camera + automation orchestrator
Client
NVR motion events → vision pipeline
Client
Doorbell snapshots → vision pipeline