← writing
published

The Eval That Said No: Trying to Cut My Inference Bill by 71%

2026-0710 min readevalsllmcostragplatform

In plain terms — I found out this site's most expensive feature cost more than my own dashboard claimed, because the expensive part wasn't being measured at all. So I tried swapping in a cheaper AI model to save money. Before shipping it, I ran the automated test suite that checks whether the site's answers stay honest and well-sourced. It failed — twice, for two unrelated reasons. I kept the expensive model. This is what the tests caught.


The bill that wasn't

This site does two expensive things. It answers questions about my work with citations, and it turns a pasted job description into a requirement-by-requirement fit report. I have a small private dashboard that tells me what those cost.

It said fit reports ran about eight cents each.

That number was wrong, and the way it was wrong is the more interesting part. The fit pipeline makes two model calls: one to extract the requirements out of the job description, another to assess each one against my corpus and write the report. Neither of them logged a single token. The dashboard was summing the messages table — which only ever contained chat turns — and reporting the total as if it covered everything.

So the most expensive operation on the site was invisible to the thing whose job was to watch it. Worse, the daily spend guard I had added specifically to bound costs read from the same table, which meant the guard could not see the largest cost path either. Its own code comment described chat as "the dominant cost path." It wasn't.

I added token logging to the fit pipeline and measured properly:

inputoutputcost
Fit report~18,700~15,900~$0.20
Chat turn~4,000~150~$0.01

Two and a half times the estimate, and the shape explains why: a fit report is output-heavy, and output tokens cost five times input. A chat turn is the opposite — a 27:1 input-to-output ratio, because retrieval stuffs a pile of context into every prompt and gets back a paragraph.

There's a second lesson buried in the dashboard. The "authoritative lifetime spend" figure it displayed came from the provider's account-level endpoint, and I run other tooling on that same account under a different key. So it was quietly attributing roughly half its headline number to software that has nothing to do with this website. A reconciliation gap the dashboard framed as unexplained site spend was mostly just a different program.

The tempting arithmetic

With real numbers in hand, the substitution looked obvious. Against the Claude Sonnet model this site runs on, a comparable open-weight model priced at roughly a third of the input rate and a quarter of the output rate would take monthly spend at my expected outreach volume from about $5.50 to about $1.60.

A 71% cut. And a deadline: the introductory pricing on my current model expires at the end of August, after which doing nothing gets 50% more expensive on its own schedule.

There was also a smaller swap available. The site uses a fast, cheap model for a supporting task — rewriting a follow-up question into a standalone search query, and gating whether pasted text is really a job description. Two candidate replacements were cheaper than the incumbent on both input and output. On price alone there was no argument for keeping it.

I want to be precise about the size of the prize, because it matters for the conclusion: the generation swap was worth about $46 a year. The supporting-task swap was worth about $1 a year.

The instrument

I did not have to guess whether the cheaper models were good enough, because this site already ships with the thing that answers that question.

There is an evaluation harness — a set of cases that hit the real HTTP endpoints and assert on what comes back. Citation grounding. Refusal of out-of-scope questions. Resistance to prompt injection. Whether the agent says "I don't have that" instead of inventing something. It gates releases; I run it before deploys and treat a failure as a blocker.

But it had a weakness for this decision. Of its 40 cases, only five exercised the fit pipeline — which is both the most expensive operation and the highest-stakes output, since fit reports are the thing I send to hiring managers. Five cases is too thin a basis for a model swap.

So I added five more, each targeting a property I expected a cheaper model to drop first:

Forty-five cases. The incumbent configuration scored 44/45, including 10/10 on fit. The new cases were passable; the baseline passed them.

Two failures, and the difference between them

I ran the cheap model in the generation slot, then each candidate in the supporting slot, one variable at a time against the baseline.

The generation swap: 34/45

Nine of ten fit reports failed with the same message — no report arrived. Not a bad report. No report at all.

The server logs gave the cause immediately:

