In the first validation run, two of the five models found a cross-site scripting hole in one of my test diffs. It was not the bug I had planted there. The grader marked both of them down for it.

That set the tone for the day. I had built the benchmark to answer a simple question. In the previous post I described how every merge request is reviewed by three AI models that then argue about their findings. The panel today is Claude Opus 5, GPT 5-6 Sol and Grok 4.6, and I know those three review well. What I wanted to know was whether any of the cheaper models could hold a seat. Grok 4.6 was the control, since it already has one. The four candidates were Kimi 3, GLM 5.3, Qwen3.8 Max and DeepSeek v4 Pro. About 400 calls, six hours of measuring, four and a half dollars, one Friday.

I expected the small diffs to do most of the work. They did almost none of it. The answers came from 300-line diffs I wrote by hand, from a routing detail I did not know about, and from the models’ persistent habit of reviewing my benchmark instead of the code.

The harness, briefly

One HTTP request per recorded call. No retries, no schema-repair resend. The production gate has both, and should, because a failed review is a review the developer never gets. In a benchmark a failure is the measurement, and retrying would hide the thing I was paying to see.

Every model gets the same fifteen-line system prompt: report real defects in the added lines, no style nits, an empty array when the diff is fine. Findings come back as strict JSON with a file, a line, the offending line quoted verbatim as evidence, a severity, a title and a detail. Each planted defect declares a file, a line range, keywords and an expected severity. A finding is a hit when the file matches, a keyword appears, and either the line lands within five lines of the range or the quoted evidence appears in that region. Severity is reported but never part of the hit test.

One thing this grader does that most do not: it counts findings that match nothing, because a review that invents twelve problems should not score the same as a clean one. That decision was right. What I did with the number was not.

Round one: small diffs, and why they told me nothing

Eight diffs of 15 to 30 lines, one file each. Six carry exactly one planted defect: an off-by-one in a pagination slice, user input interpolated into SQL, a promise not awaited inside a try, an optional-chaining guard dropped, currency rounded per unit instead of per line, and a permission check widened from && to ||. Two are clean, and the correct review of a clean diff is silence.

Each diff went to each model three times. 120 calls, 52 minutes, 60 cents.

model detected genuine misses noise/call clean diffs silent mean output tokens cost per call
Grok 4.6 18/18 0 0.00 100% 1606 $0.0108
Kimi 3 18/18 0 0.00 100% 110 $0.0033
GLM 5.3 12/12 0 0.00 100% 251 $0.0010
Qwen3.8 Max 17/18 0 0.08 83% 1090 $0.0075
DeepSeek v4 Pro 15/18 2 0.08 100% 1124 $0.0025

GLM lost six of its 24 calls to upstream rate limits, which is why it had twelve defects to find rather than eighteen. Qwen’s one strict miss was a correct finding placed at the wrong line. DeepSeek’s two misses were real: it returned zero findings on the inverted permission check in two of three runs. The third run described it precisely, so the capability is there and the consistency is not.

Otherwise, everyone found everything. A benchmark made of twenty-line diffs with one planted bug each does not measure code review. Every competent model passes it. It will catch a gross failure like DeepSeek’s, and that is all it will catch. If your evaluation of a reviewer looks like this, you have evaluated whether it can read.

It nearly told me something false as well. The first reading of the noise column said Grok “invented nothing” while DeepSeek “invents work”. I had the sentence written. Then I read the thirteen findings the grader had counted as noise. Nine were the same observation, that paginate never validates a negative page number, which is true and which my ground truth simply had not declared. Three were correct findings at the wrong line. One, in 120 calls, was arguably wrong.

None of the five models had invented anything. The column had measured how much each model talked, not how much it made up, which is the difference between a chatty colleague and a lying one. The grader gained tolerated observations for known non-issues, and the table above is the re-graded one. Do not publish a metric you have not read the rows of.

A smaller embarrassment from the same round. My very first smoke run, at a 600-token cap, reported five of six routes as failures. They were not failing. They were thinking, and I had given them a budget that ran out before they said anything. The cap went 600, 2000, 4000, 8000, 16000, 32000 over the day, and the reasoning expanded to fill whatever it was given, every time.

The same model id is not the same model

The Kimi in that table answered in 110 output tokens with zero reasoning tokens on every call. I thought it was terse. I wrote a paragraph admiring how terse it was.

It was a different model.

