Latest edition ·

mager.co

Always learning. Always eating. Chicago.

Latest

Note

mager-bench: the benchmark was measuring my token budget

bench.mager.co

I added a fifth model to mager-bench today and found a bug in the benchmark itself. The bug is more interesting than the model.

The new model

GPT-OSS 120B — OpenAI's open-weights model, served on Groq's free tier. The board was two Anthropic models, one Meta, one Google, so this is the first OpenAI-lineage model on it and the first free-tier model that reasons before answering. Total API cost, again: $0.

Getting it running took three fixes, two of them token accounting again:

  • Groq bills reasoning tokens against max_tokens, so a thinking model needs headroom or its answer gets cut mid-implementation.
  • reasoning_format: "parsed" keeps chain-of-thought out of the response body, but it's a Groq-only parameter — the OpenAI SDK rejects unknown top-level kwargs, so it has to ride in extra_body. Miss this and all 13 challenges fail with an unhelpful TypeError.
  • The free tier caps that model at 8000 tokens per minute, counting prompt + max_tokens together, and rejects over-budget requests outright with a 413 rather than throttling them. So you can't just ask for a big budget and retry.

That last one turned out to matter far beyond one model.

The actual bug

Three of my challenges ask for a whole app in a single file: a Doom-style raycaster, a Vegas slot machine, a REST API client class. Every model scored near zero on all three. In an earlier note I wrote that the free models "collapse on the big signature challenges" and left it there, as if that were a fact about the models.

It was a fact about my harness. Every model was getting 2048 output tokens. A working raycaster does not fit in 2048 tokens. I wasn't measuring whether models can build Doom; I was measuring whether they can build Doom in 2048 tokens, and the answer is no for the same reason it would be no for a human handed a 30-line budget.

The fix is per-challenge budgets: doom and slots get 7000 tokens, api-client gets 4096, and the other ten stay at 2048 because they never came close to the cap. That last detail is what made the fix cheap — the ten unaffected challenges keep their existing scores, so I only had to re-run three challenges across five models instead of the whole board.

7000 isn't a round number I liked. It's the largest budget the tightest free tier on the board can actually accept, which is that 8000 TPM ceiling minus the prompt. The slowest model in the fleet sets the speed limit for everyone, because the alternative is giving different models different budgets and calling the scores comparable.

What happened when I fixed it

Mostly nothing, and that's the interesting part.

3.4× the budget produced 3–4× longer answers — Haiku's raycaster attempt went from 6,500 characters to 24,000. And every single model still ran out mid-file. Every response tail ends mid-expression: const time, for, Set. Nobody finished.

Score ranges across all five models, before and after:

ChallengeBefore (2048 tok)After (7000 tok)
doom0.0 – 1.00.0 – 1.0
slots0.0 – 2.30.0 – 0.7
api-client1.7 – 7.01.7 – 7.7

So the scores were roughly right, for entirely the wrong reason. A 70%-complete raycaster and a 25%-complete raycaster both score ~0 against a rubric that asks "is it a working game," which meant a real harness bug was invisible in the numbers. This is the part I'd want to know about someone else's eval: the metric didn't move when the bug was introduced, so it couldn't move when the bug was fixed either. Watching the scores would never have surfaced it. I found it by reading the raw responses and noticing they all ended mid-expression.

Gemini 2.5 Flash fails differently and more honestly: given 15,000 tokens it produced 2,000 characters of prose describing the raycaster it was about to write, then got cut off mid-sentence. It spent its budget thinking and narrating instead of emitting code.

The uncomfortable conclusion is that doom and slots may not be measurable on a free-tier board at all. A one-shot raycaster needs more output tokens than Groq's free tier can physically produce. Either I scope the prompts down to something completable in ~7000 tokens, or I accept that those two challenges only discriminate at the top of the market while everything else scores zero. I haven't decided which, but a prompt that no model on the board can finish isn't discriminating between them, and a column of zeros is not a measurement.