AI_NoObjectGeneratedError: could not parse the response
  caused by AI_JSONParseError: JSON parsing failed: Text: ```json

The model was wrapping its structured output in markdown code fences. The parser expected raw JSON, got a fenced block, and gave up. Plus a couple of timeouts on the longest reports.

This says nothing about whether that model writes good fit reports. I never found out. It's an output-contract incompatibility — plausibly fixable with a different structured-output mode or a preprocessing step. I want to be careful here, because "the cheap model was worse" is the conclusion this data does not support, and it's the one that would be easiest to write.

What the data does support is a second finding, and this one is a genuine behavioral difference. Look at the split:

CategoryIncumbentCandidate
Factual accuracy14/1515/15
"I don't have that"5/53/5

The cheaper model was better at answering questions and worse at declining them. It failed on missing honest-gap language — cases where the correct response is to say the corpus doesn't contain something.

For most products that trade is fine, maybe even good. For this one it is exactly backwards. The entire premise is an agent that refuses to oversell me. A model more willing to answer and less willing to admit ignorance is the wrong instrument regardless of what it costs, and that finding is independent of the JSON problem.

The supporting swap: 22/35

The second failure was worse, and much quieter.

Swapping only the supporting model — the one that rewrites a question into a search query — dropped factual accuracy from 14/15 to 8/15. Adversarial and logistics categories fell too. Nothing else changed.

The mechanism is a single catch block. When the rewrite step fails, the code falls back to using the raw user message as the search query:

} catch {
  return { query: message, intent: "about_work" };
}

That is a reasonable piece of defensive engineering. It is also a perfect way to hide a bad model. No exception surfaces. No error rate moves. No alert fires. The retrieval step just gets a worse query, pulls less relevant context, and the answer comes back a little thinner and a little less well-sourced — indefinitely, and invisibly.

I would not have caught this in production. There is no symptom to notice. The only reason I found it is that I ran forty-five cases against it and watched the citation assertions fail.

The other candidate for that slot passed at 32/35 — above my 90% threshold, but still two cases below the incumbent's 34/35 on the identical subset. Two cases is within the run-to-run variance I see from this harness, so I won't claim it's definitely worse. I'll only claim there's no evidence it's better, and the entire upside is about a dollar a year.

What I kept

Nothing changed. Both models stayed.

That is not the outcome I wanted when I started, and I want to be plain about the economics: I spent roughly $3.60 on evaluation runs to protect a $46/year saving. On its face that's a poor trade.

It isn't, and the reason is what the failure would have cost. The fit report is not a feature of this site — it's the artifact I send to hiring managers. Nine out of ten of them silently failing to generate is not a degraded experience, it's a broken one, discovered by someone I was trying to impress. And the retrieval regression had no symptom at all, which means the alternative to catching it in an eval was never catching it.

The dashboard error is worth restating on its own. I built a cost dashboard and a spend guard, and both were blind to the single most expensive thing the system did. Not because of a subtle bug — because nothing wrote the number down. If you're going to make decisions from a measurement, it's worth periodically checking that the measurement is measuring.

What I can claim, and what I can't

I can claim: on my corpus, with my schema and my integration, the cheaper generation model could not complete the fit pipeline, and was measurably worse at declining to answer. The cheaper rewrite model degraded retrieval badly enough to cut factual accuracy nearly in half, silently.

I can't claim either model is worse in general. One failed on output format, not reasoning. Both were evaluated against one corpus, one schema, and one integration path, on a 45-case suite. A different harness might well get a different answer, and if I fixed the fence-parsing issue the generation result could change entirely.

I also can't claim the harness is complete. Forty-five cases is enough to catch a 9-out-of-10 structural failure and a 40-point accuracy drop. It is not enough to detect a subtle quality regression, and I don't currently have a way to measure whether one model writes a more useful fit report than another — only whether it writes a valid, grounded, honest one. That's a real gap, not a rhetorical one.

The thing I'd take away, if I were reading this rather than writing it: the cheapest part of this exercise was the part that told me no.