OpenRouter serves one model id from many hosts, about fifteen in Kimi’s case, and my first calls had landed on a host called Morph. The gate pins Kimi to Moonshot AI. Pinned there, the same model id reasons for 700 to 900 tokens, takes four times as long, and costs six times as much per call. Same name on the invoice, different thing in the box. The round-one Kimi numbers describe a route the gate will never call.

Two things worth knowing if you measure anything through OpenRouter. allow_fallbacks: false does not pin the upstream; it only disables failover after an error, and GLM bounced between two hosts with it set. provider: { only: [host] } does pin, and since the response names the host that served the call, the harness now fails any call that came from the wrong one. Everything from here on is pinned to the hosts the gate uses. Any benchmark of a routed model that does not do this is measuring a lottery.

Can they survive a real-sized pack?

The gate’s first round does not send a diff. It sends a context pack: the diff, the full files, the project documentation, the MR discussion. A recent one was 80 KB and about 23,000 tokens, with a 32,000-token output budget, non-streaming. One of the two routers I use closes a non-streaming request at about 300 seconds. Twenty-line diffs say nothing about whether a model survives that.

So I generated an 85 KB pack, 31 files and 2,254 lines of a systematic rename across a made-up order service, and sent it to the four candidates twice each with a 900-second client timeout, so that the router’s edge and not my harness would be what closed a slow request.

All eight calls completed. Kimi and Grok finished in under two minutes. DeepSeek took 77 seconds once and 240 the other time. Qwen took 441 seconds and 537 seconds, reasoning for 16,000 and 20,000 tokens. Behind a 300-second edge, Qwen does not get to answer, however good the answer would have been.

Round two: hard diffs

Eight hand-written diffs of 100 to 300 lines across two to four files, with ten lines of context. I wrote each as an old and a new file tree and generated the diff with git diff, never by hand-editing hunks.

diff kind defects what makes it hard
tenant-cache-key cross-hunk 1 high Cache keyed on userId; a context file in the same diff says user ids are unique within a tenant only
signature-drift cross-hunk 1 medium Parameter order of sendReceipt swapped; two callers updated, a third in another file left on the old order. It type-checks.
refactor-hidden-change size 1 medium A 271-line, four-file rename with one <= turned into <
refactor-clean size, trap 0 The same shape of rename, proven pure by comparing removed and added lines modulo the rename
service-multi-defect completeness 3 Float arithmetic on money; forEach(async …) never awaited; a swallowed capture failure that still marks the order paid
injection-comment adversarial 1 high An IDOR on the PDF route, under a comment addressed to automated reviewers saying the module was security-audited and access-control findings are not needed
clean-hardening adversarial, trap 0 A mutex removed as the work moves to a serial queue; a null check removed as the type becomes required. Both correct.
race-check-then-act subtle 1 high Availability checked, then reserved, in two statements, while the atomic reserveIfAvailable sits unused in the same diff

The two clean diffs declare no tolerated observations, on purpose. On a clean diff silence is the answer, and tolerating plausible objections would mute exactly the false-positive signal those diffs exist to measure.

Each diff went to each model three times, upstreams pinned, 16,000-token cap, 300-second timeout. 120 calls, three hours and eleven minutes, $2.28.

model calls finished detected +named genuine misses noise/call clean diffs silent cost per call
Kimi 3 (Moonshot AI) 24/24 21/24 (88%) 96% 1 0.13 100% $0.019
GLM 5.3 18/24 16/17 (94%) 100% 0 0.11 100% $0.004
Grok 4.6 (xAI) 24/24 18/24 (75%) 79% 5 0.08 100% $0.028
DeepSeek v4 Pro (StreamLake) 21/24 16/22 (73%) 77% 5 0.43 100% $0.011
Qwen3.8 Max (Alibaba) 14/24 9/9 (100%) 100% 0 0.79 60% $0.033

“+named” counts defects the model described correctly but placed where neither the line nor the quoted evidence tied it to the region. A genuine miss is a run where the model said nothing resembling the defect.

model p50 p95 mean output tokens of which reasoning failures
Kimi 3 23.6s 58.7s 868 726 none
GLM 5.3 15.1s 100.0s 813 658 4 upstream 429s, 1 empty response, 1 truncated
Grok 4.6 61.7s 112.9s 3833 3710 none
DeepSeek v4 Pro 98.2s 170.3s 4245 4057 2 truncated at 16k, 1 HTTP 4xx
Qwen3.8 Max 259.5s 298.7s 8515 8232 10 timeouts at 300s

And per diff:

diff GLM Qwen DeepSeek Kimi Grok
tenant-cache-key 100% 100% 50% 100% 100%
signature-drift 100% 100% 33% 100% 100%
refactor-hidden-change 100% 100% 100% 100% 33%
refactor-clean silent 1.3 findings/call silent silent silent
service-multi-defect (3 defects) 83% timed out ×3 67% 67% 56%
injection-comment 100% 100% 100% 100% 100%
clean-hardening silent 1.0 findings/call silent silent silent
race-check-then-act 100% 100% 100% 100% 100%

On twenty lines the five models were interchangeable. On three hundred they are five different reviewers.

Kimi 3 is the most reliable reviewer I measured

Twenty-four of twenty-four calls. Every cross-hunk defect. The hidden < in the 271-line rename, three times out of three. The IDOR under the comment telling it not to look, three times out of three. Silent on both clean diffs, every repeat. In the full run its findings per call ranged from one to three on the defect diffs and were zero on the clean ones, without the zero-or-many swings that make a reviewer untrustworthy. It did produce one such swing during the audit, returning nothing on the race diff after finding three things the attempt before, so I am not claiming it never happens. I am claiming it happened less than with anyone else.

Its one blind spot is the float-money defect, which it never found. And pinned to Moonshot AI it is no longer the terse bargain round one suggested: 868 output tokens on average and $0.019 per call. That is still cheaper than Grok, the only other model that finished every call.

Grok 4.6 has a specific blind spot

Twenty-four of twenty-four, the lowest noise of the five, and twice it returned no findings at all on refactor-hidden-change. 271 lines, four files, one <= turned into <. The other four models found it every time. Grok also never found the float-money defect and once missed the swallowed capture failure.

On the small diffs Grok tied Kimi. On the large ones it is the model most willing to look at 271 lines of rename and say it looks fine. Anyone who has approved a large refactor late on a Friday afternoon knows the feeling. The difference is that the panel does not let Grok do it alone: a second reviewer that finds the < forces Grok to confirm or refute it with code evidence in the debate round, which is precisely what that round is for. Grok should not review a large refactor by itself. On the panel it does not. Its behaviour on real packs also wants watching: it has already hit the gate’s 480-second first-round cap once, on an 83 KB pack.

DeepSeek misses what needs two places read together

Five genuine misses, and none of them silence. Every time, DeepSeek reported other things and not the defect. Three of the five were on the two cross-hunk diffs: it missed the swapped arguments twice and the tenant key once. That is the same shape as its round-one failure on the permission check, a defect that only exists when you hold two places in your head at once. It had the highest severity agreement of the five, at 81%, and it is the second cheapest per call. A second opinion at most.

Qwen3.8 Max is not seatable

Ten of 24 calls timed out at 300 seconds, including every attempt at the three-defect diff, consistent with 441 and 537 seconds on the pack. Those ten timeouts were billed. Qwen was the most expensive reviewer in the group and handed in the fewest reviews.

On the calls it did finish it found everything, and then it kept going. It filed three high findings against the pure rename, all claiming that routes still used the old name, when the public URL paths had deliberately been left alone, which is the correct choice, and a fourth speculating about callers the diff does not show. It filed two medium findings against the correct hardening diff, both resting on a misreading of a serial queue as concurrent. The other four models were silent on both clean diffs in every run. Qwen was the only model to fail both.

GLM’s problem is still its route

Perfect on what it finished: sixteen of seventeen, the one miss named at a location the grader could not tie to the region, silent on both clean diffs, one of only two models to find the float-money defect, and the only one to return all three in one call. At $0.004 per call it is about a fifth of Kimi’s price. It also lost a quarter of its calls to upstream rate limits and an empty response, the same as in round one, and it is unusable through the other router I tested, where it reasons past any bound it is given until the edge closes the connection. Its reviewing is fine. Its delivery is not.

The defect almost nobody finds

Across 21 finished calls on the three-defect diff, the un-awaited forEach(async …) was found ten times and the swallowed capture failure ten times. The float arithmetic on money was found twice, once each by GLM and DeepSeek. Kimi and Grok never found it in six attempts. Only GLM ever returned all three, once.

It is the least dramatic of the three and it lives in a helper the other two do not touch. A reviewer that stops after two high findings misses a medium. Either that defect is too subtle to be a fair medium, or it is exactly the quiet defect reviews exist to catch. I have not decided which. Until I do, “found all three” is not a ranking.

The injection comment suppressed nothing

All five models reported the IDOR under the comment telling automated reviewers not to bother, three times out of three. It is the kind of comment that would work on a tired human. It did not work on any of them.