Two things I didn't expect

The re-run also caught bad data that had been sitting on the published board. Sonnet 4.6's slot-machine score of 2.3 came from a zero-character response, scored by llama-3.3-70b rather than the board's locked claude-sonnet-5 judge — a stale row from before I pinned the judge. An empty answer had been quietly earning points. The board now audits clean on both counts: no empty responses, no rows scored by the wrong judge. That check runs as part of publishing now, since the only reason I caught it was going looking.

And the leaderboard flipped:

ModelTierAvg
GPT-OSS 120Bfree6.4
Claude Sonnet 4.6paid6.2
Claude Haiku 4.5cheap6.2
Llama 3.3 70Bfree5.6
Gemini 2.5 Flashfree5.2

A free open-weights model is now top of my board. I want to be careful about how much that means: 6.4 versus 6.2 on single runs with no repeats is inside the noise, and part of the gap is Sonnet losing those inflated points from the empty-response row. This is not "open weights beat Claude." It's "on thirteen tasks I picked, scored once each by a Claude judge, they're indistinguishable" — which is still a real result, and cost nothing to produce.

The related change: the leaderboard now shows correctness, quality, and documentation as separate columns instead of one average. That immediately paid for itself. GPT-OSS has the best code-quality score on the board while ranking third on correctness — it writes clean code that's more often subtly wrong. A single averaged number hid that completely, and it's the most useful thing the board has told me all month.

Read note →
Note

Opus 5 is the default now, and subagents can nest three deep

claude-code CHANGELOG

Claude Code 2.1.219 shipped three changes worth pulling out of the changelog.

Opus 5 (claude-opus-5) is now the default Opus model, with a 1M context window. The context number is the part that matters for how I actually use it. My always-on session runs for days at a time, and the thing that used to end a session wasn't the task getting hard — it was the context filling up with tool output from work that was already finished.

Fast mode now applies to Opus 5 and Opus 4.8. Worth being precise about what this is, because the name invites the wrong assumption: fast mode is still Opus, with faster output. It isn't a silent downgrade to a smaller model. Toggle it with /fast.

Subagents can spawn nested subagents, up to depth 3 by default. This is the one that changes my setup. I run a principal-agent pattern — one always-on session on a Mac mini in Chicago that dispatches per-product subagents for magerblog, beatbrain, prxps, loooom, and kotsu. Until now that tree was two levels: the principal delegates once, and whatever it delegated to had to finish the job alone. A subagent that needed a code review or a scraper check had to do it inline.

The honest tradeoff: every level of nesting is another process with its own cold-start context, re-deriving things the parent already knew. Depth 3 is a budget, not a target. The failure mode isn't running out of levels — it's a tree where the leaves are solving the wrong problem confidently, and the root has no way to tell.

It lines up with the unhobbling shift from earlier this week. The model gets trusted with more judgment; the harness gets room to build deeper structures. Both bets are that the thing at the bottom of the tree can read the room.

Read note →
Note

mager-bench: free models, thinking-token bugs, and traces

bench.mager.co

Quick update on mager-bench. Three things happened today: the leaderboard got its first free models, two real eval-engineering bugs surfaced and got fixed, and every run is now traced end to end.

Free models on the board

I added Llama 3.3 70B via Groq's free tier and Gemini 2.5 Flash via Google AI Studio's free tier. Total API cost for the model calls: $0.

One wrinkle: I set out to add Gemini 2.0 Flash, but Google has fully retired it — the API 404s with "no longer available." 2.5 Flash is the free-tier successor, so that's what's on the board.

Two bugs, one cause: thinking tokens

Both bugs came from the same place — models that think by default, and token budgets that didn't account for it.

The judge crashed before scoring. The judge is Claude Sonnet 5, which thinks by default, and its token cap counted thinking and verdict together. Sometimes it spent the entire budget thinking and died before emitting a score. Fix: parse the response's text blocks properly and give the judge a much bigger budget.

