Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Judge methods

Every judge method this build registers — what each decides, what it costs, how it fails, and whether it composes from a config file.

ReferenceLast updated

On this page

A judging setup asks a model one question about one of your responses, and which question it asks settles everything after it: the shape of the answer that comes back, how many provider calls it took to get there, and what happens when the reply cannot be read. That question has a name in the config file, and the set of names is closed at five.

Two of the five compose from a config file. The other three are turned away at tokentriage validate and again at boot, by a message that names the leaf which stopped them. So the first thing to look up about a method is not what it decides but whether a configuration can reach it at all.

Every entry below carries the same eight parts: what it decides, a block that was loaded by the binary, the mechanism, the knobs with their absent-readings, the call multiplier, the named failure, the output that shows it running, and when to pick something else. The config surface as a whole is Configure a judge; this page is its method column.

The vocabulary

The method name is the first thing a judge block is checked on, and it is checked against a closed set: a value outside the five is turned away before anything else in the block is looked at, and the value you wrote is not echoed back into the message.

$ tokentriage validate --config typo.yaml
Error: configuration is invalid: config typo.yaml: evals.evaluators[1].judge.method: not a §2.1 judge method — the closed vocabulary is pointwise-bool|pointwise-rubric|pairwise|reference|panel (absent reads as pointwise-bool, the shipped grader). The value you wrote is deliberately not repeated back in this message

That is one copy of a doubled message, and the doubling is the binary’s: every warning and every error on this surface is printed twice, so a capture showing one copy has been edited. The command was run from the directory holding typo.yaml, which is why the path in the message is a bare filename; the evaluator index [1] is that file’s and yours will be your own. Exit status was 1. Booting a copy of the same file printed the same body under an Error: config <path>: prefix and also exited 1.

Token What it decides Judge calls per graded row Composes from a config file
pointwise-bool acceptable or not, for one response 1 yes, and an absent method: reads as this
panel acceptable or not, by majority vote of N seats N yes, at three or more seats from two or more model families
pointwise-rubric a 1..K rating, scaled to [0,1] 1 no
pairwise which of two responses is better, or a tie 2 no on evals.evaluators[].judge; the leaf that composes it is shadow.duplicate.pairwise
reference acceptable or not, against a supplied reference answer 1 no

Two things that table does not say. The multiplier is per graded row rather than per served request: what decides how many rows get graded is sample_rate, and the arithmetic joining the two is what judging costs. And panel has no fixed multiplier, because its multiplier is its seat count.

The two that compose

These are the shapes a config file can reach. Both grade one response at a time and both answer with a boolean, which is why a panel can be built out of the first one.

pointwise-bool

Decides whether one response is acceptable, from the instruction that produced it and the response itself.

evals:
  evaluators:
    - id: judge-task-completion
      judge:
        external: allow             # the air-gap gate, and the only value it takes
        method: pointwise-bool      # the absent-reading, written out
        model: gemini-2.5-flash
        api_key_env: JUDGE_API_KEY  # an env-var NAME, not the key itself
      sample_rate: 0.20
      max_usd_per_day: 20.0

How it decides. The judge is asked for one JSON object and a strict parse reads one key from it, {"score": <bool>}. The object is the span from the first { to the last }, so a reply wrapped in prose or a code fence still parses. A missing key, a null, a quoted "true" and a numeric 1 are each read as no verdict rather than as a grade. The boolean lands on the row as value: 1 or value: 0, and the row’s pass is derived from the metric’s own floor, which for task_completion is 1.0. The version stamped beside it is prompt_sha, a seven-character digest of the effective prompt, which is an identifier for a template rather than a commitment to one.

Knobs.

Key under evals.evaluators[].judge Absent-reading What it does
method pointwise-bool selects this method
model required the model id sent to the judge provider
api_key_env required the env-var name holding that provider’s key
endpoint_env the shipped Anthropic Messages endpoint the env-var name holding the URL the judge posts to
external required, the literal allow the air-gap gate
self_consistency_k 1 above 1 this leaf is turned away on the evals plane

What it costs. One provider call per graded row, the lowest multiplier this build offers. The call is made by the off-path pass after the client’s response has been written, so it adds nothing to the served request’s latency. Its dollars land on that evaluator’s own metacost line and count against max_usd_per_day.

