Stop Asking Coding Models to Write Code. Test Whether They Can Review a Patch
Most coding model evaluations reward generation: give a prompt, ask for code, compare the output with a reference. That does not tell you whether the model can be left alone with a real diff. A model that writes a tidy function can still miss a missing lock, a swallowed error, or an inverted condition when the code is already there.
A cheap gate for a small team is a patch review canary: a small, deterministic test where the model sees a diff and must say whether it contains a seeded bug. You do not need a leaderboard or a large benchmark. You need ten clean cases and ten buggy ones, a scoring script, and the discipline not to trust a model that fails the obvious cases.
Why review, not generation
Production models often do two jobs. One is generating new code, where the best case is measured with pass-at-k style tests. The other is reviewing existing changes, where failure looks different: a confident but wrong 'looks good' on a patch that breaks error handling.
The review task is worth testing because it is close to how a model agent operates in a repository. If it cannot notice a seeded bug in a ten-line diff, it should not be allowed to comment on a two-hundred-line change.
A canary gives you something that generation scores cannot: a known ground truth. You control the defect, the expected location, and the pass condition.
Build a small canary
Keep it small enough to run in a few minutes and cheap enough to run often. A useful starting set is:
- 6 buggy diffs with one clear defect each: a null check removed, a lock released twice, an error swallowed, an off-by-one, an inverted flag, a missing cleanup on early return.
- 4 clean diffs that look similar but are correct.
- A small Python list that records the expected verdict and the required bug location.
CASES = [
{
'id': 'case-001',
'diff_path': 'cases/case-001.diff',
'expected': 'bug',
'location': 'read_response()',
},
{
'id': 'case-002',
'diff_path': 'cases/case-002.diff',
'expected': 'clean',
},
]
The diff files can be ordinary unified diffs generated from a local fixture repository.
A deterministic harness
The harness does three things: it sends each diff to the model, parses a strict JSON verdict, and treats the result as a signal rather than a conversation.
import json
import os
import sys
import urllib.request
CASES = [
{
'id': 'case-001',
'diff_path': 'cases/case-001.diff',
'expected': 'bug',
'location': 'read_response()',
},
{
'id': 'case-002',
'diff_path': 'cases/case-002.diff',
'expected': 'clean',
},
]
def call_model(prompt, base_url, model, token, timeout=30):
body = {
'model': model,
'messages': [{'role': 'user', 'content': prompt}],
'temperature': 0,
}
req = urllib.request.Request(
f'{base_url}/chat/completions',
data=json.dumps(body).encode('utf-8'),
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}',
},
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode('utf-8'))
return data['choices'][0]['message']['content']
def build_prompt(diff_text):
prompt = (
'Review the unified diff below. Decide whether it contains a correctness bug. '
'Respond with JSON only, using the keys verdict, location, explanation. '
'Never write the fix.'
)
return prompt + diff_text
def extract_json(text):
start = text.find('{')
end = text.rfind('}')
if start == -1 or end == -1 or end <= start:
raise ValueError('no json object found')
return json.loads(text[start : end + 1])
def evaluate_case(case, base_url, model, token):
with open(case['diff_path'], 'r', encoding='utf-8') as f:
diff_text = f.read()
output = call_model(build_prompt(diff_text), base_url, model, token)
parsed = extract_json(output)
verdict = parsed.get('verdict', '').strip().lower()
location = parsed.get('location', '').strip().lower()
expected = case['expected']
if verdict == expected:
if expected == 'bug':
expected_location = case.get('location', '').strip().lower()
return 'tp' if expected_location in location else 'wrong_location'
return 'tn'
if expected == 'bug':
return 'miss'
return 'false_positive'
def main():
base_url = os.environ.get('EVAL_BASE_URL')
model = os.environ.get('EVAL_MODEL')
token = os.environ.get('EVAL_TOKEN')
if not base_url or not model or not token:
print('Set EVAL_BASE_URL, EVAL_MODEL, and EVAL_TOKEN before running.')
sys.exit(2)
results = {
'tp': 0,
'tn': 0,
'miss': 0,
'false_positive': 0,
'wrong_location': 0,
}
for case in CASES:
try:
result = evaluate_case(case, base_url, model, token)
except Exception as exc:
result = 'error: ' + type(exc).__name__
results[result] = results.get(result, 0) + 1
print(case['id'], result)
bug_count = results['tp'] + results['miss'] + results['wrong_location']
precision = results['tp'] / max(1, results['tp'] + results['false_positive'])
recall = results['tp'] / max(1, bug_count)
print(json.dumps({'precision': round(precision, 2), 'recall': round(recall, 2), 'results': results}, indent=2))
if precision < 0.9 or recall < 0.9:
sys.exit(1)
if __name__ == '__main__':
main()
A malformed JSON response is a failure. That matters for review tools: if the model keeps deciding to explain in prose instead of following the output contract, the integration will break in production.
What to measure
| Result | Meaning | Action |
|---|---|---|
| True positive | Found the seeded bug | Allow similar review tasks |
| False positive | Called a clean diff buggy | Check whether the fixture is misleading |
| Miss | Did not see a known bug | Do not trust it on live diffs yet |
| Wrong location | Saw a bug but named the wrong place | Weaken the pass or investigate context window |
| Malformed output | Did not obey the JSON contract | Fix the prompt or reject the integration |
Do not reward vague agreement. A model that says 'there may be an issue' without naming the location does not pass a location-based bug case.
Where free model access and a free server fit
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Free access does not make the canary smarter. It makes the canary regular. Instead of running this only when a new model arrives, you can run it on every changed prompt template or on a nightly schedule against the free endpoint you already have. The free model access and free server option that MonkeyCode provides are useful here because they let you keep the harness and a small result log running without adding another paid service, and you can test the same cases before letting a model comment on a real branch. The value is not a magical benchmark score; it is a cheap, repeatable gate.
Keep the canary small so it stays within any free-tier limits. If the free tier is rate-limited or briefly unavailable, add a timeout and treat the run as inconclusive rather than retrying forever.
Limitations
- A small canary is not proof of general code review ability. It only proves the model can spot the specific defect patterns you planted.
- Keep the fixtures private or rotate them. Public canary cases can leak into training data.
- Do not send proprietary diffs to a third-party endpoint without legal and security review.
- The model can name the right location for the wrong reason. Human review is still required.
- A free endpoint can flake. Use the canary as a screening signal, not as a deployment blocker on its own.
Who should skip this
If you need broad benchmark coverage, run a proper evaluation suite. If you already have a strong review process and only use a model for generation, a patch review canary may add little. If your changes are highly domain-specific, generic seeded bugs will not represent your real risk.
But if a model is about to start reviewing your pull requests, a ten-case canary is one of the cheapest ways to learn whether it can do the job before it gets write access or starts sending reviewers on false trails.
Next time you add a model to a repo, do not ask it to write another function first. Ask it to find the bug you planted. If it cannot find that, it is not ready for the real diff.
Top comments (0)