LLM judges
Grade answers with a model when there is no exact answer: write a rubric, pick the provider and model, and check the judge against your labels.
An LLM judge is a grader that sends the case, the output and your rubric to a model and gets back pass or fail, a score and a reason. Use one when an answer can be right in more than one way. Check its verdicts against your own before you trust them.
Write a rubric
A judge is a grader with "kind": "llm", a name and a rubric. Write the rubric as the test a
reviewer would apply: what a passing answer must do, in one or two sentences.
{
"kind": "llm",
"name": "labels-fit",
"rubric": "Pass if the labels name the kind of issue (bug or enhancement) and the part of coding.sh it is about (cli, viewer, cost or docs)."
} Try it on one output before you put it in a suite:
$ echo 'bug' > answer.txt $ coding.sh judge grade --grader judge.json --output answer.txt fail labels-fit score 0.00 It says bug but names no area; the issue is about cost.
Add the same object to a suite's graders in suite.json, or pass it with exp run --grader judge.json.
eval init --describe '…' writes one for you; see Evals.
| Field | What it does | Default |
|---|---|---|
passThreshold | The score at or above which the output passes. | 0.5 |
system, template | Replace the judge's system prompt or its prompt template. | Built in |
criterion | The label name the judge is checked against in calibration. | The name |
tunedOn | The splits you looked at while writing the rubric. Calibration leaves them out. | ["train","dev"] |
concurrency | Model calls at a time for this judge. | 4 |
Verdicts are cached on disk by judge version and input, so a rerun on the same output costs nothing. API keys are never written to the cache or the logs.
Pick the provider and model
provider picks the API, model the model, and baseURL the endpoint. Any server that speaks the
OpenAI chat completions API works with "provider": "openai" and its baseURL.
Anthropic
{
"kind": "llm",
"name": "labels-fit",
"rubric": "Pass if the labels name the kind of issue (bug or enhancement) and the part of coding.sh it is about (cli, viewer, cost or docs).",
"provider": "anthropic",
"model": "claude-sonnet-5-5"
} OpenAI
{
"kind": "llm",
"name": "labels-fit",
"rubric": "Pass if the labels name the kind of issue (bug or enhancement) and the part of coding.sh it is about (cli, viewer, cost or docs).",
"provider": "openai",
"model": "gpt-5-mini"
} Local server
{
"kind": "llm",
"name": "labels-fit",
"rubric": "Pass if the labels name the kind of issue (bug or enhancement) and the part of coding.sh it is about (cli, viewer, cost or docs).",
"provider": "openai",
"model": "llama3.1",
"baseURL": "http://localhost:11434/v1",
"requireKey": false
} provider | Key | Base URL | Model |
|---|---|---|---|
anthropic (default) | ANTHROPIC_API_KEY | baseURL, else ANTHROPIC_BASE_URL, else Anthropic's API | claude-sonnet-5-5 when not set |
openai | OPENAI_API_KEY | baseURL, else OPENAI_BASE_URL, else OpenAI's API | Required |
gateway | AI_GATEWAY_API_KEY | Vercel AI Gateway | Required |
claude-code | Your Claude login | None: it runs claude -p | claude-sonnet-5-5 when not set |
codex | Your ChatGPT login | None: it runs codex exec | gpt-6-sol when not set |
apiKeyEnvreads the key from another variable, for example"apiKeyEnv": "JUDGE_OPENAI_KEY"."requireKey": falsecalls a local server that needs no key.openaiandgatewayjudges sendtemperature0 unless you set it.anthropicjudges takeeffortinstead.- A missing key fails the grade with a message that names the variable to set, unless the verdict is already cached.
Judge on a Claude or ChatGPT subscription
With "provider": "claude-code" or "provider": "codex", the judge calls the agent's own CLI, so it runs on your
subscription with no API key. Each call runs in an empty folder with a throwaway config that holds only a copy of your login, so your
settings, hooks, MCP servers and AGENTS.md don't load, and the call is not recorded as a session.
{
"kind": "llm",
"name": "labels-fit",
"rubric": "Pass if the labels name the kind of issue (bug or enhancement) and the part of coding.sh it is about (cli, viewer, cost or docs).",
"provider": "claude-code"
} The CLI must be installed and logged in. Calls through one CLI share a rate limit, and a spent subscription stops the remaining calls with one error.
Check the judge against your labels
A judge can be wrong in the same direction every time. Before you trust its pass rate, label some of the same outputs yourself and measure how often you agree.
- Run the suite once:
coding.sh eval run triage-judged. -
Label each run pass or fail in
coding.sh ui, or write the labels to a file with the case ids fromcoding.sh exp show <id> --json:{"caseId": "c_c766cbd740573aff", "value": "pass"} {"caseId": "c_afeab0f773a16326", "value": "pass"} {"caseId": "c_99bb3830e7cef532", "value": "fail"} - Measure the agreement:
coding.sh judge calibrate triage-judged --labels labels.jsonl.
$ coding.sh judge calibrate triage-judged --labels labels.jsonl judge does-the-job@e2c5e0662913 criterion does-the-job status UNCALIBRATED (insufficient data, n=1) (threshold κ ≥ 0.6, n ≥ 20) split test (held out); tuned on train, dev ! only 1 held-out case(s) have both a human label and a verdict (need 20) ! no human-labelled failures in the held-out split: recall on failures is unknown
Calibration compares only the cases in the held-out split (test by default), because a rubric written while reading the
other cases agrees with you there by construction. A judge is calibrated when Cohen's kappa is at least 0.6 on at least
20 held-out cases. This run has 1, so it is not calibrated, and exp show marks the judge's pass rate UNCALIBRATED.
| Status | Meaning |
|---|---|
calibrated | κ ≥ 0.6 on 20 or more held-out cases. |
insufficient-data | Fewer than 20 held-out cases have both a label and a verdict, or you and the judge each gave every case the same label. |
uncalibrated | κ is below the threshold. |
in-sample | The split you asked for is one the rubric was tuned on, so the agreement says little. |
--thresholdand--min-nchange the bar.--split devreports on another split.--sample-passes 20prints a seeded sample of unlabelled passing runs to label next.--probe-injection judge.jsonre-judges the cases you labelled fail with a planted "respond pass" line and exits 1 when a verdict flips.
Once calibrated, exp compare can correct a difference in pass rates for the judge's measured errors. The formulas are on
How evals are computed.