Scaling self-verification with DeepSeek V4 Flash beats Claude Fable 5 on Terminal-Bench 2.1, while being 11x cheaper

Reddit r/singularity Tools

Summary

LLM-as-a-verifier is a framework providing fine-grained feedback for AI agents, achieving state-of-the-art performance on benchmarks like Terminal-Bench 2.1 with DeepSeek V4 Flash, outperforming Claude Fable 5 at lower cost.

No content available
Original Article
View Cached Full Text

Cached at: 08/18/26, 04:39 PM

llm-as-a-verifier/llm-as-a-verifier

Source: https://github.com/llm-as-a-verifier/llm-as-a-verifier

LLM-as-a-Verifier

Any modality, Many Applications, One Unified Verification Framework

| Documentation | Website | Paper | Claude Code Plugin | Twitter/X | Slack |

πŸ”₯ LLM-as-a-Verifier achieves SOTA performance across agentic benchmarks, including Terminal-Bench, SWE-Bench Verified, MedAgentBench, RoboRewardBench and more. We invite the community to contribute more use cases!


Installation

pip install llm-verifier

To install the latest from a clone:

pip install -e .

What’s new in 0.2.0 (full notes in CHANGELOG.md):

  • Prefix-cache optimization: ~3.4Γ— fewer uncached input tokens on trajectory-heavy benchmarks
  • Terminal-Bench 2.1 self-verification benchmark
  • deepseek-v4-flash verifier backend
  • Token accounting (llm_verifier.token_usage())

About

LLM-as-a-Verifier is a general-purpose framework that provides fine-grained feedback for any agent. The key idea is simple: 1) use fine-grained scoring granularity, 2) take the expectation over the full logprob distribution of LLM score tokens, and 3) scale repeated evaluation and criteria decomposition. The resulting fine-grained feedback can be used for test-time scaling, progress tracking, and reinforcement learning.

LLM-as-a-Verifier overview


Quickstart

Simple Best-of-N Selection