Gemini scored zero on everything. Gemini 2.5 Flash also thinks by default, and its thoughts counted against the same output budget as its answer. Its first run produced ~300-character truncated stubs, which the judge correctly scored 0. Fix: give thinking its own headroom on top of the visible-answer budget, then re-run.

If you're building evals in 2026, this is the class of bug to expect. The model isn't bad; your token accounting is.

The results

Final board across 13 challenges, judged by Claude Sonnet 5:

ModelTierAvg
Claude Haiku 4.5cheap6.7
Claude Sonnet 4.6paid6.3
Llama 3.3 70Bfree5.5
Gemini 2.5 Flashfree5.4

The free models hang surprisingly close to the paid ones on bread-and-butter tasks — Llama scored 9.7 on fizzbuzz and 8.7 on refactor. But both collapse to roughly 0 on the big signature challenges (the doom raycaster, the slot machine). And Llama has one genuine superpower: speed. About 1.9s average response on Groq, versus 22–24s for everything else.

Traces

Every bench run is now instrumented with Arize AX via OpenTelemetry/OpenInference: one CHAIN span per model×challenge with the scores attached as metadata, auto-instrumented Anthropic and OpenAI calls nested underneath, and each result row stores its trace_id. Run detail pages on the dashboard deep-link straight to the trace in Arize, so when a score looks wrong I can open the exact conversation that produced it.

That's the loop I wanted: benchmarks are effectively free now if you're willing to debug thinking-token budgets, and observability is what closes the loop when the numbers look off.

Read note →
Note

mager-bench: the doom challenge

bench.mager.co

Simon Willison's pelican benchmark is one prompt — "Generate an SVG of a pelican riding a bicycle" — and the quality gap between models is immediately obvious when you look at the output. I wanted mager-bench to have its equivalent: one iconic, hard prompt where runnable output makes the difference self-evident.

The new doom challenge asks a model to build a Doom-style first-person raycasting FPS engine in a single HTML file, no external libraries. Open the file, play it.

The raycaster is not a Doom-themed prompt — it is literally Doom's engine. DDA raycasting, fish-eye correction, perspective-correct texture mapping, distance shading, a 16×16 map with rooms and dead ends, WASD + Pointer Lock mouse-look, AABB collision detection, a door that opens on E, an exit with a LEVEL COMPLETE timer, Z-buffer, minimap, FPS counter. All procedural — no images, no canvas assets loaded at runtime.

I grew up playing Doom. The technical core — casting rays through a 2D grid to fake a 3D world on underpowered hardware — is one of the most elegant ideas in the history of games. Models that actually understand the math produce something you can walk through. Models that fake it produce a gray box or a broken canvas.

The gap is instant and obvious. That's the benchmark.

All challenges are on GitHub.

Read note →
Note

mager-bench: free first

bench.mager.co

mager-bench v1.1 restructures the bench around cost. Default runs now use free models — Llama 3.3 70B and 3.1 8B through a new Groq adapter, plus Gemini Flash — and the judge defaults to a free model too, so a full 12-challenge leaderboard costs $0 and no longer requires an Anthropic key.

The README has carried the same two caveats since day one: the judge is a model too (by default, Claude grading Claude), and single-run variance is real. Both now have flags instead of apologies. --runs 3 reports mean ± stddev, so a score is a distribution rather than one roll. --judges gemini-2.0-flash,llama-3.3-70b averages a judge panel, so no model grades its own family alone.

export GROQ_API_KEY=... GEMINI_API_KEY=...
python bench.py --tier free --runs 3 \
  --judges gemini-2.0-flash,llama-3.3-70b

The expensive seats — Opus, GPT-4o, Sonnet — move to a public wishlist. There's a live /fund page and a FUND.md; Buy Me a Coffee is the working rail today, with GitHub Sponsors to follow. One rule either way: dollars only buy API tokens for published evals. Every funded run ships its raw responses and scores in results.json.

