Ladder — the onboarding "climb"
A small, self-contained local web UI for watching and comparing LLMs on the same task. It is the no-data front door: a developer launches it and watches a small local model and a frontier model attempt the same task side by side — live reasoning, real tool calls, strict scoring — without needing any of their own traces first. As the task gets harder, the small local model keeps pace and then drops off where the frontier model carries on. That contrast is the hook into the rest of the improvement loop.
When to use
- A new user asks "what can a local model actually do?" / "is a small model good enough?" / "show me local vs frontier."
- During or right after
onboard, to make the local-vs-frontier difference concrete before steering them toward their own workload. - Any time a visceral, zero-setup-data demo of agentic behavior + scoring helps.
For the user's own eval across many candidate models, hand off to
compare-model-sweep. To serve a local model against the user's real
workload, hand off to run-local-model-lab.
Run it
Prerequisites (otherwise-silent assumptions):
- Apple Silicon (arm64) Mac with
uvonPATH— the local lane needs a current mlx stack (systemmlx_lmis often too old to load the model). - The local model cached at
~/.understudy/models/gemma-4-e2b-it-qat-mlx-vlm-understudy— thegemma-4-e2bid resolves to that directory, not a dir literally namedgemma-4-e2b. If it's missing, pull it with themanage-local-modelsskill (understudy models pull gemma-4-e2b-it-qat-mlx-vlm-understudy); the server prints an actionable error if it's absent. - Remote lanes only: run
understudy loginonce sounderstudy runcan inject the gateway key and org id. Without it a remote lane returns the literal notice[gateway not configured …]rather than erroring — don't mistake that for a real run.
Run from the repo root (the path below is relative):
understudy run -- uv run --with mlx-vlm --with mlx-lm python skills/ladder/serve.py
First invocation builds the mlx stack from PyPI (network, a few minutes) and the model cold-loads (~60s) — that pause is normal, not a hang. Then open http://localhost:8011/ladder.climb.html, or drive it headlessly (below).
This is a developer-run demo/onboarding tool, not a headless production service — though the SSE endpoint is browserless-friendly for an agent.
What you see
- Easy / medium rungs — single-shot classify tasks; watch the model think, answer, and get a Pass/Fail against the gold label.
- Hard rung — a real tool-calling agent loop against the synthetic Larkfield
world (find the account, read the policy, apply the latest discount, update the
plan, email the right teams,
finish), scored by final state. - VS mode — two models race the same task side by side. Local-vs-local serializes on the one GPU; local-vs-gateway runs concurrently.
Remote runs go through the Understudy gateway and are billed. The picker marks every gateway-backed lane and every such run is disclosed.
What you can actually run ($0 vs billed)
- $0, fully local — the "climb". Run
gemma-4-e2bacross all three rungs and watch where it breaks: it passes the easy/medium classify tasks and typically fails the hard tool-calling task. That difficulty climb is the free demo and a real signal on its own, entirely on your machine. - A side-by-side model-vs-model comparison usually costs money. The "compare
two" (VS) button pairs the local model against a gateway-backed remote model
by default — so triggering VS bills a gateway run. No second free model ships
enabled (the local LFM lane is coded but commented out in
serve.py). So "$0" and "two models side by side" are not both available out of the box: take the free climb, or accept the gateway cost for the race.
Drive it headlessly
No browser required — the page is just a client of one SSE endpoint:
curl -N 'http://localhost:8011/run?task=sort-email&model=gemma-4-e2b'
The stream ends with a done event carrying the result (correct, response,
tok_s for the classify rungs; strict / dense / passes for the hard rung).
GET /tasks lists task ids (sort-email, match-search, hard.sla_route); GET /models lists the lanes.
Safety Gates
- Local-first, $0 on the local lane. The mlx model runs on the developer's
machine, the server binds
127.0.0.1only, and nothing is uploaded. - Gateway runs are billed. Remote lanes go through the Understudy gateway and cost money. The picker marks them and every such run is disclosed in the UI — do not route to one without the developer understanding it bills.
- Synthetic data only. The "Larkfield" world is invented; no real customer or workload data is involved.
- No silent downloads. The server only loads a cached local model from
~/.understudy/models; if it is missing it says so — pull it withmanage-local-models, never auto-download weights.
What's here
Four stdlib, self-contained files: serve.py (HTTP + SSE server + the agent
loop), env/world.py (the synthetic Larkfield world + scoring),
fixtures/hard/tool_tasks.jsonl (the hard tasks as data — add a row and it's
live and scored), and viewer/ladder.climb.html (the UI).
See reference.md for the architecture, the model lanes +
tool-call dialects, the Larkfield world + scoring contract, how to add your own
tasks (a new hard tool task is a single JSONL row — schema, assertion types, and
tool list are documented there), and the running gotchas (uv-vs-system mlx, the
one-GPU thread funnel, UNDERSTUDY_MODEL_HOME).