How it fails. A transport error or a reply the parse cannot read returns no verdict and an error, and the grade is left unset rather than guessed. The runner then counts the row under evaluator_error, which is a reason and a count with no message field, so the text of the failure does not survive. A missing credential and a same-family judge land in that same bucket and are indistinguishable from outside it. A timeout or a shutdown is counted apart, under cancelled, on the grounds that “we stopped it” and “it broke” send you to different places.

How to tell it is working. The boot log names the composed method and the instrument before any traffic arrives:

{"time":"2026-08-24T13:17:09.9297915-07:00","level":"INFO","msg":"evals: judge evaluator wired (air-gap gate OPEN — this deployment PHONES OUT)","evaluator":"judge-task-completion","judge_method":"pointwise-bool","judge_instrument":"gemini-2.5-flash","judge_model":"gemini-2.5-flash","api_key_env":"JUDGE_API_KEY","sample_rate":0.2,"max_usd_per_day":20}

One line lifted out of a longer boot log on 2026-08-24, from a source build on Windows booting the shipped examples/evals/journey-2-validated-judge.yaml example with the entry above pasted over its judge entry and its two listeners moved to port 0. The timestamp and its local offset are that machine’s; everything else on the line is read from the config. On a graded row the same answer comes back as judge.method, which the writer takes from the scorer that produced the outcome rather than from the config text, so a deployment that configured one method and composed another cannot record its intent in place of its behaviour.

Reach for something else when one model’s opinion is not enough evidence, or when the grader shares a family with a tier you route to. A panel is the setup that composes for both, at its seat count in calls.

panel

Decides acceptable or not by strict majority of several models’ ballots, from the same instruction and response that every seat is shown.

evals:
  evaluators:
    - id: judge-task-completion
      judge:
        external: allow
        method: panel
        model: gemini-2.5-flash      # required on a panel, and no seat reads it
        api_key_env: JUDGE_API_KEY   # required, for the same reason
        panel:
          rule: majority             # the whole vocabulary, and the absent-reading
          members:
            - { model: gemini-2.5-flash, api_key_env: JUDGE_GEMINI_KEY }
            - { model: mistral-large-2,  api_key_env: JUDGE_MISTRAL_KEY }
            - { model: llama-3-70b,      api_key_env: JUDGE_LLAMA_KEY }
      sample_rate: 0.20
      max_usd_per_day: 20.0          # one cap for the whole fan-out

How it decides. Each seat grades the item with pointwise-bool and its boolean is rendered as one of two ballot tokens, pass or fail. Every seat votes or the invocation stops, so the ballot count is always the seat count. The tally decides when twice the leading bucket exceeds that count:

decided   ⟺   2 × top > n
margin    =   (top − runner-up) / n

With two tokens over n ballots the leading bucket holds at least half of them, so an odd panel always decides: three seats fall 2–1 or 3–0 and both clear the test. Only an even seat count can leave the two buckets level, and a level vote is an abstention carrying panel_split. Three seats is the floor the config layer applies and there is no ceiling, so four seats validates and boots, and four is the smallest shape that can produce that abstention.

Knobs.

Key Absent-reading What it does
judge.panel.members[] required with this method the seats: at least three, from at least two model families
judge.panel.rule majority the aggregation rule, closed at that one value
…members[].model required per seat the model in that seat; a repeated model is turned away
…members[].api_key_env required per seat that seat’s provider key, by env-var name
…members[].endpoint_env that seat’s shipped provider endpoint the URL that seat posts to, by env-var name
judge.model, judge.api_key_env required still required here, and read by no seat
judge.endpoint_env turned away on a panel the seats are the transports, so a top-level endpoint would be dead config

What it costs. N provider calls per graded row against the single max_usd_per_day on the evaluator entry, not one cap per seat. The cap is consulted before each seat, so a fan-out stopped part-way has already made and settled the calls that came before the stop. The validator states the multiplier before you boot, in a warning that names the seat count and the cap in one sentence.

How it fails. Fail-closed. A seat that cannot be reached, a seat that panics, a seat answering with a non-boolean verdict, a budget stop part-way through the fan-out, and a vote vector that does not line up positionally with the members each stop the whole invocation rather than that seat alone. None of them writes a row, and all of them arrive at the runner as evaluator_error, because the two finer tallies that would separate them, judgecore.Panel.Counts and PanelScorer.Counts, have no call site outside their tests.

A level vote is different in kind: it is an answer about the item, it is counted under abstained, and the whole vote vector travels out with it.

How to tell it is working. The boot line carries a composite instrument id and the seat count:

{"time":"2026-08-24T13:17:22.6485871-07:00","level":"INFO","msg":"evals: judge evaluator wired (air-gap gate OPEN — this deployment PHONES OUT)","evaluator":"judge-task-completion","judge_method":"panel","judge_instrument":"panel(majority:gemini-2.5-flash,mistral-large-2,llama-3-70b)","configured_judge_model":"gemini-2.5-flash","api_key_env":"JUDGE_API_KEY","sample_rate":0.2,"max_usd_per_day":20,"panel_members":3}

A second boot of the same example under the same conditions, carrying the entry in this block. A single-model judge prints judge_model where this one prints configured_judge_model, which is the boot log saying that no seat used the top-level value. On a graded row the ballots come back beside the verdict:

"panel": {
  "members": [
    { "model": "claude-haiku-4-5", "family": "claude" },
    { "model": "gpt-4o-mini",      "family": "gpt" },
    { "model": "gemini-2.5-flash", "family": "gemini" }
  ],
  "votes": ["pass", "fail", "pass"],
  "rule": "majority",
  "margin": 0.3333333333333333
}

That is the panel object out of the tracked golden fixture the product pins its own wire shape against, with whitespace added and no value changed; go test ./internal/declog/ -run TestJudgeRowGolden compares the whole file byte for byte and passes on this tree. It is a shape pin rather than a live capture, and its three seats are the fixture’s. votes is positionally aligned with members and margin is derivable from it, so two pass against one fail over three seats prints (2 − 1) / 3.

Reach for something else when the seat count multiplies a bill you cannot carry, or when one grader is the whole measurement you need. pointwise-bool is the same question asked once.

The three this build turns away

All three are registered methods with working implementations, and none of them composes over the evals.evaluators[].judge transport, which is a provider Messages API. The composition check reads strings and builds nothing: no client, no credential read, no connection. It produces one sentence per method, and the sentence is the same one with the token substituted.

$ tokentriage validate --config m-pointwise-rubric.yaml
Error: configuration is invalid: evals.evaluators[1].judge.method: "pointwise-rubric" is a registered §2.1 method that this build cannot compose over a `judge:` (provider API) transport — internal/evaluator ships no Scorer that drives it there, and the composition site will NOT quietly grade pointwise-bool instead. Over the §2.4 wire the same three methods are served by evaluator.NewSidecarJudgeScorer, which no composition site builds from config yet. Configure pointwise-bool, or configure a panel
Error: configuration is invalid: evals.evaluators[1].judge.method: "pointwise-rubric" is a registered §2.1 method that this build cannot compose over a `judge:` (provider API) transport — internal/evaluator ships no Scorer that drives it there, and the composition site will NOT quietly grade pointwise-bool instead. Over the §2.4 wire the same three methods are served by evaluator.NewSidecarJudgeScorer, which no composition site builds from config yet. Configure pointwise-bool, or configure a panel

Captured on 2026-08-24 from a source build, over a copy of the shipped journey-2-validated-judge.yaml example with one line changed. Both copies of the message are the binary’s. The same file through tokentriage run prints the same body under Error: evals: and also exits 1, and two consecutive runs of each were byte-identical. The §2.4 wire the message names is a self-hosted judging container, and every sidecar_judge: block that would configure one is turned away at both verbs as well.

pointwise-rubric

Would decide a 1..K rating against a named rubric, scaled to the unit interval, from the instruction, the response and the rubric’s anchor text.

evals:
  evaluators:
    - id: judge-task-completion
      judge:
        external: allow
        method: pointwise-rubric
        rubric: { id: task-completion-v1 }   # required with this method, and only with it
        model: gemini-2.5-flash
        api_key_env: JUDGE_API_KEY
      sample_rate: 0.20
      max_usd_per_day: 20.0

How it decides. The prompt asks for reasoning first and a verdict second, and the parser version records that requirement in its own name, scalar-v1;cot=required. The reply is {"score": <integer>} on the rubric’s scale, read from the same object span the boolean parser uses, and the score is rescaled to (score − 1) / (scale − 1). A score outside 1..K, a float, a string or a null is read as no verdict: an out-of-range value is turned away rather than clamped, because clamping a 0 would publish a broken reply as the worst legal grade. Both shipped rubrics carry five anchors, so the scale is 5 and a 3 scales to 0.5.

Knobs.

