< back to notebook > entry
> 2026-07-14 · standing · 9 min read

The model may not source a fact

  • ai
  • llm
  • architecture
  • validation
  • self-hosting
  • cost
  • sumo

I gave a language model the results of a sumo tournament and asked it to write the day's report. It lied for twelve versions. The fix wasn't a better prompt — it was a validator that takes away the model's authority to know anything. And the validator's rejection rate is what finally told me the truth about running models on my own box — a GPU VPS is $500 a month, my newspaper desk needs twenty prompts, and the EU-hosted 120b that does the job better costs four cents.

Sumo App has a newspaper desk in it. After each day of a tournament, The Dohyō Times files a short report: who won, who was upset, who leads the yūshō race, who withdrew. It’s a few sentences of prose, and it was the most obvious job for a language model I’ve ever had.

I had the facts already. The app computes them anyway — day digests, upsets by rank, the leaderboard, kyūjō withdrawals. All I wanted was for something to turn a struct into a sentence. Hand the model the numbers, ask for a paragraph of newspaper prose, cache it. An afternoon’s work.

It took twelve versions, and it ended with me deleting the model.

What a small model does with a fact you hand it

The failures were never stylistic. The prose was fine. The prose was, in fact, excellent — which is exactly the problem, because excellent prose is how a false claim gets past you.

A record of 12-3 came back described as perfect. A yūshō race with three wrestlers tied at the top came back as a sole leader — because the model, asked to write elegantly, found the tie inelegant. An outright win became a playoff. A wrap-up report invented an entire narrative for a tournament that hadn’t had one. Wrestlers acquired ordinals they’d never earned — “his third straight” — and adjectives of drama that no fact supported: historic, stunning, dominant. And at one point I found that the model had learned my own bug: I’d been computing the yūshō lead by win count rather than by record, and the model dutifully narrated the wrong leader with total confidence.

Each one got a patch. Version 2: a strict, example-led prompt, temperature 0.2. Version 3: vary the lead, right-size the wrap-up. Version 4: forbid “perfect” for a record with a loss in it. Version 5: make the facts tie-aware and champion-aware. Version 7: regenerate everything on a different model. Version 8: fix the lead calculation. Version 9: stop the wrap-up confabulation, cap the leader list when twenty people are tied on day one. Version 11: gate the invented qualifiers and ordinals.

Somewhere around version 9 I noticed what I was actually doing. Every patch was a prompt patch, and every prompt patch was a bet that the next hallucination would be one I had already imagined. I was writing a blocklist against an infinite space. I was going to lose, and worse, I was going to lose quietly — because the failure mode of this system is a confident, well-written sentence that is not true, published under my name, about a sport I’d told people I cared enough to build an app for.

Taking away its authority to know things

Version 10 is where I finally understood the actual bug. It was never the model’s writing. It was that I had made the model the source of the facts — I handed it raw numbers and asked it to both decide what mattered and phrase it, and the moment a model is allowed to decide what’s true, everything downstream of it is a hope.

So I shrank the job to the smallest one that still had a point. Deterministic code would compose the whole report — every name, record, technique, the complete story, publishable as-is. The model would receive that finished draft and be allowed to do exactly one thing: change the wording. Not report. Rephrase. And then I’d check its homework mechanically instead of trusting it, with a validator that diffs the rewrite against the draft and throws the rewrite away on any discrepancy.

And that is what finally told me the truth about the model I was running.

What the gate revealed

With the gate in place, the rejection rate became a measurement rather than a vibe. The local model was failing it roughly half the time — while doing nothing but rewriting a paragraph I had already handed it, correct and complete. Not reasoning. Not selecting. Not computing a standings table. Rewriting. It would still drop a wrestler, still round a record, still find a way to smuggle in a word that wasn’t in front of it.

Twelve versions of prompt-tuning had not moved that number to somewhere I’d trust. And the reason wasn’t the prompt — it was the hardware I’d decided to be proud of.

The box is a classic CPU VPS. The models with enough capacity to be reliable — 35b and up — are bound there by both CPU and RAM; they don’t run so much as crawl. The models that comfortably do fit, the 9–11b tier, aren’t smart enough to be trusted with the facts, and, as the gate proved, weren’t reliably smart enough to be trusted with a style rewrite of text that was already correct. That’s the whole local option on a box like mine: too slow to be usable, or too small to be trusted. There was no third door.

So in version 12 the model went in the bin: drop the model — deterministic drafts with phrasing variety. Facts in, sentences out, no network call, no temperature, no surprises. And the honest result: the deterministic version was better. Never wrong. Just flat — the same six shapes of sentence, forever, and you could feel the template through the paint. A dull report that is true beats a lively report that might not be, and if the story had ended there it would still have ended correctly.

Do I need a holy grail to drink tea?

Flat bothered me, though, so I costed the dream properly, and the dream is a GPU VPS: real hardware, real weights, my box, my model, nobody’s API. It runs $500+ a month.

For twenty prompts a month.

