Never ship unverified LLM output: multi-model councils, severity-graded audit gates, bounded retries, deterministic fallbacks. Zero dependencies.
Project description
quorumgate
Never ship unverified LLM output.
quorumgate is a zero-dependency reliability layer for LLM pipelines. It was
distilled from a production system that emails AI-generated reports to real
subscribers every day, where a hallucinated number or a half-rendered template
is not a bug ticket — it lands in someone's inbox. The rules that system lives
by are the rules this library encodes:
- Every output is audited before it ships. Checks are graded HIGH / MED / LOW. HIGH means do not ship this.
- Failure earns a bounded retry, not a shrug. The retry sees exactly which checks failed, so it can switch to a stronger model or a tighter prompt instead of rolling the same dice again.
- The last line of defense is deterministic. When generation cannot pass the gate, a template built without any LLM ships instead — degraded, but never wrong, and never silent.
- Ensembles must degrade gracefully. A multi-model council is an enhancement, not a dependency: dead seats get circuit-broken, quorum failures are reported instead of raised, and a wall-clock budget stops a sick provider from blowing your deadline.
No LLM SDK is imported anywhere. A "model" is any Callable[[str], str] you
provide — OpenAI, Anthropic, Gemini, Ollama, a local server, or a lambda in a
test. The framework supplies structure; you supply models, checks, and
fallbacks.
pip install quorumgate # stdlib only, Python >= 3.9
Quickstart
from quorumgate import AuditGate, check, Severity
@check("no_invented_links")
def no_invented_links(output, context):
if "http" in output and "http" not in context["source"]:
return "summary contains a URL that is not in the source"
@check("long_enough")
def long_enough(output, context):
return len(output.split()) < 15 and "summary under 15 words"
def summarize(attempt, context):
model = strong_model if attempt.is_retry else cheap_model # your callables
return model(f"Summarize:\n{context['source']}")
def first_sentences(context): # deterministic: never wrong
return " ".join(context["source"].split(". ")[:2]) + "."
gate = AuditGate([no_invented_links, long_enough], max_retries=1, cooldown=60)
result = gate.run(summarize, fallback=first_sentences, context={"source": text})
result.output # what ships — always
result.source # "primary" | "retry" | "soft-pass" | "fallback"
result.verified # True unless HIGH failures survived (fallback is audited too)
With a fallback, gate.run never raises — the deadline philosophy is
that shipping the deterministic version beats shipping nothing, and both beat
shipping something wrong. Without a fallback, unresolvable HIGH failures
raise GateError, because silence is the one thing the gate will not do.
A multi-model council
import json
from quorumgate import Council, Seat, JudgeArbiter, CircuitBreaker
council = Council(
seats=[
Seat("gemini-flash", call=my_gemini), # each: prompt -> raw text
Seat("gpt-mini", call=my_openai),
Seat("local-qwen", call=my_ollama),
],
arbiter=JudgeArbiter( # an LLM judge synthesizes...
call=my_judge_model,
build_prompt=lambda ops: "Synthesize one verdict from:\n"
+ json.dumps([o.content for o in ops]),
), # ...and if the judge dies,
quorum=2, # highest-confidence wins
breaker=CircuitBreaker(max_strikes=3, dead_markers=("quota", "402")),
time_budget=600, # whole batch, wall-clock
opposed=[("long", "short")], # head-on conflict = dissent 2
)
verdict = council.convene("Assess the 1-2 week outlook for ACME.")
verdict.quorum_met # False -> use your single-model path, don't crash
verdict.dissent # 0 unanimous / 1 mixed / 2 head-on conflict
verdict.verdict # the arbiter's synthesis
Seats parse their own replies (default: tolerant JSON extraction that strips
markdown fences); arbitration is a strategy object (JudgeArbiter,
MajorityArbiter, HighestConfidenceArbiter, or your own). Disagreement is
a first-class signal: pass dissent downstream so a split council produces a
hedged output, not false confidence.
Composed
from quorumgate import Pipeline
pipeline = Pipeline(
generate=write_report, # (attempt, context, council_result) -> str
gate=gate,
council=council,
council_prompt=lambda ctx: f"Debate the outlook for {ctx['topic']}.",
fallback=deterministic_report,
)
result = pipeline.run({"topic": "solar"})
Two runnable, key-free demos live in examples/:
daily_brief.py (council + gate + fallback +
systemic alerting across a subscriber batch) and
summarize.py (escalate-on-retry summarization).
Architecture
┌─────────────────────────────────────────┐
│ Pipeline │
│ │
context ──────► Council (optional) │
│ seats (parallel) ──► parse ──► quorum? │
│ │ per-seat CircuitBreaker │
│ │ wall-clock budget │
│ ▼ │
│ Arbiter (judge / majority / custom) │
│ │ judge fails -> fallback arbiter │
│ ▼ verdict + dissent │
│ │
│ generate(attempt, ctx, verdict) │
│ │ │
│ ▼ │
│ AuditGate ── checks (HIGH/MED/LOW) │
│ │ HIGH? -> cooldown -> retry │
│ │ (attempt carries failures) │
│ │ still HIGH? │
│ │ ├─ all soft + policy ok -> ship │
│ │ ├─ fallback() -> audit -> ship │
│ │ └─ no fallback -> GateError │
│ ▼ │
│ GateResult(output, source, verified) │
└─────────────────────────────────────────┘
Production extras you will eventually want
- Soft HIGH checks — some checks gate quality ("reasoning too
shallow"), not correctness ("price is fabricated"). Mark them
soft_checksand supply asoft_policyto decide when a below-bar-but- not-wrong output may ship (e.g. to an internal audience) instead of degrading to the fallback. - Systemic escalation — when the same HIGH check fails across
systemic_thresholdindependentrun()calls, that is not per-item bad luck; it is a template or prompt bug hitting your whole batch.on_systemicfires exactly once per check so a human is paged before the batch finishes, not after. - Retry cooldown — free-tier rate limits are usually per-minute windows.
An instant retry lands in the same window and fails the same way;
cooldown=60makes the retry actually mean something. - Circuit breaker dead-markers — "quota exceeded" and "payment required" are not transient. Substring markers open the seat on the first strike instead of paying for three.
What this is not
quorumgate is a reliability layer, not an orchestration framework.
| LangChain / LlamaIndex / DSPy | quorumgate | |
|---|---|---|
| Core question | How do I chain LLM calls together? | How do I make sure a bad LLM output never reaches a user? |
| LLM clients | Bundled integrations | None — you inject callables |
| Dependencies | Many | Zero (stdlib only) |
| Prompting | Templates, optimizers | Your problem, on purpose |
| Failure model | Exceptions / callbacks | Graded checks → retry → deterministic fallback, never silent |
| Ensembles | Chains/graphs of calls | Council with quorum, circuit breaking, budget, dissent signal |
Use both if you like: build your chain in anything, then put its final output
behind an AuditGate. The gate does not care who generated the text.
Similarly, this is not a guardrails DSL — there is no YAML, no built-in
toxicity classifier, no schema language. A check is a Python function over
(output, context), because in practice the checks that save you are
domain-specific ones nobody could have shipped in a library.
API surface
| Object | Role |
|---|---|
Seat(name, call, parse?) |
One model voice: prompt → raw text → parsed opinion |
Council(seats, arbiter, quorum, breaker, time_budget, opposed) |
Parallel collection + arbitration + graceful decay |
Arbiter / JudgeArbiter / MajorityArbiter / HighestConfidenceArbiter |
Pluggable arbitration strategies |
CircuitBreaker(max_strikes, dead_markers) |
Skip dead providers for the rest of a run |
@check(name, severity) |
Predicate → graded check |
AuditGate(checks, max_retries, cooldown, soft_checks, soft_policy, systemic_threshold, on_systemic) |
Verify → retry → fallback; never ship unverified |
Pipeline(generate, gate, council?, fallback?) |
The common composition in one call |
extract_json(text) |
Tolerant JSON-from-LLM-text helper |
GateResult / CouncilResult / Failure / Attempt / Severity |
Typed results end to end |
Testing
pip install -e ".[dev]"
pytest
The suite covers council arbitration (majority, judge, judge-death fallback, quorum, dissent, budget exhaustion), gate behavior (retry, fallback, soft policy, systemic escalation, crashing checks), and the circuit breaker — all with plain callables, no network.
License
MIT © Delvin Chang
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quorumgate_llm-0.1.0.tar.gz.
File metadata
- Download URL: quorumgate_llm-0.1.0.tar.gz
- Upload date:
- Size: 22.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fae1d9664640d90a424aba5b811a14765ec0c788526c27f1013f466aad07db37
|
|
| MD5 |
556ba974c9f5249934ed1e1b16e268cf
|
|
| BLAKE2b-256 |
785313e4d536b2f68f28ff38d4734a828235c50f398faf1d95362103a8bff9a2
|
File details
Details for the file quorumgate_llm-0.1.0-py3-none-any.whl.
File metadata
- Download URL: quorumgate_llm-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
100e93e4427c02e476ada0fd599ea516a69623bad9a94ca08c2b3f6c2d13cd7e
|
|
| MD5 |
221b0a977dfe754f970146063b10644d
|
|
| BLAKE2b-256 |
d2a2e8bf9380b466d400bd8e17510e7c680825ed9e8f0e95bdba573469c4e3b7
|