Key Absent-reading What it does
judge.rubric.id required with this method names the rubric; the shipped ids are task-completion-v1 and groundedness-v1
judge.rubric under any other method absent written under another method it is turned away, because nothing would consume it

What it costs. One call per item wherever the method runs. On the evals.evaluators[].judge transport it costs nothing at all, because the composition check happens before a client is built, so the process stops without a credential being read or a connection being opened.

How it fails. Turned away at tokentriage validate and again at boot, by the message above, and the composition site does not fall back to pointwise-bool behind it. The rubric id itself is checked for shape and for coherence with method:, and its membership is looked up on no path a config file reaches, so a rubric id nobody registered produces the method message above with the rubric not mentioned at all. The lookup that would list the registered ids sits inside this method’s own runtime path, which a config-file deployment does not get to.

How to tell it is working. What a config-file deployment sees is the message above with "pointwise-rubric" in it, on both verbs. Nothing else about this method reaches a boot log or a row.

Reach for something else when you wanted a graded scale rather than a boolean. This build has no config-reachable substitute for that: pointwise-bool answers the same question with one bit, and a panel answers it with a vote vector and a margin.

pairwise

Decides which of two responses to one instruction is better, or that they tie, and derives that answer from both presentation orderings rather than from one.

shadow:
  duplicate:
    enabled: true
    sample_rate: 0.05
    max_usd_per_day: 5.0     # absent or 0 turns the whole feature off, silently
    pairwise: true           # the both-orderings co-run

That block is the leaf that composes. Writing method: pairwise under evals.evaluators[].judge instead gets the message above with "pairwise" in it.

How it decides. Two calls inside one grading call, with the pair presented once each way. Each reply is {"winner": "first"|"second"|"tie"} under parser version abtie-v1, in prompt coordinates: the tokens name slots rather than answers, and only the executor resolves a slot into a side, because it is the half that knows which side sat where.

Both answers then sit in one frame, where side A is the response your router served. The same side both times gives that side the win, two ties counting as agreement; different sides give no winner at all, and the trial abstains with swap_inconsistent. The confidence feature is swap agreement, 1.0 or 0.0. A pair run in one ordering has no representation: the only constructor takes both winners, so a half-run pair cannot be built.

Knobs.

Key under shadow.duplicate Absent-reading What it does
pairwise false co-runs this method beside the two pointwise grades on every graded duplicate
enabled false the duplication feature as a whole
sample_rate 0 the share of requests duplicated; 0 composes no sampler
max_usd_per_day none, and absent or 0 composes no sampler the cap over the duplicate and every judge call under it
self_consistency_k 1 k samples of each pointwise grade; the pairwise co-run is not k-wrapped
judge absent closed at the single value oracle

What it costs. Two judge calls per pair, on top of the two pointwise grades that plane already makes, and on top of the duplicate request itself. The validator does that arithmetic for the config you wrote and prints the total, “the grader makes 4 Anthropic Messages call(s) per sampled request under this config (2 pointwise grades × self_consistency_k, plus 2 for the pairwise co-run when shadow.duplicate.pairwise is on)”. A failure on the first ordering ends the trial before the second call is spent.

This plane also carries a cost the other one does not: it dials a compiled-in provider URL that no shadow.duplicate.* key moves, so it cannot be rehearsed against a listener on your own box, and the first pair you judge is billable.

How it fails. A transport or parse failure on either ordering writes nothing, not a winner and not an abstention, and is counted. A budget stop writes nothing either, on the grounds that what was stopped is what was not measured. A pair with an empty arm is skipped and counted, because a comparison against an absent answer is not a comparison. The flip itself is not a failure: it is the abstention above. The trials this method records are the swap set a validation run reads, and the share of them that flipped is the rate its ceiling applies to.

How to tell it is working. The boot log says the co-run composed:

{"time":"2026-08-24T13:17:34.7250048-07:00","level":"INFO","msg":"shadow duplication enabled","sample_rate":0.05,"max_usd_per_day":5,"judge_model":"","judge_key_env":"ANTHROPIC_API_KEY","pairwise":true,"self_consistency_k":1}

A third boot of the same example under the same conditions, with the block in this entry appended to it. The empty judge_model is that config’s: this plane takes its model and key from classifier.oracle.*, and the key env falls back to ANTHROPIC_API_KEY. Delete the max_usd_per_day line and the same file still validates at exit 0 and still boots, and its boot output then holds zero lines matching shadow against the three the capped one prints. That is the thing to check first. On a graded row the trial comes back as its own block, carrying both orderings and the derived agreement:

