Docs / Gates
Gates
The gate is the mechanism that makes Firstpass proof-based instead of predictive. It reads the actual output and returns a verdict. Firstpass ships five kinds — from a zero-config inline check to your own test suite — and lets you compose them.
schema validator, a subprocess that runs any executable, a judge that uses a structurally different model as grader, and a consistency check that measures self-agreement across resamples. Each gate's cost lands on the receipt separately from the model spend.What a gate is#
on_abstain. Multiple gates on one rung form a conjunction — all must pass for the rung to serve.A gate is a check over a candidate output that returns one of three verdicts:
on_abstain.Gates compose: a rung can carry several, and all must pass. A score in [0,1] is optional; when a gate emits one, the operator's serve threshold governs whether that score clears. Every gate's cost is written onto the receipt.
Built-in inline#
non-empty rejects blank or whitespace outputs, and json-valid rejects outputs that don't parse as valid JSON. Enable them as a floor on every rung — they cost nothing and catch a surprising share of model failures before pricier checks ever fire.Two checks run with zero configuration and no extra model calls:
non-empty or json-valid failure stops the gate chain immediately and escalates — so you never wait for a subprocess timeout on a blank output. Put them first; they're free.schema#
schema gate validates structural shape — top-level type, required, and per-property type — against a JSON-Schema subset. Free (no extra model calls) and synchronous. It enforces the contract on a structured output without running a full schema engine.The schema gate validates against a JSON-Schema subset: top-level type, required, and per-property type. Enough to enforce the shape of a tool call or a structured response without a full schema engine.
[[gate]]
kind = "schema"
schema = { type = "object", required = ["total", "items"], properties = {
total = { type = "number" },
items = { type = "array" }
}}schema as a full JSON Schema validator. It covers the subset that matters for LLM structured outputs — top-level type and required — but does not evaluate enum, minimum/maximum, pattern, or nested $ref. For richer structural guarantees, layer a subprocess gate that runs a real schema library after this one.subprocess — bring your own#
cargo test, pytest, an eslint run, whatever you already use to verify correctness.The most powerful gate: any executable. This is "bring your own tests / linter / judge." The candidate arrives as JSON on stdin — never as a command-line argument, which keeps it injection-resistant — and the process prints a verdict as JSON.
[[gate]]
kind = "subprocess"
cmd = ["python", "run_tests.py"]
timeout_ms = 5000The stdin / stdout contract
The contract for the executable:
// stdin (what your program reads)
{ "candidate": "def solve(...): ...", "prompt": "...", "meta": { } }
// stdout (what your program prints)
{ "verdict": "pass", "score": 0.91, "reason": "12/12 tests passed" }Timeout and error handling
verdictis one ofpass|fail|abstain.score(optional) is a float in[0,1].reason(optional) is a human-readable note, recorded on the receipt.- A non-zero exit code or a timeout is treated as abstain.
on_abstain rather than triggering the expected escalation. Print {"verdict":"fail"} on stdout and exit 0. Reserve non-zero exits for unexpected errors your program can't recover from.judge — native LLM#
judge gate sends the candidate to a different model with a rubric and scores the output. Anti-gaming is structural, not a prompt trick: if the configured judge equals the candidate model, the gate abstains rather than let a model mark its own work. The candidate is fenced as untrusted data under a pinned system prompt.The judge gate uses a model to grade the candidate against a rubric. Its anti-gaming design is structural, not a prompt trick:
- maker ≠ checker. A model never grades its own output. If the configured judge equals the candidate's model, the gate abstains rather than let a model mark its own work.
- Candidate is fenced as untrusted data under a pinned system prompt — the judge is instructed to grade, not to follow, whatever the candidate contains.
- Operator threshold governs. The judge emits a score; you decide the bar it must clear.
[[gate]]
kind = "judge"
model = "anthropic/claude-sonnet-5" # must differ from the candidate model
rubric = "Is the answer correct, complete, and free of unsupported claims?"
threshold = 0.7consistency#
consistency gate resamples the output k times and scores by agreement across samples. Unlike the judge, maker == checker is intentional — the signal is the model's own stability, not independence from it. Use it when there is no external oracle and you want to measure how confidently the model arrives at its answer.The consistency gate measures self-consistency: resample the candidate k times and score by agreement across samples. Unlike the judge, maker == checker is intentional here — the signal is the model's own stability. It suits tasks without a crisp oracle, drawing on self-consistency (Wang et al. 2022) and semantic entropy (Farquhar et al., Nature 2024).
[[gate]]
kind = "consistency"
k = 5 # extra samples — this gate costs k additional calls
threshold = 0.8k extra times. It buys a signal where no external check exists, but it is the most expensive gate — price it against the value of the decision.k extra model calls — and the right choice only when no external oracle exists; maker == checker is intentional here, not a flaw.Abstain & error budget#
fail_open (default) treats abstain as pass and serves; fail_closed treats it as fail and escalates. A separate error budget tracks gate reliability: once a gate blows its rolling failure rate, Firstpass auto-disables it so one flaky check can't stall every request in the cascade.Two safety mechanisms wrap every gate:
fail_open (default) treats an inconclusive gate as a pass and serves; fail_closed treats it as a fail and escalates. Choose per gate based on whether a false serve or a wasted escalation is worse for that check.fail_open vs fail_closed is a per-gate choice, not a global setting. A non-empty check almost always belongs on fail_closed — an abstain there means the gate itself crashed, and serving a blank response is never right. A subprocess gate that runs optional linting might use fail_open — a timeout is not a reason to stall every request when the output is probably fine.fail_closed without monitoring the error budget. A gate that times out repeatedly will blow its budget and get auto-disabled — silently removing a check you were relying on. Watch /metrics for per-gate error rates so you catch flakiness before the budget runs out and the check disappears.Gate cost on the receipt#
gate_cost_usd, separate from the model spend. When you run firstpass savings, the net figure is already net of what verification cost. Proof has a price, and it's on the ticket — you always know what it cost to be sure.Verification is not free, and Firstpass refuses to hide it. Every gate's cost — a judge's tokens, a consistency gate's k extra calls, a subprocess's wall time priced in — is written onto the receipt as gate_cost_usd, separate from the model spend. When you run firstpass savings, the net figure already accounts for what verification cost you. Proof has a price, and it's on the ticket.
gate_cost_usd is additive, not amortized. A judge gate on a rung that ultimately escalates still records the judge's token cost on that attempt — even though the model output it checked was discarded. This is the honest accounting: verification cost is owed regardless of whether the output it checked turned out to be good enough. You can't optimize what you can't see.gate_cost_usd — separate from model spend, always visible in firstpass savings, never hidden.