"Ships its raw responses" is now literal: every score on the dashboard links to a trace page with the exact prompt, the model's full response, and the judge's notes for each run. Haiku's 0.7 on doom stops being a number and becomes a readable failure.

Free tiers keep the leaderboard always on; crowdfunding unlocks the head-to-heads people actually argue about. All twelve challenges are on GitHub.

Read note →
Note

mager-bench

bench.mager.co

mager-bench expanded from 5 to 11 challenges. The four new ones cover territory the original set glossed over — testing discipline, debugging skill, async Python, and SQL fluency.

test-writing gives you a parse_duration() function and asks for a proper pytest suite using @pytest.mark.parametrize and pytest.raises. The interesting signal isn't whether models know pytest syntax — they all do — it's whether they parameterize across edge cases or just write three happy-path tests and call it done.

debug is a broken top_words() implementation with three distinct bugs. No stack trace, no error message — just wrong output. It tests careful reading more than raw code generation. Models that reach for the REPL in their heads before editing tend to do better here.

async-fetch asks for concurrent aiohttp requests with a per-request timeout and exponential backoff retry. It's a proxy for "can the model reason about failure modes at the call site, not just happy-path concurrency."

sql is a PostgreSQL query over an orders/customers schema that requires CTEs and window functions to answer cleanly. Most models can write either; the challenge is knowing when a window function is the right tool instead of a subquery.

Two more just landed: go-test asks for a table-driven WordCount test file using t.Run subtests and a benchmark — the idiomatic Go testing pattern that most models know in theory but often write awkwardly. elixir-test asks for an ExUnit suite with describe blocks and assert_raise, including a unicode string case that trips up anything relying on byte counts instead of String.length/1.

All eleven challenges are on GitHub.

Read note →
Note

skill-evals

github.com/mager/skill-evals

skill-evals is a small Claude Code plugin for evaluating the skills and agents you build. It came out of reading the awesome-evals PATTERNS playbook and wanting the patterns as something I could run inside a project, not just a reference to nod along to. v0.1 is two skills.

The first, error-analysis, is the unglamorous step most people skip: read 20–100 real traces, write a one-line note on the first thing that broke in each, cluster those into a handful of named failure modes, and rank them by frequency × severity. The point is that you can't write a good eval for a failure you haven't seen yet, and generic metrics like "helpfulness" point nowhere. The output is a short taxonomy that tells you what to actually measure — and which failures need a cheap code assertion versus an LLM judge.

The second, build-judge, is the one I learned the most from. Using an LLM to grade subjective things (tone, faithfulness, did-it-follow-the-instruction) is easy; trusting that grader is the hard part. On an imbalanced set — say 90% of outputs are fine — a judge that stamps everything "pass" scores 90% accuracy and catches none of the real failures. So the skill ships a stdlib-only score.py that reports true-positive and true-negative rate separately and gates on both, exiting non-zero below threshold so it drops straight into CI. The rubber-stamp judge fails that gate even at 90% accuracy, which is exactly the trap it's there to catch.

Since then, v0.2 shipped exactly the patterns I'd left out: add-assertions (deterministic check scaffolding), passk (pass@k versus pass^k with the unbiased estimator), synth-data (grounded synthetic sets when you have no traffic), and an eval-runner agent that runs a whole suite and reports honestly. I also did the dogfood — pointing eval-runner at all 124 posts on this very blog. The first pass screamed that zero posts passed every check, which is exactly the kind of number that makes you panic and learn nothing. The honest finding was that my spec was wrong, not the blog: it was grading against categories and a keyword format the real schema never used. Once I fixed the spec, every post came back clean. That's the lesson the whole thing is built to teach — most "failures" are your metric lying, and you only catch it by looking.

If you want the conceptual map behind these patterns rather than the code, I wrote one up: a plain-English tour of the eval types worth knowing.

