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

  1. 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.
  2. 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.
  3. 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: CRM brief over tool calls · 18 held-out · bar = best score or one case from itgrok-4.616/18 · 164gemma4-e2b-ctx16k15/18 · ≈0 · pickeddeepseek-v4-pro-081315/18 · 86gemma-4-31b-it14/18 · 5.41gpt-5.6-sol14/18 · 127gemma-4-26b-a4b-it13/18 · 5.01glm-5.312/18 · 24qwen3-14b11/18 · 7.48mistral-small-3.2-24b-…10/18 · 4.79gpt-5.4-mini10/18 · 34kimi-k39/18 · 114gpt-oss-20b1/18 · 0.16quality bar · 15 of 18
    taskbarcheapest at the barheld-out$ / 10kresists injectionGPT-5.6-Sol, same task
    Email triage9 of 11gemma4:e2b-it-qat · local machine10/11≈00/29/11 · 5.72 (ledger)
    Expense fields8 of 9gemma4:e2b-it-qat · local machine8/9≈0—8/9 · 5.49 (ledger)
    Reply draft9 of 10gemma4:e2b-it-qat · local machine10/10≈01/210/10 · 16 (ledger)
    Digest of many items7 of 10gemma4:e4b-it-qat · local machine7/10≈0—8/10 · 44 (ledger)
    CRM brief over tool calls15 of 18gemma4-e2b-ctx16k · local machine15/18≈03/314/18 · 127 (ledger)
    CRM fields from a call16 of 19kimi-k3 · own VM, GPU node17/1992 (ledger)—15/19 · 65 (ledger)
    Call summary + action items13 of 19gpt-5.6-sol · vendor only13/1978 (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.

  4. 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:
    candidateheld-outwhat movedverdictcause named
    CRM follow-up, LoRA v1 · 11 training rows12 → 12 of 182 up / 3 down · 3 held-out cases down, one passrefusednine of the eleven rows made one tool call; the adapter stops after one call
    CRM follow-up, LoRA v2 · 16 rows12 → 12 of 182 up / 2 down · 2 stable held-out cases down, three loadsrefusedflat total hiding two up, two down; too few rows with two calls in one turn
    CRM follow-up, LoRA v3 · 30 rows12 → 11 of 184 up / 5 down · 3 stable held-out cases down, three loadsrefusedno 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 masked12 → 9 of 181 up / 7 down · 4 stable held-out cases down, three loadsrefusedlearned the decisions, and a “call again” reflex with them
    Request intake, thinking switched off by the chat template (no training)19 → 17 of 200 up / 2 down · same speed as the adapterrefusedthe switch loses the hand-over rules the adapter kept
    Request intake, LoRA · 22 rows, 9 minutes of training19 → 18 of 200 up / 1 down · −87% output tokens, 4.2× faster; one pass, not snapshottedheld within one case; not promotedremoves 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

tiermodels 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.

Evidence: the local champion ties or beats the hosted flagships on email triage, expense fields, reply drafts, the CRM brief over tool calls (where the 2B is the best model of any size) and the digest; it sits one case behind the top group on CRM field write-back and four behind on long call summaries. Cost: local ≈0, the best open-weights hosted rows $0.23–$5.41, the vendor flagship $5–$127 per 10k. metered
vendor only (closed weights)own VM (open weights, measured via API)local machine (3-load snapshot)hover a mark for the model
Email triage · 11 held-out$0.1$1$10$100≈050%100%$ per 10k tasks (log)gemma4:e2b-it-qatmistral-small-3.2…Expense fields · 9 held-out$0.1$1$10$100≈050%100%$ per 10k tasks (log)gemma4:e2b-it-qatmistral-small-3.2…Reply draft · 10 held-out$0.1$1$10$100≈050%100%$ per 10k tasks (log)gemma4:e2b-it-qatmistral-small-3.2…Digest of many items · 10 held-out$0.1$1$10$100≈050%100%$ per 10k tasks (log)gemma4:e4b-it-qatgpt-5.6-solCRM brief over tool calls · 18 held-out$0.1$1$10$100≈050%100%$ per 10k tasks (log)gemma4-e2b-ctx16kgrok-4.6CRM fields from a call · 19 held-out$0.1$1$10$100≈050%100%$ per 10k tasks (log)gemma4-e4b-ctx16kkimi-k3Call summary + action items · 19 held-out$0.1$1$10$100≈050%100%$ per 10k tasks (log)gemma4-e4b-ctx16kgrok-4.6

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

modelcan run onheld-outpass^kresists injection$ / 10krun
gemma4:e2b-it-qatlocal machine10/11pass^3 10/110/2≈0snapshot
mistralai/mistral-small-3.2-24b-instructown VM, one 24–48 GB card10/111 run—0.22 (ledger)unpinned
google/gemma-4-26b-a4b-itown VM, one 24 GB card10/111 run—0.23 (ledger)unpinned
google/gemma-4-31b-itown VM, one 48 GB card10/111 run—0.24 (ledger)unpinned
openai/gpt-5.4-minivendor only10/111 run—1.98 (ledger)pinned
openai/gpt-oss-20bown VM, one 24 GB card9/111 run—0.11 (ledger)unpinned
qwen/qwen3-14bown VM, one 24 GB card9/111 run—1.02 (ledger)unpinned
openai/gpt-5.6-solvendor only9/111 run—5.72 (ledger)pinned

Expense fields · 9 held-out cases · 8 candidates, 8 within one case of the best

modelcan run onheld-outpass^kresists injection$ / 10krun
mistralai/mistral-small-3.2-24b-instructown VM, one 24–48 GB card9/91 run—0.22 (ledger)unpinned
google/gemma-4-26b-a4b-itown VM, one 24 GB card9/91 run—0.23 (ledger)unpinned
google/gemma-4-31b-itown VM, one 48 GB card9/91 run—0.23 (ledger)unpinned
gemma4:e2b-it-qatlocal machine8/9pass^3 8/9—≈0snapshot
openai/gpt-oss-20bown VM, one 24 GB card8/91 run—0.16 (ledger)unpinned
qwen/qwen3-14bown VM, one 24 GB card8/91 run—0.78 (ledger)unpinned
openai/gpt-5.4-minivendor only8/91 run—2.03 (ledger)pinned
openai/gpt-5.6-solvendor only8/91 run—5.49 (ledger)pinned

Reply draft · 10 held-out cases · 8 candidates, 8 within one case of the best

modelcan run onheld-outpass^kresists injection$ / 10krun
gemma4:e2b-it-qatlocal machine10/10pass^3 10/101/2≈0snapshot
mistralai/mistral-small-3.2-24b-instructown VM, one 24–48 GB card10/101 run—0.39 (ledger)unpinned
google/gemma-4-26b-a4b-itown VM, one 24 GB card10/101 run—0.49 (ledger)unpinned
google/gemma-4-31b-itown VM, one 48 GB card10/101 run—0.50 (ledger)unpinned
qwen/qwen3-14bown VM, one 24 GB card10/101 run—0.89 (ledger)unpinned
openai/gpt-5.4-minivendor only10/101 run—5.06 (ledger)pinned
openai/gpt-5.6-solvendor only10/101 run—16 (ledger)pinned
openai/gpt-oss-20bown VM, one 24 GB card9/101 run—0.19 (ledger)unpinned

Digest of many items · 10 held-out cases · 8 candidates, 5 within one case of the best

modelcan run onheld-outpass^kresists injection$ / 10krun
openai/gpt-5.6-solvendor only8/101 run—44 (ledger)pinned
gemma4:e4b-it-qatlocal machine7/10pass^3 7/10—≈0snapshot
google/gemma-4-26b-a4b-itown VM, one 24 GB card7/101 run—1.53 (ledger)unpinned
google/gemma-4-31b-itown VM, one 48 GB card7/101 run—1.53 (ledger)unpinned
mistralai/mistral-small-3.2-24b-instructown VM, one 24–48 GB card7/101 run—1.59 (ledger)unpinned
qwen/qwen3-14bown VM, one 24 GB card6/101 run—3.11 (ledger)unpinned
openai/gpt-5.4-minivendor only5/101 run—14 (ledger)pinned
openai/gpt-oss-20bown VM, one 24 GB card3/101 run—0.70 (ledger)unpinned

CRM brief over tool calls · 18 held-out cases · 12 candidates, 3 within one case of the best

modelcan run onheld-outpass^kresists injection$ / 10krun
x-ai/grok-4.6vendor only16/181 run—164 (ledger)pinned
gemma4-e2b-ctx16klocal machine15/18pass^3 15/183/3≈0snapshot
deepseek/deepseek-v4-pro-0813own VM, GPU node15/181 run—86 (ledger)unpinned
google/gemma-4-31b-itown VM, one 48 GB card14/181 run—5.41 (ledger)unpinned
openai/gpt-5.6-solvendor only14/181 run—127 (ledger)pinned
google/gemma-4-26b-a4b-itown VM, one 24 GB card13/181 run—5.01 (ledger)unpinned
z-ai/glm-5.3own VM, GPU node12/181 run—24 (ledger)pinned
qwen/qwen3-14bown VM, one 24 GB card11/181 run—7.48 (ledger)unpinned
mistralai/mistral-small-3.2-24b-instructown VM, one 24–48 GB card10/181 run—4.79 (ledger)unpinned
openai/gpt-5.4-minivendor only10/181 run—34 (ledger)pinned
moonshotai/kimi-k3own VM, GPU node9/181 run—114 (ledger)pinned
openai/gpt-oss-20bown VM, one 24 GB card1/181 run—0.16 (ledger)unpinned

CRM fields from a call · 19 held-out cases · 10 candidates, 1 within one case of the best

modelcan run onheld-outpass^kresists injection$ / 10krun
moonshotai/kimi-k3own VM, GPU node17/191 run—92 (ledger)pinned
openai/gpt-oss-20bown VM, one 24 GB card15/191 run—0.83 (ledger)unpinned
google/gemma-4-31b-itown VM, one 48 GB card15/191 run—2.88 (ledger)unpinned
z-ai/glm-5.3own VM, GPU node15/191 run—26 (ledger)pinned
openai/gpt-5.6-solvendor only15/191 run—65 (ledger)pinned
gemma4-e4b-ctx16klocal machine14/19pass^3 14/19—≈0snapshot
mistralai/mistral-small-3.2-24b-instructown VM, one 24–48 GB card13/191 run—2.92 (ledger)unpinned
google/gemma-4-26b-a4b-itown VM, one 24 GB card11/191 run—2.85 (ledger)unpinned
qwen/qwen3-14bown VM, one 24 GB card11/191 run—4.39 (ledger)unpinned
openai/gpt-5.4-minivendor only11/191 run—23 (ledger)pinned

Call summary + action items · 19 held-out cases · 12 candidates, 3 within one case of the best

modelcan run onheld-outpass^kresists injection$ / 10krun
x-ai/grok-4.6vendor only14/191 run—133 (ledger)pinned
moonshotai/kimi-k3own VM, GPU node14/191 run—134 (ledger)pinned
openai/gpt-5.6-solvendor only13/191 run—78 (ledger)pinned
google/gemma-4-26b-a4b-itown VM, one 24 GB card12/191 run—2.68 (ledger)unpinned
openai/gpt-5.4-minivendor only12/191 run—24 (ledger)pinned
deepseek/deepseek-v4-pro-0813own VM, GPU node12/191 run—42 (ledger)unpinned
google/gemma-4-31b-itown VM, one 48 GB card11/191 run—2.70 (ledger)unpinned
qwen/qwen3-14bown VM, one 24 GB card11/191 run—4.14 (ledger)unpinned
gemma4-e4b-ctx16klocal machine10/19pass^3 10/19—≈0snapshot
z-ai/glm-5.3own VM, GPU node10/191 run—29 (ledger)pinned
openai/gpt-oss-20bown VM, one 24 GB card9/191 run—0.78 (ledger)unpinned
mistralai/mistral-small-3.2-24b-instructown VM, one 24–48 GB card6/191 run—2.72 (ledger)unpinned

2. Temperature zero is not determinism on hosted APIs. A gate that re-sits every case catches what moved.

Evidence: GLM-5.3 passed 10 of 13 call summaries on one run and 6 of 13 across six runs the same day (pass^6). Kimi K3 lost 0–1 case per task. The local champions passed every case on all three loads (pass^3 equals the single-run score on all 8 exams, 0 unstable). When the local runtime was upgraded on 2 October, re-sitting all 8 exams moved 1 of 220 cases, 0 of them held-out, 0 unstable; the August bump had moved 5 of 22 on one exam in both directions. metered
taskmodelheld-out nsingle runpass^3 pinnedpass^3 unpinnedpass^6
Call summary + action itemsz-ai/glm-5.31310/138/136/136/13
Call summary + action itemsmoonshotai/kimi-k31310/1310/139/139/13
CRM brief over tool callsz-ai/glm-5.31610/1610/169/168/16
CRM brief over tool callsmoonshotai/kimi-k3169/169/169/169/16
Digest of many itemsz-ai/glm-5.3105/105/104/104/10
Digest of many itemsmoonshotai/kimi-k3106/104/106/104/10
Email triagez-ai/glm-5.31111/1110/1110/1110/11
Email triagemoonshotai/kimi-k31111/1110/1110/1110/11
Expense fieldsz-ai/glm-5.398/98/98/98/9
Expense fieldsmoonshotai/kimi-k399/99/98/98/9
Reply draftz-ai/glm-5.31010/109/109/109/10
Reply draftmoonshotai/kimi-k31010/109/1010/109/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.

Evidence: LoRA on the 2B for the CRM brief, 30 training rows. v1–v3 trained with no prompt mask on rows that were 95% tool payload by characters, so the loss never saw the decisions: held-out 12→11, the adapter contradicting its own training row. v4 cut one example per assistant step with the prompt masked: it fixed the two-call case for the first time and taught a "call again" reflex, held-out 12→9. Every attempt was killed by its pre-registered rule (any stable held-out case down). metered

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.

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.