verdict

Small, fast, honest decision models. Open alternative to Jev: zero-shot, fit on your labels in seconds, calibrated with a coverage guarantee, Jev wire-compatible.

Stars
3
Language
Python
License
Apache-2.0
Updated
  • calibration
  • conformal-prediction
  • decision-model
  • guardrails
  • jev
  • laya
  • llm-routing
  • onnx
  • sentence-transformers
  • system-one
  • webgpu
  • zero-shot-classification
from verdict import Verdict

v = Verdict()                                                      # verdict-small: 118M params, 100+ languages, CPU is fine
v.choose("Billed twice, refund or we cancel", ["billing", "technical", "sales"])   # -> billing  p=0.71
v.score("Great product, slow delivery", scale=(1, 5))                             # -> 4  expected 3.6
v.check("Can I talk to a person?", claim="the user asks for a human")             # -> True p=0.84

Three question kinds, one mechanism. No text generation, no schema errors, no hallucinated labels: the answer is always one of the options you gave.

Why another one

TypeSafe's Jev (closed, hosted) and Laya (open, 421M ModernBERT) made "System One" decision models a category in September 2026. Both have the same three gaps, and Verdict is built around closing them:

JevLayakevVerdict
Zero-shot, Banking77 (77 intents)0.80-0.870.4250.425 (third-party)0.594 base, 0.556 trained
Trained on your labelsnot possible4-5 h on 2xT4delta fine-tune, GPU0.9 s on a laptop CPU (16 labels/class)
Accuracy after training, Banking770.80-0.87 (zero-shot only)0.425not reported0.858 with 16 labels/class, 0.913 full
Calibration (ECE, lower is better)0.144-0.2460.466 as shipped"still a bit overconfident" (author)0.014-0.030
"I don't know"opaque confidenceact_probability always 1.0threshold on confidenceconformal abstain with a coverage guarantee
Max options255~20 before degradingcollapses past ~20unbounded (options embedded once, cached)
LanguagesEnglish firstEnglish ckpt fails silently on non-LatinEnglish100+ (multilingual encoder)
Latency70-500 ms network33 ms GPU, 329 ms+ CPU12-26 ms H100, 780 ms M50.5 ms/example batched (M-series), single-digit ms CPU
Weights, data, training codeclosedweights onlyall openall open, Apache-2.0
Wire format/v1/systemone/v1/systemone/v1/systemone/v1/systemone compatible + native API + MCP
Option order changes the answeryes, log-odds shift 0.3-0.5yes, flip rate up to 0.23yes, 13.75 pp in a third-party testno, by construction

Jev, Laya and kev numbers are from their own docs and published third-party evals; see docs/RESEARCH.md. Verdict numbers are from bench/ and reproducible with one command below.

The honest-probability part

Every answer carries a Confidence:

a = d("the courier lost my parcel")
a.confidence.probability     # temperature-scaled, so 0.9 means right ~90% of the time
a.confidence.abstain         # True when the model cannot commit at your chosen coverage
a.confidence.conformal_set   # ["shipping"] or ["shipping", "refund"]: the labels it cannot rule out

calibrate() fits a temperature and a conformal threshold on held-out data. After that, on data like your held-out set, the true label is inside conformal_set at least coverage of the time. That is a distribution-free guarantee, not a heuristic. The number you actually care about comes out of evaluate():

Banking77, 16 labels per class, coverage 0.90:
  automation_rate 0.76   accuracy_when_committed 0.947   accuracy overall 0.858   ECE 0.020

Read: at 90% coverage Verdict commits on 76% of tickets, is right 94.7% of the time when it does, and hands the rest to a human or an LLM. The cascade is one object:

from verdict.cascade import Cascade

route = Cascade(d, fallback=lambda text, answer: llm_router(text, shortlist=answer.confidence.conformal_set))
route("where is my parcel")   # Verdict's label when it commits, the LLM's otherwise
route.stats()                 # calls, committed share, average ms on each path

Teach it your decision

from verdict import Verdict, Question
from verdict.types import options_from

v = Verdict()
d = v.compile(Question(kind="choose", options=options_from(["refund", "shipping", "account", "other"])))

d.fit(train_pairs)                       # [(text, label), ...]  seconds on a CPU
d.calibrate(held_out_pairs, coverage=0.9)
d.evaluate(test_pairs)                   # accuracy, ECE, automation_rate, accuracy_when_committed
d.save("support.verdict")                # one JSON file

