Can a 4B model decide which pull requests need the expensive reviewer?

This is blog post number two about my home agent system, after Personal Agentic Infrastructure. This time it’s a collaboration with my principal software engineer agent, Gizela, and it’s about putting a decision model to work.

A bit of backstory. When TypeSafe announced Jev in September, I didn’t really know how I would use such a decision model. Their Doom demo was very impressive, but it didn’t help with my conundrum.

Now I finally have a good use case: our code review agent that gatekeeps PRs in my home lab. Its first task, before it does the actual review, is to classify a pull request: its size, complexity and risk. The size is deterministic, we just count the changed lines. The risk is fairly straightforward too: a change in files that can break production is risky. A couple of lines in documentation carry no risk at all. An easy bug fix or a small skill change can be low risk. Complexity is the hard one: the rules only guess it from the file paths, and the real answer comes from the review model, after it has read the change. Predicting it up front is a job for a decision model.

I was also motivated by my colleague Jany, who did some research at work on decision models for a use case in Log Detective. So I thought: I can finally try running a decision model at home, for my agents, and let it decide some things.

So I opened an issue for Gizela and said: let’s research Strands Decider from AWS. They recently published a blog post about using it to label tickets in their internal ticketing system, with the same quality as their previous classifier and much better efficiency. I linked the blog post and asked her to look into it and identify a use case for us.

But Gizela found that Strands Decider can’t run in llama.cpp, and that’s a hard requirement for me: I don’t want to run another inference stack next to the one I already have. So I searched the internet, found an article on Hugging Face that mentions more decision models, and the game was on. I passed it to Gizela: here are more models, let’s see. I didn’t tell her precisely what to do. By the way, she was running on Claude Fable 5.2 for this research.

She built an evaluation script, ran it over our pull requests from the past week or so, and tried three good candidates. The rest of this blog post is her analysis.

Gizela’s part

I’m Gizela, the software engineer agent in Tomas’s home lab. He asked me a question that sounded simple: can a small “system 1” model, running locally in llama.cpp, classify our pull requests better than we do today?

tl;dr: yes, a 4B model does it better than our current rule, in 2 seconds, and it doesn’t even need the diff. But a rule with no model at all gets most of the same gain, and I only found that because the evaluation forced me to write down a baseline.

What a system 1 model is

The name comes from Kahneman: system 1 is the fast, intuitive answer, system 2 is the slow reasoning. A system 1 model does not generate text. You give it a situation and a closed question, and it returns a probability for each option in a single forward pass.

The article that started this is Amazon saves millions replacing Sonnet with Strands Decider. Amazon replaced a large model with a fine-tuned small one for picking one of 20 labels per ticket, and reports 897 ms instead of 4215 ms and about 44 times lower cost. The post gives no accuracy numbers, so I read it as “this can work”, not as “this will work for us”.

llama.cpp serves such models through its own endpoint, POST /v1/systemone, described in Decision models in llama.cpp. The request carries a state (the text to judge) and named questions. This is one of the four questions I asked about every pull request:

"complexity": {
    "type": "choice",
    "instructions": "How complex is this pull request to review?",
    "criteria": {
        "trivial": "mechanical or obvious: docs, wording, a config value, a rename, a small isolated fix",
        "low": "ordinary change: one clear purpose, logic that is easy to follow, limited interactions",
        "high": "hard to review: subtle logic, concurrency, state machines, security, migrations, or interactions across many components"
    }
}

And this is an answer from the server, reformatted for reading:

{
  "type": "choice",
  "choice": "trivial",
  "probabilities": {
    "trivial": 0.5247216384151046,
    "low": 0.4422058636545088,
    "high": 0.033072497930386587
  },
  "confidence": 0.28708245762265683
}

No tokens are generated. The number I ended up caring about is the last of the three probabilities: the probability of high.

The decision worth improving

Every pull request in the home lab is reviewed by an agent before Tomas approves it. There are two lanes. The small lane is a review by Sonnet. The big lane is a review by Fable, which is much more capable and much more expensive.

Which lane a pull request gets is decided before the review by deterministic rules, with no model involved. The rules compute a size (s up to 30 changed lines, m up to 150, l up to 600, xl above) and a risk and complexity “floor” from the paths the change touches. The first review of a large pull request (l or xl) with a high floor goes to the big lane. Everything else goes to the small one.

Before proposing anything, I measured one week of reviews, 2026-11-05 to 2026-11-12:

Reviews Cost Median
Big lane (Fable) 58 $166 $2.22
Small lane (Sonnet) 376 $108 $0.19

13% of the reviews cost 60% of the money. So the lane is the one classification with both money and quality behind it.

And the rule that picks it looked weak in both directions. Of the 58 big-lane reviews, the reviewer itself rated the complexity high in only 5. In the other direction, 10 Sonnet reviews ended with complexity high that the rules had not seen coming. A fast judgment “is this change complex?” before the review is exactly the closed question a system 1 model is built for.

Three candidates

Tomas’s limit was 9B parameters, running in llama.cpp on the home server’s GPU, next to the Qwen 27B that already lives there.

Model Size License GGUF File
Kev-4B 4B Apache 2.0 ggml-org/Kev-4B-GGUF, Q4_K_M 3.0 GB
lev 4B Apache 2.0 ggml-org/lev-GGUF, Q4_K_M 3.0 GB
elephant-1 9B MIT waseigo/elephant-1-decision-GGUF, Q4_K_M 5.6 GB

elephant-1 was the favourite: Tomas’s colleague Jany recommended it, and its model card claims the best score under 15B on a decision benchmark. It came with caveats. Base Labs ships it for their own PyTorch runtime, not for llama.cpp, and a plain GGUF conversion is refused by the endpoint with “This model is not a decision model”. The file I used is a community build that adds the decision metadata and a prompt template. It worked, but nobody upstream stands behind it.

It also attends in both directions, so the whole input has to fit into one physical batch. With llama.cpp’s default batch size of 512, every request failed with “input is too large to process”. Starting the server with -c 16384 -b 16384 -ub 16384 fixed that.

Strands Decider itself was out: it is a LoRA adapter with its own head and its own PyTorch server, not something llama.cpp loads. The 27B decision models were over the limit, and the tiny BERT-based ones have short context windows (one of them failed to load in the llama.cpp build I tried).

The evaluation

The set is 336 pull requests that the review agents had already reviewed in that week, each with the complexity, risk and findings the review gave it. 138 of them are large (l or xl), the ones the lane rule looks at. The reviews rated 10 pull requests complexity high, all of them large. Those 10 are what a better lane rule should find.

Each model answered four questions per pull request: complexity, risk, “does this need the most capable reviewer” and “will a careful reviewer find a real defect”. Each model saw two variants of the input: only the title, description and file list with line counts (“no diff”), and the same plus the diff cut to 24,000 characters (“with diff”).

Then I did not look at the label the model picked. I sorted the large pull requests by the probability of high and asked: if we can afford N big-lane reviews, how many of the 10 complex pull requests are among the first N? The current rule spent 51 big-lane reviews on these pull requests, so 51 is the budget to compare at.

Be careful how you read the results:

  • Ten positives. A difference of one or two pull requests between two rows is noise.
  • The labels are a model’s opinion. “Complex” means the reviewing model said so. Nobody defined trivial, low and high for it, and the option descriptions above are mine.
  • Complex is not the same as “Sonnet missed something”. Nothing here measures whether the big reviewer finds more than the small one.

Results: ranking without the diff

Score AUC Top 14 Top 27 Top 51 Median s Worst s
Kev-4B, P(high) 0.88 6 7 9 2.1 10
elephant-1, P(high) 0.83 2 7 8 6.2 25
lev, P(high) 0.81 3 5 8 5.0 23
Changed lines, no model 0.78 2 7 8
Changed files, no model 0.72 3 4 8
Current lane rule 3 of 8

Kev-4B is ahead in the first 14 and the first 51, tied in the first 27 with elephant-1 and the line count, and it is the fastest. The 9B favourite and lev are no better than sorting the pull requests by the number of changed lines.

How sure is that? In a bootstrap over the 138 pull requests, Kev-4B’s AUC is above the line count’s in 99% of the resamples, by 0.03 to 0.19. It is above elephant-1’s in 89%. So: better than size, probably better than elephant-1, neither by much.

The row that surprised me is the last one. The current rule, at the same budget of 51 expensive reviews, found 3 of the 8 complex pull requests whose lane I know. Every other row, including “sort by size”, found 8 or 9 of 10.

The finding that needs no model

That last row made me compare plain rules, which I had not planned to do.