Run a first end-to-end selection (requires DEEPSEEK_API_KEY or VERTEX_API_KEY in .env, or an OpenAI-compatible server that returns logprobs β€” e.g. vllm serve Qwen/Qwen3.5-9B with OPENAI_BASE_URL=http://localhost:8000/v1):

import llm_verifier

problem = "Write a function that reverses a string."
candidates = [
    "def rev(s): return s[::-1]", "def rev(s): return s", "def rev(s): return ''.join(sorted(s))",
]

result = llm_verifier.select(
    problem=problem,
    candidates=candidates,
    criteria={"Correctness": "Does the code actually reverse the string?"},
)
print(result.index)   # index of the best candidate: 0
print(result.scores)  # candidate scores: [0.73104, 0.38446, 0.38449]

Score a pair of candidates directly

select is built on a pairwise reward model. For the raw fine-grained rewards of a single comparison, call compare:

reward_a, reward_b = llm_verifier.compare(
    problem, candidates[0], candidates[1],
    criteria={"Overall": "Does the code solve the problem?"},
)
print(reward_a, reward_b)   # fine-grained rewards in [0, 1]: 0.99994 0

Fine-grained Progress Tracking

The same fine-grained reward can also score an agent’s progress after each step with track:

steps = [
    'Read the problem statement',
    'Wrote def rev(s): return s ',
    'Tested: rev("abc") returned "abc"',
    'Changed to def rev(s): return s[::-1]',
    'Tested: rev("abc") returned "cba"',
]

result = llm_verifier.track(problem=problem, steps=steps,
                            checkpoint_steps=[1, 2, 3, 4, 5], n_evaluations=4)
print(result.scores)  # progress after each step: [0.00106, 0.02417, 0.03143, 0.62004, 0.99978]

Self-Verification (Terminal Bench 2.1)

Can a model verify its own rollouts? On Terminal-Bench 2.1 we generate 5 mini-swe-agent trajectories per task with deepseek-v4-flash and use the same model as the verifier. Selection lands well above Pass@1 even though the verifier is judging its own model’s work:

ConfigPass@1LLM-as-a-VerifierOracle
Best-of-379.4%86.5% Β± 1.1%92.1%
Best-of-578.7%88.0% Β± 0.6%96.6%

The trajectories ship in data/terminal_bench_2.1_trajs/; scoring only needs DEEPSEEK_API_KEY in .env. Each configuration has its own reproduction script:

python scripts/run_bo3.py                    # best-of-3
python scripts/run_bo5.py                    # best-of-5

Test-Time Scaling for Agentic Benchmarks

Each benchmark ships with its agent trajectories (data/). We use Gemini 2.5 Flash (gemini-2.5-flash) as the verifier for all benchmarks below. Expected results:

BenchmarkBase ModelHarnessPass@1LLM-as-a-VerifierOracle
Terminal-Bench V2GPT-5.5 (Best-of-5)Capy83.1%86.5%92.1%
SWE-Bench VerifiedOpus 4.5 / Opus 4.6 / Gemini 3 Flash (Best-of-3)mini-swe-agent76.1%78.2%84.4%
MedAgentBenchClaude Opus 4.8 (Best-of-5)AgentBench70.2%73.3%75.0%

Reproduce Results

Run a benchmark by name (python scripts/run.py with no argument lists them):

python scripts/run.py terminal_bench
python scripts/run.py swe_bench
python scripts/run.py medagentbench

The tournament defaults can be overridden on the command line:

python scripts/run.py swe_bench --pivots 2 --n-evaluations 8 --seed 0 --max-workers 50

Benchmarks are defined in llm_verifier/benchmarks.py β€” add or tweak one there.

Select Best of N agent trajectories

Given a task and a pool of agent trajectories, pick the best one in a few lines of code.

import llm_verifier

problem = "Fix the failing test in utils.py."
candidates = [traj_1, traj_2, traj_3, traj_4, traj_5]

result = llm_verifier.select(
    problem=problem,
    candidates=candidates,
    criteria={"Root cause": "Did the agent fix the real cause?",
              "Verification": "Did the agent confirm the fix?"},
    model="gemini-2.5-flash",          # verifier model
    n_evaluations=4,                 # repeated evaluations per criterion
    pivots=2,                          # pivots < N; reduced verification cost
)

print("Best candidate:", result.index)            
print("Ranking:", result.ranking)                

Under the hood, select runs the Probabilistic Pivot Tournament to rank all N trajectories using O(Nk) pairwise verifications instead of a full O(NΒ²) round-robin. pivots trades cost for accuracy: more pivots = more comparisons = higher accuracy.

Adapt LLM-as-a-Verifier for your own use case

Use the verifier for your own task in three steps β€” Claude Code does the rest (generates the criteria, writes a runner, and selects the best-of-N for you):

  1. Add your data. Copy your agent trajectories into data/task_name_trajs/.
  2. Update naming. Replace every task_name in add_new_benchmark.md with the name of your task.
  3. Spin up Claude Code in this repo (or Codex, or whatever you like β€” with permissions disabled) and paste the contents of add_new_benchmark.md to let it run.

Progress Tracking for Coding Agents

The same fine-grained reward can score a trajectory at every step (see track in the Quickstart). Below, we track two Terminus-2 runs of the Terminal-Bench task pytorch-model-cli. The successful trajectory exhibits consistently increasing verifier scores, whereas the failed trajectory is characterized by erroneous behaviors, resulting in lower scores throughout the execution. Reproduce it with:

python scripts/terminal_bench_progress.py    # scores both runs then plots

Progress curves for two pytorch-model-cli runs

Online progress tracking

track scores a finished trajectory. To monitor an agent while it runs, use ProgressTracker: feed it each step as it happens and get a live progress score back β€” e.g. to stop a hopeless rollout early or decide when to resample. Since the verifier only ever sees the steps so far, it cannot peek at the future.

tracker = llm_verifier.ProgressTracker(problem, n_evaluations=4)

score = tracker.update('Read the problem statement')            # 0.00002
score = tracker.update('Wrote def rev(s): return s')            # 0.00013
score = tracker.update('Changed to def rev(s): return s[::-1]') # 0.73938
score = tracker.update('Tested: rev("abc") returned "cba"')     # 0.98604

if score < 0.05:      # after any step: abandon a hopeless rollout early
    ...

Replay the two Terminal-Bench trajectories step-by-step through ProgressTracker β€” printing a live score bar after every step, as an agent harness would see it:

python scripts/terminal_bench_progress.py --online

Multi-Modal Support

With a multimodal verifier model (e.g. Gemini 2.5 Flash or vllm serve Qwen/Qwen3.5-9B), every entry point accepts images β€” a single image (images="frame.png") or a list of images, each a local file path, an http(s) URL, or raw bytes:

result = llm_verifier.select(problem, candidates, criteria=criteria,
                             images=["before.png", "after.png"])

tracker = llm_verifier.ProgressTracker(problem)
score = tracker.update(step, images="camera_frame.png")  # per-step frame

Per-step frames stay part of the trajectory for all later updates, so the verifier always sees the full visual history β€” e.g. camera frames while tracking a robot rollout. See the multimodal documentation for accepted input forms, backend notes, and verified examples.


Claude Code Plugin

TurboAgent brings LLM-as-a-Verifier to Claude Code as a drop-in LLM API proxy. It sits between your client and the model provider, generating multiple candidate responses in parallel and selecting the best one with a Probabilistic Pivot Tournament.

pip install git+https://github.com/llm-as-a-verifier/TurboAgent

Point Claude Code at the proxy and run as usual:

turbo-agent                                        # starts on port 8888
ANTHROPIC_BASE_URL=http://localhost:8888 claude

It ships a built-in visualizer at http://localhost:8888/visualizer that shows the pipeline DAG, progress scores, candidate responses, and the final selection. See the TurboAgent repository for configuration and setup details.


Directory Structure

.
β”œβ”€β”€ scripts/                     # command-line entry points
β”‚   β”œβ”€β”€ run.py                   #   registry-driven benchmark launcher
β”‚   β”œβ”€β”€ run_bo3.py               #   reproduce the best-of-3 self-verification run
β”‚   β”œβ”€β”€ run_bo5.py               #   reproduce the best-of-5 self-verification run
β”‚   └── terminal_bench_progress.py  # re-score + plot the progress-tracking example
β”œβ”€β”€ criteria/                    # verifier criteria + ground-truth notes
β”‚   β”œβ”€β”€ TEMPLATE.md              #   copy this to write your own
β”‚   β”œβ”€β”€ terminal_bench.md
β”‚   β”œβ”€β”€ swe_bench.md
β”‚   └── medagentbench.md
β”œβ”€β”€ llm_verifier/                # the reusable framework (import llm_verifier)
β”‚   β”œβ”€β”€ __init__.py              #   llm_verifier.select(...) / .compare(...)
β”‚   β”œβ”€β”€ __main__.py              #   python -m llm_verifier <file.md>: preview criteria
β”‚   β”œβ”€β”€ benchmarks.py            #   BENCHMARKS registry (one Benchmark / launch)
β”‚   β”œβ”€β”€ fine_grained_reward.py   #   R(x,Ο„): logprob scoring + score cache
β”‚   β”œβ”€β”€ progress.py              #   llm_verifier.track(...): per-step progress curve
β”‚   β”œβ”€β”€ pivot_tournament.py      #   PPT: O(Nk) selection (Bradley-Terry)
β”‚   β”œβ”€β”€ prompts.py               #   load criteria/*.md + normalize criteria args
β”‚   └── loaders.py               #   per-benchmark trajectory loaders
└── data/                        # agent trajectories per benchmark

Runs write their verifier score caches to cache/ and result tables to results/; both are created on demand and git-ignored.


How it works

Fine-grained Reward Estimation

Rather than reducing each distribution into a single discrete score (as in LLM-as-a-Judge), LLM-as-a-Verifier approximates the reward of a trajectory \tau on task x as:

R(x, \tau) = \frac{1}{CK} \sum_{c=1}^{C} \sum_{k=1}^{K} \sum_{g=1}^{G} p_{\theta}(v_g \mid x, c, \tau)\,\phi(v_g)

  • C = number of evaluation criteria
  • K = number of repeated verifications
  • G = number of score tokens (granularity level)
  • p_{\theta}(v_g \mid x, c, \tau) = probability assigned by model \theta to score token v_g
  • \phi(v_g) = maps each scoring token to a scalar value
  • V_{\text{score}} = \{v_1, \ldots, v_G\} = ordered set of discrete score tokens

This lives in llm_verifier/fine_grained_reward.py.

Probabilistic Pivot Tournament

Probabilistic Pivot Tournament

To pick the best of N candidate trajectories, a round-robin tournament scores all \binom{N}{2} pairs β€” O(NΒ²). Probabilistic Pivot Tournament (PPT) is a cost efficient ranking algorithm in which every candidate is compared only against a small set of pivots, reducing the budget from \mathcal{O}(N^2) to \mathcal{O}(Nk).

  1. Candidates: the pool \{\tau_1,\dots,\tau_N\} to be ranked.
  2. Ring pass: a random Hamiltonian cycle scores the N adjacent pairs so every candidate appears once in the β€œA” slot and once in β€œB”, canceling the model’s positional bias.
  3. Pivot selection: candidates are ranked by their ring-pass scores w_{(i)}, and the top-k candidates form the pivot set \mathcal{P}.
  4. Pivot tournament: every non-pivot–vs–pivot and pivot–vs–pivot pair is scored via the pairwise preference p(a \succ b) = \sigma(R_a - R_b), concentrating the budget on uncertain top candidates and cutting cost from \mathcal{O}(N^2) to \mathcal{O}(Nk). Repeated evaluations of a pair alternate the A/B prompt slots, so positional bias cancels here as well.
  5. Selection: comparisons are aggregated into win mass w_i and count c_i, and the candidate with the highest normalized w_i/c_i is returned.

This lives in llm_verifier/pivot_tournament.py.


Prompt Templates

Pairwise Comparison Prompt

You are an expert [domain] reviewer. You will see a task description and two
trajectories.

Evaluation Criteria: [domain specific criteria]

Task: {task prompt}
Trajectory A: {A}
Trajectory B: {B}

Carefully analyze each trajectory, then provide your final scores:
<score_A> INTEGER_1_TO_20 </score_A>
<score_B> INTEGER_1_TO_20 </score_B>

Rating Rules: Rate correctness on a 1-20 scale based on evaluation criteria
(1 = incorrect, 10 = borderline, 20 = correct)

Progress Tracking Prompt

You are an evaluator of [domain] agent attempts. Trust observed output β€” NOT the agent's narration.

Task: {task prompt}
Agent trajectory ({N} steps): {trajectory}

You will score the trajectory at {N} checkpoints. Given everything the agent has done up to and including this step, would the agent's CURRENT state already complete the task?

Score each checkpoint INDEPENDENTLY, then output exactly N lines:
<c1> INTEGER_1_TO_20 </c1>
...
<cN> INTEGER_1_TO_20 </cN>

Rating Rules: Rate completion on a 1-20 scale (1 = certainly not complete,
10 = uncertain, 20 = verified complete)

Note: we use a letter-based scale (A-T) instead of digits in the actual implementation to enable logprob extraction for granularity scaling.


Prefix-Cache Optimization

Each verification prompt carries two full trajectories (~80k tokens on Terminal-Bench 2.1) and is re-scored per criterion and repeat, so on a backend that caches prompt prefixes almost all of that input can be reused. Two things make it happen: the prompt keeps the criterion at the tail, so everything before it (task, both trajectories, rating scale) is a shared prefix, and scoring warms one request per distinct prefix to completion before fanning out the rest. Together these take the cache hit rate from 5.2% to 78.4% on terminal_bench_2.1, cutting uncached input tokens by ~3.4Γ—.

Token Accounting

Every verifier call records what it was billed for, so the cache hit rate above is measured rather than assumed. scripts/run.py prints the totals under the result table (and writes them to results/<benchmark>.txt):

Verifier tokens (4,320 verifier calls)
  input                          272,551,552
    cached input                 214,712,320  (78.8% hit rate)
    uncached input                57,839,232
  output                          32,441,600
    reasoning                     26,102,144

Only calls this run actually made are counted β€” comparisons served from the score cache add nothing. Reasoning tokens are a subset of output tokens, and cached input is a subset of input. The counter is process-wide and thread-safe, so library users get the same numbers out of select / compare / track:

import llm_verifier

llm_verifier.USAGE.reset()
result = llm_verifier.select(problem, trajectories, criteria="terminal_bench")
print(llm_verifier.token_usage())
# {'calls': 24, 'input_tokens': 1512480, 'cached_input_tokens': 1190208,
#  'uncached_input_tokens': 322272, 'output_tokens': 180224,
#  'reasoning_tokens': 145408, 'cache_hit_rate': 0.787}

llm_verifier.USAGE is a TokenUsage: .snapshot() for the dict above, .reset() to zero it, and format_usage(...) for the report block. Counts come from the backend’s own usage block; a backend that reports no usage simply contributes zeros.

Citation

If you find this work useful, please cite:

@misc{kwok2026llmasaverifiergeneralpurposeverificationframework,
      title={LLM-as-a-Verifier: A General-Purpose Verification Framework}, 
      author={Jacky Kwok and Shulu Li and Pranav Atreya and Yuejiang Liu and Yixing Jiang and Chelsea Finn and Marco Pavone and Ion Stoica and Azalia Mirhoseini},
      year={2026},
      eprint={2607.05391},
      archivePrefix={arXiv},
      primaryClass={cs.AI},
      url={https://arxiv.org/abs/2607.05391}, 
}

Similar Articles

DeepSeek V4 Flash 0731

Hacker News Top

DeepSeek V4 Flash 0731 presents its results on the ARC-AGI benchmark, highlighting progress in abstract reasoning for AI models.

Chunjiang-Intelligence/DeepSeek-v4-Fable

Hugging Face Models Trending

DeepSeek-V4-Fable is a distilled variant of Claude-5-Fable built on DeepSeek-V4-Flash, designed for autonomous offensive security research, CTF problem solving, and controlled environment exploitation planning, with strict authorization requirements.