"pairwise": {
  "prompt_sha": "e5f6a7b",
  "winner_ab": "A",
  "winner_ba": "A",
  "agree": true
}

Those four keys are quoted out of the same tracked golden as the panel block above. That golden sets every field it can, so it is a field-by-field shape pin and not a row any run produced: read the keys, not the combination. prompt_sha here is the trial’s own version rather than the pointwise grades’, which answer to a different one.

Reach for something else when the deployment has no outbound path, or when the question is whether one answer is acceptable rather than which of two is better. Position bias, self-preference and panels is what the two orderings buy and where the method stops helping.

reference

Would decide whether one response is acceptable against a reference answer supplied alongside it.

evals:
  evaluators:
    - id: judge-task-completion
      judge:
        external: allow
        method: reference
        model: gemini-2.5-flash
        api_key_env: JUDGE_API_KEY
      sample_rate: 0.20
      max_usd_per_day: 20.0

How it decides. The same strict boolean parse pointwise-bool uses, bool-v1, over a prompt that renders a reference block around the reference text. An item whose reference is empty or blank is turned away before a call is spent, because rendering without that block would send pointwise-bool’s own prompt under this method’s version string, and the verdict would then be described by a version that did not produce it.

Knobs.

Key Absent-reading What it does
judge.method pointwise-bool reference selects this method, which takes no leaf of its own

What it costs. One call per item wherever the method runs, and nothing on a config-file deployment, for the same reason as pointwise-rubric: the process stops before a client exists.

How it fails. Turned away at tokentriage validate and again at boot, by the message above with "reference" in it. A second gap sits underneath that one and is the more permanent of the two: outside judgecore and its own tests, nothing in this build sets the reference field on a judged item, so the input this method requires has no producer. The one component that would carry a reference onto a wire is the self-hosted judging container, and every config leaf that would configure one is turned away too.

How to tell it is working. The message above with "reference" in it, on both verbs. Nothing about this method reaches a boot log or a row from a config file.

Reach for something else when you have a known-good answer to compare against. This build’s config-reachable graders read the instruction and the response only, so a reference answer belongs in the instruction text or in a human label, and the label route is Collect the labels a judge is measured against.

What each method publishes as a confidence feature

A judged verdict can carry one scalar beside it, and that scalar is the input a risk-controlled accept region would threshold. Which methods publish one is a closed table, mirrored into the package holding the quality plane’s descriptors, and every cell in it is measured by a test that builds the registered method, runs it at one sample and at three, and reads whether a confidence value came back.

Method At self_consistency_k: 1 The feature’s name Above k: 1
pairwise yes swap agreement yes, voted per ordering, reporting the weaker ordering’s margin
panel yes panel vote margin no; this build mints no version above k: 1 for a panel, and the constructor stops one
pointwise-bool no none yes, the k-sample margin
pointwise-rubric yes rubric scalar margin no; a vote over a scalar is a mean rather than a ballot
reference no none yes, the k-sample margin

The k in that table is self_consistency_k, which is a wrapper rather than a sixth method: it asks the same method k times and takes a strict majority, and a vote with no strict majority is an error rather than an abstention, because the abstention vocabulary has no token for it. On evals.evaluators[].judge any value above 1 is turned away at both verbs, so the only k above 1 a config file composes is shadow.duplicate.self_consistency_k, on the plane that needs outbound traffic.

The feature is published whether or not anything reads it. The shipped sampler builds its judging plan with no accept region installed, which the product pins with a test of its own, so no abstention in this build comes from thresholding one of these numbers. When a judge can’t tell has the four abstention reasons and says which of them an operator can reach.

What this page does not cover

Where a method’s dollars turn up, and the cap that stops them. The per-method multiplier is above; the price per call, the meters that carry it and the daily cap’s post-hoc arithmetic are What judging costs, and how to cap it.

The rest of the config surface. The calibration, drift and validation blocks beside judge: are Evals and calibration configuration, and the pasteable walk through the judge block itself is Configure a judge.

Which of the two registered judge ids to configure. judge-groundedness publishes no pass floor, so the runner leaves every row it grades without a verdict and no gold set forms for it. Configure judge-task-completion, whose metric carries a floor of 1.0.

What a method’s verdict is worth once rows arrive. A judge outcome is reported until a validation run promotes its subject, and the walk that promotes a judge sets out how.