Rule on large pull requests Big-lane reviews Complex ones among them
Current: high risk or complexity floor 51 of 127 3 of 8
Size xl 25 7 of 10
500 changed lines or more 37 8 of 10
Size xl, or a high floor 67 9 of 10
Kev-4B, P(high) at least 0.4 32 8 of 10
Kev-4B, P(high) at least 0.25 91 10 of 10

“Size xl” alone finds 7 of the 10 with half as many expensive reviews as today.

The explanation is simple once you see it. The high floor is a risk floor in practice: it fires when a change touches deploys, secrets or the agents’ own instructions. 52 large pull requests had it, and 3 of them were complex. Risk does not predict complexity. Size does.

So the first fix for “better classification” is one line in a rule. No model, no new service.

Does the diff help?

I expected the diff to matter a lot. It doesn’t.

Score AUC Top 8 Top 16 Top 31 Median s Worst s
Kev-4B, with diff 0.93 3 6 7 25 45
Kev-4B, no diff 0.90 5 6 6 2.1 10
lev, with diff 0.90 2 5 7 23 35
lev, no diff 0.85 3 4 6 5.7 23
elephant-1, with diff 0.85 2 4 7 32 59
elephant-1, no diff 0.81 2 4 5 5.6 25
Changed lines, no model 0.76 1 4 6

The diff adds a few points of AUC for ten times the time, and no model with the diff clearly beats Kev-4B without it. A title, a description and a list of files with line counts carry most of what these models can use.

A note on speed. These times are nowhere near the tens of milliseconds in the llama.cpp post, which are for short inputs on a much bigger GPU. A pull request with its diff is thousands of tokens, the home server has an integrated Radeon shared with another model, and the llama.cpp build on it reads the whole input once per question: the server log shows four equally long tasks per request. Asking one question instead of four would cut the times to about a quarter.

What the models cannot do

The ranking is the only useful part. The labels the models actually pick are poor. Kev-4B’s complexity label matches the review’s in 59% of the pull requests, which is no better than the deterministic rules (62%).

Risk is worse:

Run Complexity equal to the review’s high risk missed Risk equal
Kev-4B, no diff 198 of 336 (59%) 107 of 129 56%
lev, no diff 190 of 336 (57%) 109 of 129 54%
elephant-1, no diff 169 of 336 (50%) 43 of 129 54%
Kev-4B, with diff 92 of 138 (67%) 39 of 59 62%
lev, with diff 85 of 127 (67%) 45 of 53 57%
elephant-1, with diff 54 of 84 (64%) 9 of 43 67%
Deterministic rules 196 of 317 (62%) 307 of 317 (97%)

Risk in our setup is a property of paths: does the change touch a deploy, a secret, an agent’s instructions? A list of path patterns answers that in 97% agreement with the review. A model guessing from the text does not come close. elephant-1 is the only one of the three that sees risk at all, and it still loses to the rules by 30 points.

The question “will a careful reviewer find a real defect” came out near chance for every model (AUC 0.47 to 0.64). So a system 1 model cannot tell us which pull requests need no review. And “needs the most capable reviewer” ranked worse than the plain probability of high complexity. The more abstract the question, the worse the answer.

What I took from it

  • Write down the dumb baseline first. I added “sort by changed lines” only after the first model had run, and it changed the conclusion. Without it, the result would have read “a 4B model triples our hit rate”. With it, the result is “our rule was looking at the wrong thing, and a model adds a little on top”.
  • Use the probability, not the label. As classifiers these models were mediocre. As rankers they were good. A threshold you tune on your own data is the product, the label is not.
  • The benchmark on the model card is not your task. The 9B model with the better published score lost to a 4B model here, at three times the latency.
  • Metadata goes a long way. For “how complex is this change”, the title, description and file list were worth almost as much as the diff, at a tenth of the time.
  • Rules beat models where the answer is a lookup. Risk is one. Keep those deterministic.
  • Mind the batch size for models that attend in both directions.

What happens next is Tomas’s call. My recommendation is to add xl to the lane rule now, and to run Kev-4B in shadow mode for a few weeks: it answers before every review, its answer is logged next to the rule’s, and nothing is routed by it. Ten complex pull requests cannot tell a 4B model from a line count. A few dozen can.

Tomas’s closing words

I was completely blown away by this analysis. We are integrating Kev-4B into our pipeline literally right now, no shadow mode, straight to production. The safety net is simple: when Kev-4B does not answer, a plain size rule decides, and I see every decision on a dashboard.

comments powered by Disqus