Don't ask the model to decide. Ask it questions. Decide in code.
A decision circuit is a set of typed questions a model answers with probabilities, and a set of gates that turn those probabilities into decisions: thresholds, AND/OR/NOT, votes, verification. The model never sees the gates. Every decision carries its probability, its outcome, and a trace. When the number sits too close to a threshold to call, the gate says so and a human gets the case.
$pip install decision-circuits# no dependencies
The family
Four models, one contract
Each is a small open model tuned to do one thing: answer a typed question with a probability you can trust. They differ in what they take in, text, pictures, or sound. They all speak the same API, so a circuit written for one runs on any of them, and the weights are yours to download.
circuit-1.7b
Qwen3-1.7B-Base · text, JSON, chat threads
SMS spam
98% / 0.02
151-way intents
86% / 0.06
per question
175 ms
Reads text, JSON, and chat threads. A yes/no, a pick from any number of options, or a score, in one pass and 175 ms on a laptop. Runs anywhere, no GPU needed.
The 8-ball. The strongest text model in the family: 151-way intents, long threads, real production questions. Probabilities steady enough to put a threshold on.
Reads receipts, charts, tables, and forms, and looks at photographs. Ask what a document says, whether a claim matches it, how many rows are flagged, whether there is a dog in the picture. No cap on options.
Listens to calls and recordings. What kind of call is this, what amount was said, did they say the word, which sentence was read. Hears the clip; no transcript needed.
Eight questions straight from the benchmark, on the clock.
Real items from the text, vision, and audio grids and the article's water-utility calls, labels assigned by the code that built them. Chat-model times were measured here through the Claude CLI; audio went through a local Whisper first, because a chat model has to read a transcript.
8 questions from the benchmark, one at a time, on the clock.
The clock starts when a question appears and stops when you pick. Each answer shows what the circuit model said and how long it took, against a chat model on the same question. The last card puts every answer and every time side by side.
·
0.0 s
Hi, my water was shut off yesterday for repairs on my street, but it still hasn't been turned back on. Can someone come restore my service?
t1 blender (not returned); t2 wireless headphones (not returned); t3 notebook set (is returned); t4 rain jacket (not returned)
The knowledge base has a new article on setting up two-factor authentication.
If you have feedback on the new dashboard, reply to this message.
The support call was on January 11, 2026. The payment was on 12 Mar 2026.
Weather permitting, deliveries in the region continue on the usual schedule.
We appreciate your patience while we migrate to the new billing system.
customer Here are the options.
customer r1: blender, price 492
customer r2: board game, price 100
customer r3: board game, price 217
API
Call the models without running anything
A hosted endpoint serves all four models at api.decisioncircuits.com: the same request format as TypeSafe's POST /v1/systemone, and for circuit-vl-4b and circuit-audio-7b the state is an image or a clip. Keys are free: 1,000 questions a day per key, no card, no account. The models scale to zero between calls, so the first call after a quiet spell takes about a minute while a GPU spins up; warm calls answer in under a second.
curl
curl https://api.decisioncircuits.com/v1/systemone \
-H "Authorization: Bearer dc-..." \
-H "Content-Type: application/json" \
-d '{
"model": "circuit-8b",
"state": "My name is Dana Whitfield and my account number is 88421.",
"questions": {
"pii": {
"type": "noul",
"instructions": "Does this message contain personally identifiable information such as a name, address, phone number, or account number?"
}
}
}'{"answers": {"pii": {"type": "noul", "noul": 0.814}}}
Python
pip install decision-circuitsfrom decision_circuits import Circuit, Q
from decision_circuits.backends import SystemOne
api = SystemOne("https://api.decisioncircuits.com/v1/systemone", api_key="dc-...")
c = Circuit()
c.noul("pii", "Does this message contain personally identifiable information such as a name, address, phone number, or account number?")
c.gate("redact", Q("pii") >= 0.6, band=0.1, on_uncertain="escalate")
out = c.run(api, "My name is Dana Whitfield and my account number is 88421.", model="circuit-8b")
print(out["gates"]["redact"])
{'value': True, 'p': 0.81, 'outcome': 'decided', 'trace': ['pii p=0.81']}
Two ways to send the same question. The output under each is what these exact calls return — 0.81 clears the gate's 0.6 threshold by more than its band, so the circuit decides rather than escalating. Shorten that message and the probability slides toward the band; that is the model being honest, not broken.
What happens to what you send: your inputs answer your request and are discarded, never stored and never trained on. The key record holds a hash of the key, the date, and an email if you give one. Terms and data use.
Reading this as an agent? decisioncircuits.com/skill.md is written for you: when a calibrated judgment beats a guess, how to get a key without a human, and how to turn the probability into a decision you can defend.
A clip instead of a sentence
Nothing about the call changes when the state is sound or a picture: the state becomes {"audio": "<url or data URI>"}, or Audio(...) and Image(...) in Python. This example runs as written — the clip is a synthesized water-utility call hosted on this site, and the model hears it without a transcript.
from decision_circuits import Audio, Circuit
c = Circuit()
c.choice("kind", "Which kind of water-utility call is this?",
{"billing": None, "metering": None, "taste and odor": None, "outage": None, "leak": None})
c.noul("urgent", "Does the caller need someone today?")
out = c.run(api, Audio("https://decisioncircuits.com/bench/call.wav"), model="circuit-audio-7b")
kind leak, p 1.00
urgent 0.99
That clip is one of the quiz's audio questions, so you can listen to it yourself above. A first call to a model that has been idle waits about a minute for a GPU; after that it answers in around a second. Pass Image("receipt.png") with circuit-vl-4b for documents and photographs.
A support-triage circuit, live
drag the probabilities a model might return; gates re-evaluate
What the model answered
billingtechnicalother
What code decided
redact(pii & ~business) ≥ 0.6, band 0.1, on_uncertain: escalate
routeargmax(dept), min_confidence 0.35
human(angry | pii) ≥ 0.6
Nudge pii to 0.65 and watch redact stop answering. That's the point: inside the band, the circuit escalates instead of guessing.
Write one
The circuit above, in Python
# questions the model answers
c = Circuit()
c.noul("pii", "Does the message contain personal information about a private individual?")
c.noul("business", "Are all identifying details about a business rather than a person?")
c.noul("angry", "Is the customer angry?")
c.choice("dept", "Which team should handle this?", {"billing": "Money, refunds", "technical": "Bugs, outages", "other": None})
# gates code evaluates
c.gate("redact", (Q("pii") & ~Q("business")) >= 0.6, band=0.1, on_uncertain="escalate")
c.gate("route", argmax("dept", min_confidence=0.35))
c.gate("human", (Q("angry") | Q("pii")) >= 0.6)
out = c.run(SystemOne(api_key=KEY), "Card charged twice, refund NOW. My card ends in 4412.")
out["gates"]["redact"]
# {'value': True, 'p': 0.97, 'outcome': 'decided', 'trace': ['pii p=0.97', 'gate _redact_1 p=0.98', 'and under independence -> p=0.95']}
The arithmetic is deliberately simple and is written into the trace: AND is a product, OR is 1 − ∏(1 − p), NOT is 1 − p. AND and OR assume the inputs are independent, and every trace says so, because a reviewer should see that assumption next to the number.
form
meaning
Q("pii"), Q("dept")["billing"], Q("urgency")[3]
a question's probability, or one option's
~a, a & b, a | b, e >= tau
NOT, AND, OR, threshold with an uncertainty band
argmax("dept", min_confidence=…)
top option; abstain below the confidence floor
majority("q1", "q2", "q3")
vote across paraphrased questions
verify("dept", check=Q("supported"), tau=…)
a negative checker: escalate when the check doesn't support the pick
order("severity", [c1, c2, …])
bucket an ordered score
G("route")["billing"]
gates over gates
c.to_mermaid() renders any circuit as a schematic: inputs, logic, decisions, colored by outcome after a run.
Numbers
The 2025 article, re-run on a model built for this
The original write-up classified 100 water-utility customer calls into eleven types with two LLM parsers and a negative checker, combined into confidence tiers. Same calls, same circuit, one call to a System One model:
2025, Claude Sonnet 3.7, three LLM calls
Jev, one call
circuit-8b, one call
circuit-1.7b, one call
single question, no circuit
91%
98%
93%
91%
circuit, overall
87%
98%
93%
91%
high-confidence calls
80 calls, 92.5% right
93 calls, 97.8% right
90 calls, 94.4% right
88 calls, 90.9% right
sent to a human (the article's rule)
2%
0%
0%
0%
latency
three round trips
325 ms for all three questions
579 ms, 88 ms of it on the GPU
720 ms, 175 ms of it on the GPU
Jev is the commercial model; the circuit columns are the open weights on this page, run through the same circuit on the same 100 calls (examples/11_water_utility_calls.py). The line worth noticing is the last column: circuit-1.7b matches the article's three Sonnet calls — 91% single, 91% circuit — from a 1.7B model you can download and run on a laptop. circuit-8b adds two points and a cleaner high-confidence tier. Latency is wall time per call from a laptop, all three questions in one request, most of it network.
That 0% is the rule going quiet, not the model being certain. The article escalates only when the negative checker says the call isn't categorizable and the two parsers disagree — and two sampled chat calls disagree often enough for that to fire. One calibrated model asked the same question two ways agrees with itself on 97 of 100 calls, so the two conditions never coincide and nothing reaches a person. Seven calls that circuit-8b itself scored below 0.5 on "is there enough information here" were answered anyway.
Disagreement was a stand-in for a number nobody had. Tier on the probability instead — the same three answers, no extra calls — and the escalation comes back: circuit-8b sends 8% to a human and is right on 95.5% of the 88 it keeps; circuit-1.7b sends 15% and is right on 100% of the 69 it keeps. That is the trade the article was reaching for, priced in a quantity the model actually reports. The example prints both readings.
And a test that didn't exist before: nine kinds of judgment (extract, compare, count, apply a rule, check a claim against a record, …) times six ways of laying out the data, every label computed by code. Jev scores 95% on it. It also gets confident on inputs built to be undecidable, which is exactly what a threshold with a band is there to catch.
Scoreboard
Every System One model we can get our hands on, same items, same labels
Jev is TypeSafe's API. Bespoke-Nimble-9B and kev-0.5b are open reproductions released September 18. circuit-1.7b is ours. Cells are accuracy / expected calibration error; lower ECE is better calibrated. Latency is per question, measured the same way for every row in a block.
cold eval, 1,200 human-labeled items
MultiNLI
SMS spam
toxicity
CLINC, 151 intents
latency
Jev
88% / 0.04
96% / 0.05
82% / 0.06
90% / 0.05
164 ms, API
Bespoke-Nimble-9B
84% / 0.09
91% / 0.06
86% / 0.08
unsupported (26-option cap)
1.8 s, laptop
kev-0.5b
46% / 0.28
50% / 0.30
62% / 0.16
62% / 0.17
325 ms, laptop
circuit-1.7b
81% / 0.09
98% / 0.02
90% / 0.16
86% / 0.06
175 ms, laptop
circuit-8b
86% / 0.08
98% / 0.02
93% / 0.14
95% / 0.03
178 ms, A6000
Those four tasks' public train splits are in circuit-1.7b's training data and presumably not in Jev's. The two blocks below were trained on by nobody.
out of distribution
Jev
Nimble-9B
kev-0.5b
circuit-1.7b
circuit-8b
the article's 100 water-utility calls, 11 types
98% / 0.02
93% / 0.05
80% / 0.13
92% / 0.08
93% / 0.05
546 production questions from a real site (agreement with Jev)
* The grid generator is ours, so that row is held-out items, not held-out structure. Trained with one operation and one format withheld, the same recipe scores 91% on the unseen format and 57% on the unseen operation. Layouts transfer; new kinds of judgment have to be in the data.
The finding nobody else measures. About five percent of grid items are built to be undecidable: a vague claim, two equal bars under "which is taller", a date with no year. The right answer is a flat distribution. Every model above, Jev included, answers those with mean confidence between 0.5 and 0.97. That is exactly the failure an uncertainty band around a threshold exists to catch, and why circuits escalate instead of trusting a number near the line.
circuit-1.7b: LoRA plus a pointer readout head on Qwen3-1.7B-Base, trained in 61 minutes on one RTX 4090 on code-labeled and CC-licensed data only. Weights and card: huggingface.co/jbarney/circuit-1.7b. circuit-8b: the same recipe on Qwen3-8B-Base, 75 minutes on one RTX A6000; it beats Jev on three of the four cold-eval tasks and matches Nimble-9B on the production questions. Weights: huggingface.co/jbarney/circuit-8b. Training and evaluation code: github.com/Barneyjm/circuit.
Agents
The circuit sits where the agent shouldn't be trusted
LangChain
CircuitToolGuard judges each tool call: allow, block, or pause with the standard human-in-the-loop interrupt. CircuitRouter picks a model per run.
OpenAI Agents SDK
Input, output, and tool guardrails from a circuit. Tripwires carry the gate's probability and trace.
Claude Agent SDK
A PreToolUse hook whose allow / deny / ask maps straight onto the gate's decided / blocked / uncertain.
Compared with a single yes/no classifier at a fixed 0.5, a circuit combines several questions, makes the threshold and its band explicit, and sends the uncertain cases to a person instead of silently allowing or blocking them.
Models
Built for System One models
Circuits need calibrated probabilities. That's what a System One model produces: typed questions in, a distribution per question out, in one pass, in about 200 ms. The SystemOne backend talks to any server speaking that contract.
Jev, by TypeSafe
The model that made this practical. Drop in an API key.
Our own S1 models: pointer readout, code-labeled and CC-licensed data only. The small one runs at 175 ms per question on a laptop; the large one leads the scoreboard.
Yours
A backend is one method: answer(state, questions). Wrap a cache, inject code-owned facts, chain fallbacks.
No S1 model yet? Chat models can stand in through logprobs (OpenAI-compatible) or tool use (Claude), with the caveat that a chat model's stated confidence isn't calibrated the way an S1 model's output is.