fit trains a head on frozen embeddings: a prototype head from as little as one example per class, a logistic head (initialised at the zero-shot solution, shrunk towards it) when you have more. Nothing is fine-tuned, so it is fast, deterministic and cheap to redo.

Distill your LLM's decisions

Your router, judge or classifier has been logging (input, answer) for months. That is a training set.

verdict distill traces.jsonl support.verdict --coverage 0.9
traces 12,400 · fit 8,680 · calibrate 2,480 · test 1,240
agreement_with_llm 0.94 · agreement_when_committed 0.98 · automation_rate 0.81 · ece 0.02

Read: Verdict now answers 81% of that traffic itself, agreeing with the LLM 98% of the time on those, and escalates the rest. The question kind is inferred from the answers (labels, small integer range, or booleans) or given with --question q.json.

Drop-in for Jev / Laya clients

pip install "verdictml[serve]"
verdict serve --port 8000

Then point any Jev, Laya or impossibl client at http://localhost:8000/v1/systemone. The choice / score / noul question shapes are accepted unchanged; answers add an abstain flag. The native POST /v1/decide returns full Choice / Score / Check objects.

No PyTorch? Use the ONNX engine

pip install "verdictml[onnx]"
v = Verdict(engine="onnx")     # int8 weights from the Hub, ~120 MB, onnxruntime + tokenizers only

Same model, same numbers, 0.7 ms per example batched and under 2 ms single on a laptop CPU. These are the exact weights the browser playground runs.

Long inputs

Inputs longer than the encoder window are chunked with overlap, embedded, mean-pooled and re-normalised. A twenty-page document becomes one vector instead of a silently truncated one.

As an agent tool (MCP)

pip install "verdictml[mcp]"
verdict mcp
{"mcpServers": {"verdict": {"command": "verdict", "args": ["mcp"]}}}

Three tools, choose, score, check, for Claude Desktop, Claude Code, Cursor or any MCP client. Give an agent a fast, calibrated judgement call instead of another LLM round trip.

CLI

verdict ask "I want my money back" -o refund -o shipping -o account
verdict ask "Can I speak to someone?" --claim "the user asks for a human"
verdict fit question.json train.jsonl support.verdict --calibrate heldout.jsonl
verdict evaluate support.verdict test.jsonl
verdict bench banking77 --shots 0,16,-1

Benchmarks

Protocol: heads fit on train minus a fixed 1,000-row calibration slice; temperature and conformal threshold fit on that slice only; every number on the untouched test split. python -m bench.run all --shots 0,16,-1 reproduces the table.

suiteshots/classaccuracyECEautomation @0.9acc. when committedms/example
Banking77 (77 intents)00.5940.0540.100.9790.8
Banking77160.8600.0360.760.9600.6
Banking77all (8,993)0.9200.0320.980.9290.6
CLINC150 (150 intents)00.5360.0480.160.9570.5
CLINC150160.9280.0181.000.9280.5
CLINC150all (14,000)0.9530.0151.000.9530.5
MASSIVE en (60 intents)00.6120.1110.120.8980.5
MASSIVE en160.7890.0340.550.9560.5
MASSIVE enall (10,514)0.8810.0300.850.9330.5
MASSIVE Hindi (English option names)00.4940.0680.020.5
MASSIVE Hindi160.7420.0530.410.5
MASSIVE Hindiall0.8630.0430.690.5
SST-5 (score 1-5)00.2740.0970.9
SST-5160.3810.0320.9
SST-5all (7,544)0.4620.0291.2
ToxicChat (check, 7% positive)0AUROC 0.592.7
ToxicChat16AUROC 0.860.0160.772.8
ToxicChatall (9,082)AUROC 0.955, acc 0.9360.0112.3
Banking77, + deberta-v3-base-zeroshot-v2.0 reranker (top-8)00.6500.1030.220.950~30
SST-5, + mDeBERTa-v3-base-xnli reranker00.34998
ToxicChat, + deberta-v3-base-zeroshot-v2.0 reranker0AUROC 0.69127
ToxicChat, + mDeBERTa-v3-base-xnli reranker0AUROC 0.4659
verdict-small (trained encoder), Banking77, full test 3,07600.5560.0300.070.9850.6
verdict-small, Banking77160.8420.0200.750.9440.6
verdict-small, SST-5, full test 2,21000.4030.0240.9
verdict-small, ToxicChat, full test 5,0830AUROC 0.892, acc 0.8940.0152.5
verdict-small, ToxicChat16AUROC 0.9390.0140.932.5
Banking77, ONNX int8, CPU only00.6040.0620.100.7 idle*
Banking77, ONNX int8, CPU only160.8580.0300.740.7 idle*

