The table decides, not taste: choosing a model per rung

I built a retrieval “ladder” for a Rails app. A question comes in, the cheapest rung that can answer it with cited sources does, and if none can it climbs. The bottom two rungs cost nothing, an exact-match cache and then local embeddings, so only the top two ever reach a paid model. Ladder shapes are common; what’s worth writing down is how I picked which model sits on which paid rung, and how I keep that pick from rotting.

TL;DR

Three ids, one config file

All three model ids are configuration:

# config/initializers/llm.rb
Rails.application.config.llm = {
  # Groq
  rung_2: { model: "openai/gpt-oss-20b" },
  # Anthropic
  rung_3: { model: "claude-sonnet-4-5" },
  # Anthropic, eval only, never serves an answer
  judge:  { model: "claude-fable-5", effort: "high" },
}

Nothing in the retrieval logic names a model. That fact ended up mattering more than the specific ids.

The first eval table

The temptation is to reach for whichever model “feels” smart enough. I wanted a number. The first metered eval run produced grounded score against dollars per answer, where grounded asks whether the answer cites the source posts it claims to.

Rung Model $/answer
2 openai/gpt-oss-20b (Groq) $0.00009
3 claude-sonnet-4-5 (Anthropic) $0.00275
judge claude-fable-5, effort: high eval only, never serves

Rung 2 landed on gpt-oss-20b because it was the cheapest model that still cited its sources: 10 of 10 source hits, grounded 0.9. That’s the whole criterion, cheapest thing that clears the grounding bar. Rung 3 costs thirty times as much per answer, which is the whole reason the ladder tries not to get there, and in the run 25 of the 27 answers never did. The judge is stronger than either, because a judge should be strictly stronger than what it grades and here it grades Sonnet.

Then I ran an A/B on rung 2 against its bigger sibling, gpt-oss-120b, behind a VARIANT=large flag, metering the whole ladder each way.

Ladder variant Grounded $/answer
rung 2 = gpt-oss-20b 0.76 $0.00019
rung 2 = gpt-oss-120b 0.73 $0.00024
ladder off, rung 3 only 0.84 $0.00319

The larger model cost more and grounded worse. No version of “bigger model, just in case” survives that table. The last row is the argument for having a ladder at all: going straight to Sonnet every time buys about nine percent of grounding for an order of magnitude in cost.

Writing the acceptance rule before the experiment

The part worth stealing sits one step past measuring. I fixed the rule for accepting any future swap before there was a swap to accept: change the id, add its price, rerun the evals, accept only if grounded rises by more than $/answer does.

That rules out “this model feels sharper”, and it rules out the slower failure mode, the ratchet toward the more expensive model because it can’t hurt. Writing the rule down while no model and no incentive is attached is what keeps it honest later. It is also what keeps two obvious candidates off the ladder today.

Considered Why it loses now What would change that
Haiku 4.5 pricier than rung 2, weaker than rung 3, so it wins neither job rung 2’s source hits drop, fall-throughs pile up, or one vendor becomes an operational requirement
Opus rung 3 isn’t failing rung 3’s grounded falls below target, a question needs synthesis across several posts, or Fable becomes unavailable as judge

Neither is a no. Both are a not on this table.

The hedge I’m not going to paper over

Rung 2’s numbers are sturdier. The 25 of 27 that never reached rung 3 is the ladder doing its job, and it is also where most of the evidence sits. That’s a real first measurement and I’ll defend it as one.

What configuration-as-data actually bought

The payoff showed up faster than I expected. Rung 2 originally ran on Groq’s Llama models, both deprecated on 2026-06-17 and shut down on 2026-08-16. I didn’t catch it from a changelog or an outage. A read-only health check, a plain GET against .../models/llama-3.1-8b-instant, came back 404 before the first metered eval run had even executed.

Ladder, the class that walks the rungs, never names a model, so the fix was one line and no retrieval logic was touched. The models a system depends on are unstable, and the cost of one going away should land on a config diff. The other side of that coin: three vendors’ ids and prices are now kept current by hand, and the Llama shutdown is proof they go stale without warning.

Where this leaves the ladder

The rung-2 pick is backed by an A/B and a table that’s hard to argue with on its own terms. The rung-3 pick is a placeholder, labeled as one. I wrote the rule for changing either before there was a motive to bend it. The table decides, and it doesn’t get to decide on evidence it doesn’t have yet.