A compact, fast, local-first multi-head classifier for coding-agent task
routing. Given a task prompt at kickoff, it predicts stable task properties; a
downstream policy/config then maps those properties to a model, provider, and
execution behavior. The classifier predicts the ontology; your config owns the
orchestration.
This is the model component of spawn — see
Afterbuild/spawn-router for the
training code and spawn-gateway for the local
gateway that wraps Claude Code / Codex and routes with these weights.
Routing is kickoff-only: classify once at task start and lock the model for
the whole task cycle (no per-turn re-routing → no context thrash).
Files
File
What
spawn_router.int8.onnx
int8-quantized graph — recommended for serving (~164 MB)
spawn_router.onnx
fp32 graph (~540 MB)
model.pt
PyTorch state dict — for fine-tuning / sub-dim outputs (~565 MB)
spm.model + *tokenizer*.json
SentencePiece (DeBERTa-v2/spm) tokenizer
model_config.json
architecture + label maps
temperature_scaling.json
per-head calibration temperatures
*_metrics.json, battery_results.json
evaluation results
Usage (ONNX, torch-free)
Needs only onnxruntime, numpy, and sentencepiece — no torch, no transformers.
python
1import numpy as np
2import onnxruntime as ort
3import sentencepiece as spm
45MODEL_DIR ="."# dir containing spawn_router.int8.onnx, spm.model, *.json6MAX_LEN =2567LABELS ={8"complexity_logits":["easy","medium","hard"],9"task_type_logits":["bugfix","feature","refactor","test",10"design","docs","migration","exploration"],11"risk_logits":["low","medium","high"],12}13TEMPS ={# from temperature_scaling.json; output name -> head temperature14"complexity_logits":0.891251,15"task_type_logits":0.707946,16"risk_logits":1.059254,17}1819sp = spm.SentencePieceProcessor(model_file=f"{MODEL_DIR}/spm.model")20sess = ort.InferenceSession(f"{MODEL_DIR}/spawn_router.int8.onnx",21 providers=["CPUExecutionProvider"])2223defclassify(text:str)->dict:24# DeBERTa-v3 spm tokenizer: [CLS]=1 + pieces (truncated) + [SEP]=225 pieces = sp.encode(f"Current: {text}", out_type=int)[: MAX_LEN -2]26 ids =[1,*pieces,2]27 feeds ={# structured inputs are text-only sentinels (no interaction context)28"input_ids": np.array([ids], dtype=np.int64),29"attention_mask": np.ones((1,len(ids)), dtype=np.int64),30"previous_action_id": np.array([0], dtype=np.int64),# "none"31"previous_outcome_id": np.array([4], dtype=np.int64),# "unknown"32"log_recency_seconds": np.array([0.0], dtype=np.float32),33"has_interaction": np.array([0], dtype=np.int64),34"has_recency": np.array([0], dtype=np.int64),35}36 out ={o.name: v for o, v inzip(sess.get_outputs(), sess.run(None, feeds))}37 result ={}38for name, labels in LABELS.items():39 logits = out[name][0]/ TEMPS[name]40 p = np.exp(logits - logits.max()); p /= p.sum()41 i =int(p.argmax())42 result[name.replace("_logits","")]={43"label": labels[i],"confidence":round(float(p[i]),4),44}45 result["complexity_sub"]={46 name.replace("complexity_sub_",""):round(float(out[name][0]),4)47for name in out if name.startswith("complexity_sub_")48}49 result["risk_sub"]={50 name.replace("risk_sub_",""):round(float(out[name][0]),4)51for name in out if name.startswith("risk_sub_")52}53return result
5455print(classify("refactor JWT key rotation in prod"))56# {'complexity': {'label': ...}, 'task_type': {'label': ...}, 'risk': {'label': ...},57# 'complexity_sub': {'reasoning_depth': ...}, 'risk_sub': {'security_surface': ...}}
Evaluation
Two complementary measures (eval scripts in
Afterbuild/spawn-router:
scripts/eval_battery.py, eval.py):
Locked kickoff battery (83 hand-labeled probes, never in training — the
canonical cross-version benchmark):
Metric
v6
Unified kickoff score
69.5%
Exact match (all 3 heads)
37.4%
complexity
65.1%
task_type
78.3%
risk
65.1%
Held-out test split (n=174, mirrors the training distribution):
Calibration: per-head temperature scaling fit on validation. ECE on the held-out
split is ~0.37 at the 0.8 automation threshold — confidence is not yet
well-calibrated for aggressive automation; gate on it conservatively.
Intended use
Pick a capability tier / provider for a coding task at kickoff, before the
first expensive agent call.
Drive a confirmation gate for high-blast-radius work (risk/security/prod).
Spread work across tiers to reduce rate-limit pressure.
Out of scope: per-turn routing; non-coding prompts; high-stakes autonomous
action without a human gate; languages other than English (trained on English).
Limitations
Cold-start ceiling. Effort/blast-radius isn't fully derivable from prompt
text — complexity=medium and risk=high are the weakest bands, especially on
short imperatives. Production signals (overrides, retries, session duration) are
the intended path past this; this checkpoint predates that loop.
Synthetic-label ceiling. Much training data is LLM-labeled; expect a
~75–80% ceiling per head until real disagreement signals are mixed in.
Quantized serving tradeoff. The fp32 ONNX graph matches the PyTorch model
on the locked battery, including sub-dimension scores. The int8 graph is the
recommended low-dependency serving artifact and preserves the established v6
serving behavior, but dynamic quantization can move borderline labels and
regression values.
5 epochs, batch 16, encoder LR 2e-5, head LR 1e-4, weight decay 0.01, warmup
0.1, seed 13; post-hoc per-head temperature scaling on validation.
Data: v6 mixed set (train 1141 / val 174 / test 174) — a mix of synthetic
coding-task prompts and real coding-agent kickoff prompts. The merged
training set is not distributed (it embeds third-party trace text and
personal usage traces); the synthetic seed data and the full data pipeline
are in the code repo. See "Training data provenance" below.
Training data provenance
Disclosed in full so downstream users can do their own diligence:
Synthetic coding-task prompts (majority of the mix) — written by Claude
sub-agents and hand-labeled; included in the code repo.
SWE-bench problem statements — used only as Claude-paraphrased
short-imperative prompts (no code, patches, or full issue text). The
SWE-bench benchmark code is MIT; the aggregated issue text is owned by its
authors and the HF dataset card carries no license tag.
Public coding-agent trace datasets (badlogicgames/pi-mono,
armand0e/gpt-5.5-agent, lewtun/ml-intern-sessions) — kickoff prompts
extracted and labeled. These carry license: other or no license; their raw
text is not redistributed here.
The author's own local agent traces — first-task prompts only; not
redistributed.
Labels and paraphrases were produced with Anthropic Claude; per
Anthropic's Commercial Terms, outputs are customer-owned. No other
provider's models were used for generation or labeling.
The model is a non-generative classifier (three softmax heads over 256-token
inputs); it emits logits, not text, and cannot reproduce training data.
Credits & provenance
Scaffolding began as a fork of tiny-router
by Udara Jay (MIT); the ontology, data, heads, and serving path were rebuilt
for coding-agent routing.
Backbone: DeBERTa-v3 (He et al.; microsoft/deberta-v3-small, MIT).
Related prior art: Vercel v0 Auto and NVIDIA's
prompt-task-and-complexity-classifier.
License: Apache-2.0 (weights), with the training-data provenance
disclosed above; the training/serving code repo is MIT.