typed-decisions (the benchmark Laya and Jev are compared on: 4 workflows, JSON states, 2,000 test decisions labelled by Jev; random 0.318, majority 0.461):

modelprotocolaccuracy
Jev 1.13zero-shot0.727
Laya basezero-shot0.362
Laya typed-decisionsfine-tuned on the train split0.766
Verdict e5-smallzero-shot, raw JSON state0.323
Verdict step-1,000 checkpointzero-shot, raw JSON state0.386
Verdict e5-small + headsfitted on the train split, raw JSON state0.625
Verdict e5-small, full fine-tune with soft targets, 3 epochsLaya's protocol0.689 (0.693 through the Verdict path, 52% automation at 0.81 accuracy when committed)
Verdict e5-base (278M), full fine-tune, 3 epochsLaya's protocol0.706

On jevbench's public tiers (mostly knowledge and multi-hop items) verdict-small scores 0.94 / 0.49 / 0.40 on easy / original / hard: routing and intent items are fine, reasoning items are not. Full table in docs/BENCHMARKS.md.

We are behind here and we know why. These states are JSON records, and the label depends on reading fields together with the options ("constraint_violations: 0" plus "harmful"). A bi-encoder scores each option against one vector of the state, by design: that is what buys unbounded options, cached inputs and the browser build. A listwise cross-encoder variant for structured states is the v2 item on the roadmap. train/finetune_typed.py reproduces both rows.

Reference points from published evals: Jev zero-shot Banking77 0.80-0.87, CLINC150 0.87; Laya Banking77 0.425, SST-5 0.372, MASSIVE non-English mean 0.451, ToxicChat 0.755.

verdict-small is multilingual-e5-small trained for 2,000 steps on the typed-decision mix in train/ (Banking77, SST-5 and ToxicChat never in the mix). Load it with Verdict(model="verdict-small"). Zero-shot check went from AUROC 0.59 to 0.89 and score from 0.27 to 0.40; intent zero-shot dipped from 0.594 to 0.556, which the next run addresses with a lower learning rate.

Where we lose, in plain words: zero-shot on score and check questions is weak with the default bi-encoder (SST-5 0.27, ToxicChat AUROC 0.59). A cross-encoder reranker helps (Verdict(reranker=...): Banking77 zero-shot 0.594 to 0.650, at ~30 ms instead of 0.6 ms); the trained-encoder track in docs/PLAN.md is the real answer. Sixteen labels per class fixes all of it, and the full ToxicChat train set reaches AUROC 0.955.

* ONNX rows were measured while a training run occupied the machine (16 ms/example then); 0.7 ms/example batched on an idle M5 CPU.

* zero-shot probabilities are uncalibrated by definition; calibrate() fixes ECE without changing accuracy. Apple M5, intfloat/multilingual-e5-small, batched.

We publish the rows we lose too. Zero-shot on fine-grained intent sets is where a bigger cross-encoder still wins; that is the trained-encoder track in docs/PLAN.md.

How it works

input ──► bi-encoder (options embedded once, cached) ──► logits
                  └─ optional cross-encoder rerank of top-k ─┘
          head: none | prototype | logistic   ◄── fit(examples)
          temperature scaling                 ◄── calibrate(held_out)
          conformal set (LAC)  ──► commit or abstain

Options are scored independently, so option order cannot change an answer (Jev shifts log-odds by 0.3-0.5 when you reorder) and P(true) + P(false) is exactly 1 (Jev's ranges 0.71-1.42). Both are tests in tests/test_invariance.py.

The default encoder is intfloat/multilingual-e5-small (118M, MIT). Swap any sentence-transformers model with Verdict(model=...). Add an NLI cross-encoder reranker with Verdict(reranker="MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7") for check questions and subtle option sets.

Install

pip install verdictml                  # import verdict
pip install "verdictml[serve,onnx,mcp]"  # + server, ONNX engine, MCP
pip install "verdictml[bench]"         # + datasets for the benchmark suite

Roadmap

  1. ONNX int8 export and a browser playground (ORT Web).
  2. TypeScript SDK with literal-union return types from the options array.
  3. Trained multilingual encoder on an open, commercially clean typed-decision mix (open data list, open script, open weights).
  4. Long inputs: chunk + max-pool.

License

Apache-2.0.