Small local models, measured on office tasks
Eight agents (seven in the hosted comparison), 226 committed cases, deterministic grading with no LLM judge, every candidate sat the same held-out cases at temperature 0. How it works in four steps, then three findings, one chart, one command.
Scores are held-out cases passed, never percentages. Dollars are per 10,000 tasks on the tokens of that run at the OpenRouter rate of 2026-10-08; metered means a run of that model, modelled means arithmetic on stated inputs. The private repo holds the build ledger; everything on this page has a probe folder in the public one.
How it works, in four steps
- Name the tasks you already send to a model. Triage, extraction, drafting, summaries. The one requirement: a person can say, case by case, whether the answer was right. A task nobody can check is not scored here, and the page says so. The eight tasks below are the demo set; a client's exam is never published.
- Write down what a correct answer looks like. Twenty to forty cases per task with their checks: the label, the fields, the facts that must appear, the tools that must be called. A third is held out and never tuned on. No model grades another model's answer, anywhere. Every local case runs three times at temperature 0 and counts only if all three agree.
- The cheapest model that passes takes the task. Every candidate sits the exam. The bar is the best score any model reached, or one case from it; models under it drop out; the cheapest left wins and its score is committed as the snapshot. One task drawn out, then the pick for all seven:
task bar cheapest at the bar held-out $ / 10k resists injection GPT-5.6-Sol, same task Email triage 9 of 11 gemma4:e2b-it-qat · local machine 10/11 ≈0 0/2 9/11 · 5.72 (ledger) Expense fields 8 of 9 gemma4:e2b-it-qat · local machine 8/9 ≈0 — 8/9 · 5.49 (ledger) Reply draft 9 of 10 gemma4:e2b-it-qat · local machine 10/10 ≈0 1/2 10/10 · 16 (ledger) Digest of many items 7 of 10 gemma4:e4b-it-qat · local machine 7/10 ≈0 — 8/10 · 44 (ledger) CRM brief over tool calls 15 of 18 gemma4-e2b-ctx16k · local machine 15/18 ≈0 3/3 14/18 · 127 (ledger) CRM fields from a call 16 of 19 kimi-k3 · own VM, GPU node 17/19 92 (ledger) — 15/19 · 65 (ledger) Call summary + action items 13 of 19 gpt-5.6-sol · vendor only 13/19 78 (ledger) — 13/19 · 78 (ledger) Hosted rows are one pass; a one-case gap is noise, so the bar is one case wide. "Resists injection" counts the planted-instruction cases (two per task on email triage, reply draft and the CRM brief) the row did not obey; "—" means that model has not sat them yet. Dollars: tokens of that run at the 2026-10-08 ledger rate. The vendor flagship column is there for the reader's own subtraction; this page states no saving in advance.
- A fine-tune ships only if it passes the same exam. When a small model almost passes, it is trained on the task's own training rows, on the machine it runs on, in minutes, and then sits the exam. If any held-out case that used to pass now fails, the version is refused, even when the total went up. Six attempts so far:
candidate held-out what moved verdict cause named CRM follow-up, LoRA v1 · 11 training rows 12 → 12 of 18 2 up / 3 down · 3 held-out cases down, one pass refused nine of the eleven rows made one tool call; the adapter stops after one call CRM follow-up, LoRA v2 · 16 rows 12 → 12 of 18 2 up / 2 down · 2 stable held-out cases down, three loads refused flat total hiding two up, two down; too few rows with two calls in one turn CRM follow-up, LoRA v3 · 30 rows 12 → 11 of 18 4 up / 5 down · 3 stable held-out cases down, three loads refused no prompt mask, so the loss was 95% tool payload; it failed two of its own training rows CRM follow-up, LoRA v4 · 96 per-step examples, prompt masked 12 → 9 of 18 1 up / 7 down · 4 stable held-out cases down, three loads refused learned the decisions, and a “call again” reflex with them Request intake, thinking switched off by the chat template (no training) 19 → 17 of 20 0 up / 2 down · same speed as the adapter refused the switch loses the hand-over rules the adapter kept Request intake, LoRA · 22 rows, 9 minutes of training 19 → 18 of 20 0 up / 1 down · −87% output tokens, 4.2× faster; one pass, not snapshotted held within one case; not promoted removes the thinking channel; the one lost case is at N=20 An average would have shipped v2 (flat total). The gate refused it. Every row is a probe folder under
docs/probes/i0-*,i0b-*,i2*; the numbers here are parsed from those files.
Then the loop: every model release, runtime bump or price change re-sits the exam per case and the report lists what moved in both directions. A challenger replaces the incumbent only if it passes every stable case the incumbent passes. On 2 October a runtime upgrade moved 1 of 220 cases, 0 held-out.
Where the model runs
| tier | models that sat the exams |
|---|---|
| your laptop a 16 GB machine; nothing leaves it; ≈$0 in tokens | gemma4-e2b-ctx16k, gemma4-e4b-ctx16k, gemma4:e2b-it-qat, gemma4:e4b-it-qat |
| your own cloud open weights on a GPU you rent or own; data stays in your account | deepseek-v4-pro-0813 (GPU node), gemma-4-26b-a4b-it (one 24 GB card), gemma-4-31b-it (one 48 GB card), glm-5.3 (GPU node), gpt-oss-20b (one 24 GB card), kimi-k3 (GPU node), mistral-small-3.2-24b-instruct (one 24–48 GB card), qwen3-14b (one 24 GB card) |
| vendor API closed weights, pay per call | gpt-5.4-mini, gpt-5.6-sol, grok-4.6 |
A score was taken on the runtime the row names. A build on your own card re-sits the exam before its row is claimed for it.
1. A 2B–4B model on a laptop holds every short decision task. It loses only long summaries.
One panel per task; no aggregate across tasks, because a sum hides the row a buyer cares about. Local marks sit in the ≈0 lane. The hosted rows are one pass each (2026-10-08 ledger); the local rows are three fresh loads that agreed on every case.
The honest limit. On call summaries with numbers the best model of any tier passes 14 of 19; the local one 10. That band is closed until real transcripts exist. Running the open weights on your own card is a residency choice, not a saving: at one request at a time an L4 costs more per task than the hosted per-token price on every row (call-fields on gemma-4-26b-a4b: hosted $2.14 per 10k (ledger) vs $5.47 on a RunPod L4 vs €8.82 on a Scaleway L4). modelled
The seven tables behind the chart
Email triage · 11 held-out cases · 8 candidates, 8 within one case of the best
| model | can run on | held-out | pass^k | resists injection | $ / 10k | run |
|---|---|---|---|---|---|---|
| gemma4:e2b-it-qat | local machine | 10/11 | pass^3 10/11 | 0/2 | ≈0 | snapshot |
| mistralai/mistral-small-3.2-24b-instruct | own VM, one 24–48 GB card | 10/11 | 1 run | — | 0.22 (ledger) | unpinned |
| google/gemma-4-26b-a4b-it | own VM, one 24 GB card | 10/11 | 1 run | — | 0.23 (ledger) | unpinned |
| google/gemma-4-31b-it | own VM, one 48 GB card | 10/11 | 1 run | — | 0.24 (ledger) | unpinned |
| openai/gpt-5.4-mini | vendor only | 10/11 | 1 run | — | 1.98 (ledger) | pinned |
| openai/gpt-oss-20b | own VM, one 24 GB card | 9/11 | 1 run | — | 0.11 (ledger) | unpinned |
| qwen/qwen3-14b | own VM, one 24 GB card | 9/11 | 1 run | — | 1.02 (ledger) | unpinned |
| openai/gpt-5.6-sol | vendor only | 9/11 | 1 run | — | 5.72 (ledger) | pinned |
Expense fields · 9 held-out cases · 8 candidates, 8 within one case of the best
| model | can run on | held-out | pass^k | resists injection | $ / 10k | run |
|---|---|---|---|---|---|---|
| mistralai/mistral-small-3.2-24b-instruct | own VM, one 24–48 GB card | 9/9 | 1 run | — | 0.22 (ledger) | unpinned |
| google/gemma-4-26b-a4b-it | own VM, one 24 GB card | 9/9 | 1 run | — | 0.23 (ledger) | unpinned |
| google/gemma-4-31b-it | own VM, one 48 GB card | 9/9 | 1 run | — | 0.23 (ledger) | unpinned |
| gemma4:e2b-it-qat | local machine | 8/9 | pass^3 8/9 | — | ≈0 | snapshot |
| openai/gpt-oss-20b | own VM, one 24 GB card | 8/9 | 1 run | — | 0.16 (ledger) | unpinned |
| qwen/qwen3-14b | own VM, one 24 GB card | 8/9 | 1 run | — | 0.78 (ledger) | unpinned |
| openai/gpt-5.4-mini | vendor only | 8/9 | 1 run | — | 2.03 (ledger) | pinned |
| openai/gpt-5.6-sol | vendor only | 8/9 | 1 run | — | 5.49 (ledger) | pinned |
Reply draft · 10 held-out cases · 8 candidates, 8 within one case of the best
| model | can run on | held-out | pass^k | resists injection | $ / 10k | run |
|---|---|---|---|---|---|---|
| gemma4:e2b-it-qat | local machine | 10/10 | pass^3 10/10 | 1/2 | ≈0 | snapshot |
| mistralai/mistral-small-3.2-24b-instruct | own VM, one 24–48 GB card | 10/10 | 1 run | — | 0.39 (ledger) | unpinned |
| google/gemma-4-26b-a4b-it | own VM, one 24 GB card | 10/10 | 1 run | — | 0.49 (ledger) | unpinned |
| google/gemma-4-31b-it | own VM, one 48 GB card | 10/10 | 1 run | — | 0.50 (ledger) | unpinned |
| qwen/qwen3-14b | own VM, one 24 GB card | 10/10 | 1 run | — | 0.89 (ledger) | unpinned |
| openai/gpt-5.4-mini | vendor only | 10/10 | 1 run | — | 5.06 (ledger) | pinned |
| openai/gpt-5.6-sol | vendor only | 10/10 | 1 run | — | 16 (ledger) | pinned |
| openai/gpt-oss-20b | own VM, one 24 GB card | 9/10 | 1 run | — | 0.19 (ledger) | unpinned |
Digest of many items · 10 held-out cases · 8 candidates, 5 within one case of the best
| model | can run on | held-out | pass^k | resists injection | $ / 10k | run |
|---|---|---|---|---|---|---|
| openai/gpt-5.6-sol | vendor only | 8/10 | 1 run | — | 44 (ledger) | pinned |
| gemma4:e4b-it-qat | local machine | 7/10 | pass^3 7/10 | — | ≈0 | snapshot |
| google/gemma-4-26b-a4b-it | own VM, one 24 GB card | 7/10 | 1 run | — | 1.53 (ledger) | unpinned |
| google/gemma-4-31b-it | own VM, one 48 GB card | 7/10 | 1 run | — | 1.53 (ledger) | unpinned |
| mistralai/mistral-small-3.2-24b-instruct | own VM, one 24–48 GB card | 7/10 | 1 run | — | 1.59 (ledger) | unpinned |
| qwen/qwen3-14b | own VM, one 24 GB card | 6/10 | 1 run | — | 3.11 (ledger) | unpinned |
| openai/gpt-5.4-mini | vendor only | 5/10 | 1 run | — | 14 (ledger) | pinned |
| openai/gpt-oss-20b | own VM, one 24 GB card | 3/10 | 1 run | — | 0.70 (ledger) | unpinned |
CRM brief over tool calls · 18 held-out cases · 12 candidates, 3 within one case of the best
| model | can run on | held-out | pass^k | resists injection | $ / 10k | run |
|---|---|---|---|---|---|---|
| x-ai/grok-4.6 | vendor only | 16/18 | 1 run | — | 164 (ledger) | pinned |
| gemma4-e2b-ctx16k | local machine | 15/18 | pass^3 15/18 | 3/3 | ≈0 | snapshot |
| deepseek/deepseek-v4-pro-0813 | own VM, GPU node | 15/18 | 1 run | — | 86 (ledger) | unpinned |
| google/gemma-4-31b-it | own VM, one 48 GB card | 14/18 | 1 run | — | 5.41 (ledger) | unpinned |
| openai/gpt-5.6-sol | vendor only | 14/18 | 1 run | — | 127 (ledger) | pinned |
| google/gemma-4-26b-a4b-it | own VM, one 24 GB card | 13/18 | 1 run | — | 5.01 (ledger) | unpinned |
| z-ai/glm-5.3 | own VM, GPU node | 12/18 | 1 run | — | 24 (ledger) | pinned |
| qwen/qwen3-14b | own VM, one 24 GB card | 11/18 | 1 run | — | 7.48 (ledger) | unpinned |
| mistralai/mistral-small-3.2-24b-instruct | own VM, one 24–48 GB card | 10/18 | 1 run | — | 4.79 (ledger) | unpinned |
| openai/gpt-5.4-mini | vendor only | 10/18 | 1 run | — | 34 (ledger) | pinned |
| moonshotai/kimi-k3 | own VM, GPU node | 9/18 | 1 run | — | 114 (ledger) | pinned |
| openai/gpt-oss-20b | own VM, one 24 GB card | 1/18 | 1 run | — | 0.16 (ledger) | unpinned |
CRM fields from a call · 19 held-out cases · 10 candidates, 1 within one case of the best
| model | can run on | held-out | pass^k | resists injection | $ / 10k | run |
|---|---|---|---|---|---|---|
| moonshotai/kimi-k3 | own VM, GPU node | 17/19 | 1 run | — | 92 (ledger) | pinned |
| openai/gpt-oss-20b | own VM, one 24 GB card | 15/19 | 1 run | — | 0.83 (ledger) | unpinned |
| google/gemma-4-31b-it | own VM, one 48 GB card | 15/19 | 1 run | — | 2.88 (ledger) | unpinned |
| z-ai/glm-5.3 | own VM, GPU node | 15/19 | 1 run | — | 26 (ledger) | pinned |
| openai/gpt-5.6-sol | vendor only | 15/19 | 1 run | — | 65 (ledger) | pinned |
| gemma4-e4b-ctx16k | local machine | 14/19 | pass^3 14/19 | — | ≈0 | snapshot |
| mistralai/mistral-small-3.2-24b-instruct | own VM, one 24–48 GB card | 13/19 | 1 run | — | 2.92 (ledger) | unpinned |
| google/gemma-4-26b-a4b-it | own VM, one 24 GB card | 11/19 | 1 run | — | 2.85 (ledger) | unpinned |
| qwen/qwen3-14b | own VM, one 24 GB card | 11/19 | 1 run | — | 4.39 (ledger) | unpinned |
| openai/gpt-5.4-mini | vendor only | 11/19 | 1 run | — | 23 (ledger) | pinned |
Call summary + action items · 19 held-out cases · 12 candidates, 3 within one case of the best
| model | can run on | held-out | pass^k | resists injection | $ / 10k | run |
|---|---|---|---|---|---|---|
| x-ai/grok-4.6 | vendor only | 14/19 | 1 run | — | 133 (ledger) | pinned |
| moonshotai/kimi-k3 | own VM, GPU node | 14/19 | 1 run | — | 134 (ledger) | pinned |
| openai/gpt-5.6-sol | vendor only | 13/19 | 1 run | — | 78 (ledger) | pinned |
| google/gemma-4-26b-a4b-it | own VM, one 24 GB card | 12/19 | 1 run | — | 2.68 (ledger) | unpinned |
| openai/gpt-5.4-mini | vendor only | 12/19 | 1 run | — | 24 (ledger) | pinned |
| deepseek/deepseek-v4-pro-0813 | own VM, GPU node | 12/19 | 1 run | — | 42 (ledger) | unpinned |
| google/gemma-4-31b-it | own VM, one 48 GB card | 11/19 | 1 run | — | 2.70 (ledger) | unpinned |
| qwen/qwen3-14b | own VM, one 24 GB card | 11/19 | 1 run | — | 4.14 (ledger) | unpinned |
| gemma4-e4b-ctx16k | local machine | 10/19 | pass^3 10/19 | — | ≈0 | snapshot |
| z-ai/glm-5.3 | own VM, GPU node | 10/19 | 1 run | — | 29 (ledger) | pinned |
| openai/gpt-oss-20b | own VM, one 24 GB card | 9/19 | 1 run | — | 0.78 (ledger) | unpinned |
| mistralai/mistral-small-3.2-24b-instruct | own VM, one 24–48 GB card | 6/19 | 1 run | — | 2.72 (ledger) | unpinned |
2. Temperature zero is not determinism on hosted APIs. A gate that re-sits every case catches what moved.
| task | model | held-out n | single run | pass^3 pinned | pass^3 unpinned | pass^6 |
|---|---|---|---|---|---|---|
| Call summary + action items | z-ai/glm-5.3 | 13 | 10/13 | 8/13 | 6/13 | 6/13 |
| Call summary + action items | moonshotai/kimi-k3 | 13 | 10/13 | 10/13 | 9/13 | 9/13 |
| CRM brief over tool calls | z-ai/glm-5.3 | 16 | 10/16 | 10/16 | 9/16 | 8/16 |
| CRM brief over tool calls | moonshotai/kimi-k3 | 16 | 9/16 | 9/16 | 9/16 | 9/16 |
| Digest of many items | z-ai/glm-5.3 | 10 | 5/10 | 5/10 | 4/10 | 4/10 |
| Digest of many items | moonshotai/kimi-k3 | 10 | 6/10 | 4/10 | 6/10 | 4/10 |
| Email triage | z-ai/glm-5.3 | 11 | 11/11 | 10/11 | 10/11 | 10/11 |
| Email triage | moonshotai/kimi-k3 | 11 | 11/11 | 10/11 | 10/11 | 10/11 |
| Expense fields | z-ai/glm-5.3 | 9 | 8/9 | 8/9 | 8/9 | 8/9 |
| Expense fields | moonshotai/kimi-k3 | 9 | 9/9 | 9/9 | 8/9 | 8/9 |
| Reply draft | z-ai/glm-5.3 | 10 | 10/10 | 9/10 | 9/10 | 9/10 |
| Reply draft | moonshotai/kimi-k3 | 10 | 10/10 | 9/10 | 10/10 | 9/10 |
Six same-day runs on 1 September (three with the creator endpoint pinned, three not), on that day's case sets. pass^k is the share of cases passed on all k runs — the τ-bench reliability metric. The gate rule: a case counts only when three fresh loads agree; a challenger replaces the incumbent only if it passes every stable case the incumbent passes.
3. Four fine-tunes of the local champion, four refusals, and the cause named.
The same gate that picks a model refuses a fine-tune. What the loop can show honestly on synthetic cases is a smaller model holding the champion's score and a format failure fixed; an accuracy gain needs cases that are not at ceiling, which means a client's.
Three times the instrument was wrong
The build ledger stays private; three of its entries are public as post-mortems, each in the same shape: what we believed, what the probe showed, what changed.
- The window, not the weights — three capability verdicts that were a 4,096-token truncation nobody had checked.
- The gate said ok while five verdicts moved — an aggregate regression check, blind to cancelling flips; what replaced it.
- A green gate that cannot go red — a test runner that reports "passed" by construction, and an answer key inside the instrument.
Reproduce it
git clone https://github.com/Michiel-DK/agent-exam-suite && cd agent-exam-suite
python3 docs/probes/pass3-column-2026-09-28/pass_k.py # finding 2, from the committed snapshots and the six-run files
python3 docs/probes/matrix-2026-09-27/build_tables.py # the seven tables (local wall times need a local results/ dir)
./.cline/test.sh # the deterministic test gate, one process per file
Re-sitting an exam locally needs Ollama and the champion model: python3 sandbox/runner.py check email-triage compares a fresh run with the committed snapshot per case.