Views
No views yet
Status: Experimental. Weights remain available for reproducibility.
zenlm/zen-nano-0.6b backboneQwen3ForCausalLM), 0.6B dense — only the routing heads are trained here; the backbone stays frozenhanzo-router task taxonomy).<route model="..." level="fast|balanced|max"/>) for gateway-side use where
only text-in/text-out is available.training/):(prompt → task, best-model) pairs built from eval profiles and
gateway telemetry: every routed request settles realized cost, latency,
and quality back into the ledger, which becomes labels.quality − λ·cost − μ·latency, the same objective the serving-side SLO
uses.1make data # build dataset from eval JSONL + usage ledger export
2make train # stage-1 SFT
3make grpo # stage-2 reward tuning
4make eval # routing accuracy / regret vs oracle
5make quantize # GGUF Q4_K_M + MLXzen-nano-0.6b
(Qwen3, 596M params, hidden 1024, 28 layers).extract_local_sessions.py and
extract_agentic_dataset.py (shared logic in session_common.py) pair each
real user prompt with the model that actually served it. quality is a fixed
proxy = 1.0 (the counterfactual quality of models that did not run is
unobservable), cost = served tokens x the catalog's cost_per_1k,
latency_ms = 0 (not in the logs), task = a keyword heuristic ported from
hanzo-router's Heuristic::classify. build_dataset.py collapses to one
max-reward row per unique prompt. Real model ids are mapped 1:1 to catalog ids
(dated Anthropic ids normalized, e.g. claude-opus-4-5-20251101 ->
claude-opus-4-5); no collapsing into generic tiers.training/catalog.yaml is the routable universe: 28
models = 3 local zen + zen cloud tiers + Anthropic / OpenAI / Gemini / Grok /
DeepSeek / Kimi / MiniMax, priced from the gateway's live config
(hanzoai/ai/conf/models.yaml, refreshed from pricing.hanzo.ai; models not yet
in it -- e.g. claude-fable-5, gpt-5.5, gemini-3-*, grok-4 -- carry a
# verify provider-list price). The route head sizes from the catalog
(classes_from: catalog), so the runs printed route head: 24/28 classes
(up from 7 in the scaffold) with no code change -- adding a routable model is
one catalog row.~/.claude/projects + ~/.codex/sessions. 5,975 raw ->
5,455 unique-prompt rows -> random 80/20 = 4,364 train / 1,091 holdout.claude-opus-4-8 94.6%,
claude-haiku-4-5 4.9%, others <0.3%; 19 catalog models have zero rows.
Tasks: code 59.6%, general 24%, cheap_chat 8.5%, math 5.3%, reasoning 2.6%,
long_context 0.07%, vision/creative 0%.hanzoai/zen-agentic-dataset-private. That corpus is 9.9B tokens / 2.3M
samples, but is mostly synthetic identity SFT + git history; the routing
signal lives in embedded Claude Code transcripts inside assistant turns.
Streaming the 35 train chunks + valid split yielded only 4,420 extractable
routing rows (transcripts sit in chunks aa-ad + valid; the rest have no
served-model label). Combined and deduped: 9,713 unique rows, stratified
80/20 by route = 7,772 train / 1,941 holdout.claude-opus-4-8 53%, claude-opus-4-5 35%, claude-haiku-4-5 6%,
claude-sonnet-4-5 5%, then claude-opus-4-1/claude-fable-5/gpt-5.5/
claude-sonnet-4-6. 8 models carry data (vs 5 in Run A).--freeze-backbone freezes the backbone
(requires_grad=False), precomputes pooled embeddings once (cached), and fits
only the three linear heads (12 epochs, seconds; loss 3.04 -> 0.97). The
published zen-router.pt is 1.2 MB of head weights only -- the backbone
stays the public zenlm/zen-nano-0.6b, so the release cannot memorize or
reconstruct any private corpus text.| metric | Run A (full, local) | Run B (frozen, combined) | reading |
|---|---|---|---|
| route models with data | 5 | 8 | corpus adds opus-4-5, sonnet-4-5, opus-4-1 |
| route head size | 24 | 28 | auto-sized from catalog |
| task_acc | 0.824 | 0.723 | freezing the backbone costs task expressivity |
| route_acc | 0.961 | 0.791 | Run B beats its 53.3% majority baseline by +25.8 pts and emits 5 models (opus-4-8 x928, opus-4-5 x856, haiku x125, sonnet-4-5 x31, fable x1) -- genuine multi-model discrimination, unlike Run A's memorized single label |
| route_top3 | 0.999 | 0.990 | |
| artifact | 2.4 GB | 1.2 MB | heads only |
claude-opus-4-8 from claude-opus-4-5 and route cheap prompts to haiku,
scoring 26 points over the majority baseline. Run A's higher route_acc is not.benchmarks/ provides, and is the stated path forward.benchmarks/, Apple M4
Max / MPS, backbone fp16, 27.8 min wall-clock to batch-embed + route all
prompts). The checkpoint scores AIQ 0.6248 — below the interpolation hull
(0.7054), the untrained prior (0.7427), and the oracle upper bound (0.8701). It
does not beat statically picking one model here. This is expected and we report
it plainly: RouterBench's 11 models are 2023-era with zero overlap with the
2026 catalog, so scores are read through a documented family/tier bridge
(benchmarks/checkpoint_map.yaml) — this measures transferred task/tier
discrimination, not native in-catalog routing — and the checkpoint saw zero
training rows for those targets (its corpus is Claude-Opus-skewed, 8/28 models
with data). The head keeps voting frontier tier and cannot trace a cost-penalized
frontier. Real RouterBench performance requires training on RouterBench's own
per-model labels; the numbers and mapping are fully reproducible (see
benchmarks/README.md).<50 ms CPU / <10 ms Metal target is at Q4 (see
make quantize, not run here). For reference, llama.cpp prompt-processing of
zen-nano-0.6b at Q4_K_M on this box runs 9,092 tok/s (a 1k-token forward
~= 112 ms).hanzo-router + enso) decides in 1.6 us (rules) to 16 us (learned
featurize + bilinear select) -- 4-5 orders of magnitude below one 0.6B forward,
so the encoder-mode router forward dominates end-to-end cost, as designed.python training/extract_local_sessions.py --out data/evals-local.jsonlpython training/extract_agentic_dataset.py -> combine -> build_dataset.py
-> stratified split -> python training/sft.py --config training/config.scaled.yaml --freeze-backbone -> python -m eval.eval_routing --model out/zen-router --data data/routing-eval.jsonl.hanzo-router Classifier seam (task head) and
the enso Featurizer seam (feature head); enso's per-user LinUCB
consumes the features, so cold-start falls back to rules and improves
online.docs/integration.md.zen-nano-0.6b backbone runs quantized in llama.cpp embedding mode
and the three linear heads run in the caller in numpy (a 1024×28 matmul is
microseconds). No torch at serve time. Export + recipe live in export/
(export_heads.py, route_gguf.py, verify_parity.py, QUANTIZED.md).prompt ─▶ llama.cpp (zen-nano Q4_K_M, --pooling last --embd-normalize -1)
└─ raw last-token hidden state x (1024-d, NOT normalized)
└─ numpy: route/task = argmax(W·x + b); feat = W_feat·x + b_featllama.cpp --pooling last --embd-normalize -1 emits the same
raw post-final-norm hidden state HF returns as last_hidden_state (torch f16
norm 101.4 / llama 102.7, element-wise match). Normalization must be off — the
head bias is calibrated to the ≈100 norm; an L2-normalized (norm 1) vector lets
the bias swamp the signal and collapses routing to one label.llama.cpp build 9430 Metal):| quant | size | task argmax | route argmax | route top-3 | pooled cosine |
|---|---|---|---|---|---|
| Q4_K_M | 397 MB | 89% | 95% | 100% | 0.969 |
| Q8_0 | 639 MB | 95% | 97% | 100% | 0.988 |
llama-server, prompt → model id, n=200 holdout
mix; token lengths min 3 / p50 159 / mean 405 / max 1373):mean 132 ms p50 97 ms p99 351 ms min 12.5 msllama-bench pp1024 ≈ 112 ms). For
reference the fp32 torch/MPS path measured p50 57 ms on the same holdout:
for a 0.6B model, in-process MPS matmuls beat a Q4 llama.cpp HTTP
round-trip, so the quantized path's win here is footprint (397 MB vs
2.4 GB) and portability (CPU / edge / Vulkan, no torch), not raw M4-Max
latency. See export/QUANTIZED.md for the full recipe and reproduction.