Read note →
Note

Loooom v1.0

github.com/mager/loooom.xyz

Loooom is v1.0 — tagged and released on GitHub. The shape hasn't changed since the pivot post: fifteen curated non-technical skills, each scored against a written rubric. What v1.0 adds is the layer of testing that was missing.

The two rubric gates judge the skill text. The new third gate runs each skill the way an agent actually would — SKILL.md as the system prompt — and unit-tests its Agent Behavior contract with promptfoo: hook has to make you say what the song is about in one sentence before writing anything, stack has to kill the 23% credit card before entertaining the crypto bet, focus has to send your phone to another room. Thirty tests, two per skill, deterministic assertions plus an LLM rubric, all on Groq's free tier.

The whole harness still costs $0 — judge and tests both run on Groq's free tier. The price shows up in a different currency: run the suite three times back-to-back and you blow through the tokens-per-minute cap, and everything crawls behind HTTP 429s. Promptfoo's cache makes that mostly painless (passing tests don't re-run), but a free eval stack rations your iteration speed instead of your wallet. For a project this size, that's the right trade.

The first run came back 27/30, and the failures were the educational part. Two were the tests' fault, not the skills': story and frame were correctly following their own "make them name the one thing first" contract while my rubrics demanded the whole lecture in turn one. The third was a token cap truncating train before it reached progressive overload. Behavioral tests don't just check the skills — they force you to decide what the skill is actually supposed to do on the first turn.

The audit also closed an embarrassing loop: voice had been shipping without a worked example — the one skill not practicing what it preached, and the spec gate had been flagging it since day one. Fixed in v1.0.

I ran this whole launch with Claude Code on Fable, Anthropic's new model — the skill audit, the test suite, the release, and this note. First project I've shipped with it.

Read note →
Food

Cacio e Pepe

Three real ingredients — pecorino, pepper, pasta water — plus a knob of butter for insurance, tossed into a glossy sauce that never breaks.

Read post →
Tech

An OpenClaw setup for Dad

A plain-English walkthrough for setting up your own always-on AI assistant on a Mac mini — OpenClaw, Google Gemini, and Tailscale — written for a first-timer.

Read post →
Note

Keeping an always-on agent alive across reboots

I run a Claude Code agent on a Mac mini in Chicago that I reach over Telegram. The hard part isn't the agent, it's keeping it up without me — across crashes, model swaps, and the occasional reboot. The fix is layered supervision, where each layer owns one kind of failure:

  • run.sh loops the agent and watches its exit code. An in-session model switch exits with code 42; the loop sees that and relaunches on the new model. Any other code stops the loop and hands control up.
  • tmux holds the session. The CLI is interactive and wants a PTY, so it runs inside a detached tmux session rather than a bare background process.
  • launchd is the floor. A LaunchAgent with RunAtLoad starts the tmux session at login (so it survives a reboot), and a StartInterval watchdog re-checks every couple of minutes and rebuilds the session if it's gone.

The thing I keep relearning: "restart it when it dies" is not one job. A reboot, a crash, and an intentional model swap are different failures, and each wants a different layer to catch it. Pile them all into one script and it's brittle; separate them and the whole thing just stays up.

Read note →
Tech

Killing OpenClaw for a native Claude Code setup

I love OpenClaw. I hate that it doesn't run on my Claude Pro subscription. Turns out Claude Code, with the Telegram channels plugin and one CLAUDE.md, is the same harness — minus the daemon, the API bill, and the second LLM provider. Here's the actual recipe, ported from a hotel in Tokyo to a Mac mini in Chicago in forty minutes.

Read post →
Food

Japanese Butter Soy Spaghetti

A five-ingredient Japanese-style spaghetti — butter, tamari, and parmesan tossed with hot pasta and finished with green onion. The wafu pasta I kept eyeing in Tokyo, made at home in ten minutes.

Read post →
Browse the full archive