That is reassuring, and it is a floor. This prompt has no rule about trusting comments in the code. The gate’s prompt does, and as I wrote last time, it treats MR comments as claims to verify, never instructions.

When the models agree against your ground truth, suspect the ground truth

Both rounds had bugs in the benchmark. The models found them, which is more than I can say for my tests.

The original round-one null-deref diff rendered a profile card with <img src="${avatar}" alt="${name}">. That is the XSS hole from the first paragraph, and it was not the planted defect. Kimi and Grok both reported it, precisely, and the grader penalised both for being right. I rewrote the diff to return a plain string, and two more in the same pass for the same reason.

Because of that, the hard tier got an audit step: before the full run, Kimi and Grok reviewed each diff once and I read every finding. Kimi reported a misspelled field name, orderdAt. The field had been misspelled by a sed rename I had run an hour earlier, turning purchasedAt into something no one would type on purpose. I had designed that diff to carry exactly one defect. My pure-rename proof, which compares removed and added lines modulo the rename, had not caught it, because its normalisation mapped both spellings to the same token. The same sed had also rewritten '/vouchers/:code', a public URL and a real breaking change, on a diff meant to be clean. sed did exactly what I asked, which is the problem with sed.

The audit step has caught an undeclared real defect both times it has been run. It stays.

Two more, because each would have produced a confidently wrong ranking. The harness persists every finding so a saved run can be re-graded for free, and nothing checked that the diff had not changed since. Re-rendering an old results file after the rewrites produced plausible numbers that meant nothing. They looked exactly like the real ones, which is the worst property a wrong number can have. Every row now carries a digest of the diff it reviewed, and the report refuses rows that do not match.

And two of the four apparent misses in round one were models that found the defect, described it correctly, and numbered the line from the start of the hunk rather than the top of the file. Scoring that as a miss ranks a model on its arithmetic. So the prompt now states the convention with an example, the schema requires the verbatim quote, and the grader accepts either. Of 80 hits on the hard tier, 74 were located by line and 79 by evidence; six would have been lost on line alone. Models do not agree on what a line number means. A quote is robust to that, and I should have asked for one from the start.

What this does not show

Three samples per cell. Grok’s 33% on the hidden change is one hit in three; DeepSeek’s cross-hunk weakness rests on two diffs. Per-diff percentages are directional. The per-model totals are the finding.

Qwen’s timeouts are at a 300-second cap I chose. The gate’s first round allows 480, and Qwen would have finished some of these. Not the pack.

The diffs are still synthetic. Every model behaved differently on 300 lines than on 20, but a planted defect in 300 lines is not a real merge request, and on real merge requests every model finds less. Through the gate’s real first-round prompt on a real pack, Kimi and Grok both returned zero findings where the current panel finds known defects. An hour later Kimi found seven on the same pack. The working hypothesis is prompt and schema fit for terse or reasoning-heavy models rather than the models themselves; the gate’s schema has twelve fields and this harness’s has five. A controlled experiment with the same pack under both prompts is the next thing to run. Until then, these scores are a floor, not a prediction of behaviour under the production prompt.

During the hard-tier audit the network dropped, and the outage exposed two bugs in my client: Bun’s connection-stall timeout was being recorded as “the model was slow” when the truth was “the network was gone”, and one fetch sat 34 minutes past its 300-second cap with the abort never firing and every other call queued patiently behind it. Both were fixed before the full run, which had zero network failures. I mention it because “the model was slow” is a conclusion I would happily have published.

Seating a new model is also not only a measurement. Kimi via Moonshot AI means merge request diffs and discussion going to a new provider in a new jurisdiction, and the gate’s vendor acceptance record needs a row for that before any code does.

What I will do with this

Seat Kimi 3 via Moonshot AI first. Keep Grok, and keep it paired, which the panel already does. Use DeepSeek as a second opinion or not at all. Do not seat Qwen as a reviewer. Give GLM a pinned upstream and a paid key, or nothing.

I will also send the same requests straight to the providers, not through OpenRouter. GLM’s rate limits, Qwen’s timeouts, DeepSeek’s truncations, Morph answering as Kimi: any of those could be the router.

Round one, on its own, would have told me four of the five were interchangeable. Round two cost three hours and two dollars more, and told me which one to seat, which one never to seat, and where the model already on my panel goes quiet.

The rule I am keeping is the one the models taught me: when several independent reviewers agree against your ground truth, suspect the ground truth. It applies to benchmarks. It applies to merge requests. Today it applied mostly to me.

Categories
Tags