That’s the sentence that struck me, and it’s the one I keep coming back to. I could rent the GPU. I could bump the box to 16 or 32GB and keep experimenting. But renting my own AI machine is a holy grail — and do I need a holy grail to drink tea? Sumo App is a free side project whose newspaper desk files a handful of reports during a tournament. To justify that machine, and the whole local workflow around it, I’d need a different application entirely:

  • one that earns, and
  • one that is predominantly AI — heavy workflows, constant generation, the lot.

An app doing twenty generations a month is neither. Wanting it isn’t a business case. “I want the sovereign version” is a real feeling, and it is not, on its own, an argument.

So I went looking for the smallest thing that would actually work — a remote generation API that still kept me inside the EU and inside my own jurisdiction, because sovereignty was never about the hardware being in my hallway; it was about who can reach my data and under whose law. I found one in Scaleway: EU-resident, fair pricing, a free allowance to start.

Then I measured it, and the maths ended the argument.

A whole month of the Dohyō Times’ generations costs about four cents. Against $500 for the GPU box. And the punchline is that the cheap option is also the better one: the model I now call is gpt-oss-120b — more than ten times the parameters of anything I could have run locally, and, unlike the 9b that kept mangling a paragraph I’d already written for it, it clears the gate.

I didn’t give up sovereignty. I gave up the aesthetic of sovereignty, on a workload that never earned it.

What the model is allowed to do now

So the model came back, in the job I’d shrunk for it in version 10 — this time with something behind the wheel that can actually do it:

The draft is finished before the model ever sees it. Deterministic code composes a complete, fact-correct paragraph. Every name, every record, every technique, the whole story — done. It is publishable exactly as it stands, and on any day the gate says no, it is exactly what gets published.

The model is a sub-editor, not a reporter. It receives the finished draft and one instruction: change the wording, change nothing else. Add no name, no number, no technique, no playoff, no prize, no adjective of drama. Drop no fact. Don’t turn a shared lead into a sole lead. It isn’t asked what happened; it’s told what happened and asked to say it better.

And then it is checked, mechanically, rather than trusted. This is the part that matters — and the part I’d spent eleven versions avoiding, because writing a validator is less fun than writing a prompt. A validator diffs the rewrite against the draft:

  • Every digit in the rewrite must already appear in the draft. Spelled-out cardinals get normalised to digits first, so “twelve” can’t smuggle past a check that’s only looking for 12.
  • No finishing technique may appear that the draft didn’t name — which is precisely what catches an invented playoff, because an invented playoff needs an invented kimarite to end it.
  • Every capitalised, proper-noun-ish word must exist in the draft or on a small allowlist. If a wrestler walks into the story who wasn’t in it, the story is rejected.
  • And a belt-and-suspenders list of never-legitimate vocabulary, for the words that are always an invention when they weren’t in the draft.

If any check fails, the rewrite is thrown away and the draft is served unchanged. If the network call fails, the draft is served. If there’s no API key configured, the draft is served, and nothing about the feature even wakes up. The restyle path never throws, because there is nothing for it to throw about: its worst case is the thing we were going to publish anyway.

The output records which one you’re reading, too — provenance is either draft or scaleway/gpt-oss-120b. If I ever have to answer for a sentence, I can say where it came from.

The rule

A language model may be the source of a sentence. It may never be the source of a fact.

That’s the whole thing, and everything else follows from it. If the model is only rephrasing, then a hallucination is definitionally something in the output that isn’t in the input — which is not a philosophical problem, it’s a set difference. You can compute it. You can unit-test it. You can look at the failure and know it failed.

And that’s the test I now apply before building any AI feature: can I write the validator? Not “will the prompt hold” — prompts don’t hold, prompts are vibes with a temperature knob. Can I state, in code, what the model is forbidden from doing, and can I check it cheaply enough to check it every single time? If yes, the model can have the job. If no, I don’t have a feature, I have a demo that will embarrass me in front of someone who knows more about the subject than I do.

The corollary that falls out of it for free: this is why the model runs nowhere near the request path. The editorial is computed once, in a worker, and stored as a snapshot — so a gate failure costs a user nothing but a slightly plainer sentence, and no reader ever waits on a model. The AI in this app is a build step that occasionally improves the prose. That’s a boring sentence, and it should be.

The epilogue, one repo over

The receipt for all of this lives in the infrastructure repo: drop ollama — unused AI experiment, frees ~17GB on the box.

That was the fun part of the sovereign stack, and the part I was proudest of, and it’s gone — not because self-hosting a model is a bad idea, but because it was the wrong tool at this size, and I’d have kept paying for it in RAM and pride if the gate hadn’t put a number on how badly it was doing. Seventeen gigabytes of ambition, replaced by four cents a month and a few hundred lines that don’t trust anything.

The dream isn’t dead; it’s just waiting for a workload that deserves it. The day I’m running an app that earns, with AI in its bones and generation happening constantly, the GPU box stops being a holy grail and starts being a line item — and I’ll buy it that afternoon. Until then I’m not going to pretend that a want is a requirement.

Two lessons, then, and I think they’re the same lesson twice. Don’t let the model source a fact — put a validator between it and your users, because a prompt is a hope and a set difference is a check. And don’t let a want source an architecture — measure the workload before you buy the machine you already wanted to buy. The gate caught the model inventing things. The invoice caught me doing it.