Blog — LLM Engineering

How to wire a System One model
into an agent pipeline.

A typed decision model such as Jev is easy to call and easy to get wrong in production. This is the integration pattern we used for our benchmark: a swappable interface, validated outputs, retries that do not hide failures, a confidence cascade and honest measurement, with the code.

Rajeish Mata Oct 22, 2026

Pick the calls first, then the model.

A System One model answers a typed question about some text and returns a value with a probability. That makes it a good fit for the small, frequent decisions inside an agent: routing a ticket, flagging a message, scoring urgency. It does not write replies or explain itself. If you have not yet decided which of your calls are decisions, start with the benchmark and the four-question test, or run your calls through the free decision-call audit.

The rest of this guide assumes you have a call that passes. The code is simplified from the open-source system-one-decision-bench harness, which has the full, tested versions.

Put the model behind an interface.

Model identifiers differ between providers, a hosted model can change under you, and in our own runs identical inputs gave slightly different accuracy on a repeat. So the first rule is that your agent never calls a vendor SDK directly. It calls a Decider that returns a plain Decision, and the vendor sits behind it. This is the same idea as in avoiding AI vendor lock-in.

@dataclass(frozen=True)
class Choice:
    prompt: str
    options: tuple[str, ...]          # the closed set of valid answers

@dataclass
class Decision:
    label: str | bool | int | None
    probabilities: dict[str, float] | float | None
    latency_ms: float
    cost_usd: float
    model_id: str                      # the version that actually answered
    error_kind: str | None = None      # None, "invalid" or "api"

    @property
    def confidence(self) -> float | None:
        p = self.probabilities
        if p is None:
            return None
        if isinstance(p, float):       # yes/no: probability of yes
            return max(p, 1.0 - p)
        return max(p.values())         # choice/score: top outcome

class Decider(Protocol):
    def decide(self, question, input) -> Decision: ...

Three things matter here. The set of valid answers is part of the question, so a bad answer can be detected. Every decision carries its latency, cost and the model version that produced it, so you can audit it later. And failures are values, not exceptions that vanish: error_kind is "invalid" when the model answered something unusable and "api" when the call itself failed.

Call it, then validate what comes back.

You send the text (or JSON) as the state, plus a question with its answer set. For a choice question the options go in as criteria:

body = {
    "model": MODEL_ID,                 # pin an exact version in config
    "state": ticket_text,              # text or JSON; text only
    "questions": {"q": {
        "type": "choice",
        "instructions": "Which team should handle this ticket?",
        "criteria": {"billing": None, "technical": None, "account": None},
    }},
}
r = client.post(f"{BASE_URL}/v1/systemone", json=body, headers=auth)
answer = r.json()["answers"]["q"]      # {"choice": ..., "probabilities": {...}}

if answer.get("choice") not in options:
    return failure("invalid", f"bad choice answer: {answer!r}")

The last check is easy to skip and should not be. Even when the response parses, confirm the answer is one of your options and that the probabilities have the shape you expect. In our benchmark we count anything unusable as wrong and report it, instead of dropping it, so a model cannot look better than it is by failing quietly. Yes/no questions come back as a probability that the statement is true, and score questions come back as a level with probabilities over your rubric.

Retry the right failures, and count them.

Retry transport errors and the status codes that mean “try again” (429 and the 5xx family), back off exponentially with jitter, and honour a Retry-After header if you get one. Do not retry a bad request. Record how many retries a call needed, and measure latency on the successful attempt only, so backoff time does not pollute your latency figures.

RETRY_STATUS = {429, 500, 502, 503, 504, 529}

def with_retries(fn, max_retries=4, base_delay=0.5):
    for attempt in range(max_retries + 1):
        try:
            return fn(), attempt           # result and how many retries it took
        except RetryableError as e:
            if attempt == max_retries:
                raise
            delay = e.retry_after if e.retry_after is not None else base_delay * 2**attempt
            time.sleep(delay + random.uniform(0, base_delay))

Keep concurrency modest. We ran four calls in flight against a hosted early-access model, which is a polite default until you know your limits.

Use the probability, but only if you have a stronger fallback.

A confidence cascade lets the fast model decide and sends a call to a second model only when the first is unsure. The whole mechanism fits on a page:

def should_escalate(primary: Decision, threshold: float) -> bool:
    conf = primary.confidence
    return primary.error_kind is not None or conf is None or conf < threshold

class CascadeDecider:
    def __init__(self, primary, fallback, threshold):
        self.primary, self.fallback, self.threshold = primary, fallback, threshold

    def decide(self, question, input):
        first = self.primary.decide(question, input)
        if not should_escalate(first, self.threshold):
            return first
        second = self.fallback.decide(question, input)
        # if the fallback failed but the primary answered, keep the primary's answer
        use = first if (first.error_kind is None and second.error_kind) else second
        use.latency_ms = first.latency_ms + second.latency_ms   # escalated calls pay twice
        use.cost_usd = first.cost_usd + second.cost_usd
        return use

Two details are worth copying. A primary failure always escalates, and an escalated call pays for both models in cost and latency, so report it that way. The threshold should come from labelled validation data, not a guess.

Now the honest caveat. In our benchmark, with Jev first and a small chat model as the fallback, the cascade barely helped: only 5.4%, 0.2% and 0% of calls escalated at the thresholds we would pick, and pushing the threshold higher dragged accuracy toward the fallback’s. A cascade only pays when the fallback is stronger than the primary, and ours was not. Test it with a stronger fallback on your own data before you build around it. If the primary and fallback are equally accurate, skip the cascade and keep the primary.

Measure it before it ships.

Run a few hundred of your own labelled examples through the interface and record the same five numbers every time:

  • Accuracy with a confidence interval, because a few hundred examples is a small sample.
  • Median and p95 latency, measured from your client with the network included. A median hides the slow tail your users will notice.
  • Cost per 1,000 decisions, from the token usage each response reports times the published price, labelled as list price and not as spend.
  • Invalid and error rates, counted separately.
  • Calibration, to see whether 90% confidence really means about 90% correct. In our run, Jev’s top confidence band was 98.4% stated and 93.8% actual, so do not treat probabilities as certainty.

Cache each decision keyed by model version, a hash of the question text and the example ID. Reruns become cheap, and editing a prompt cannot silently reuse stale answers. The harness does all of this for Banking77 and a safety dataset, and its report shows what the output looks like.

Before you turn it on.

CheckWhat to confirm
VersionThe exact model version is pinned in config and logged on every decision.
ValidationEvery answer is checked against your option set or rubric, and unusable answers are counted as failures.
Failure pathYou know what the agent does when the model is down or invalid: a fallback model, a safe default, or a human.
ThresholdAny confidence threshold comes from validation data, with a stronger fallback behind it.
Irreversible actionsA decision never triggers a refund, deletion or message on its own. A deterministic code path or a person confirms it.
MonitoringLatency, cost, invalid rate and the confidence distribution are tracked, so drift shows up before users report it.

Adversarial and edge-case testing of these paths is part of our AI agent testing and evaluation work, and the approval pattern for irreversible actions is the one in our agent guardrails case study.

Where this fits.

Connecting models to a product’s real systems is our LLM integration work. If you want help deciding which calls to move and proving it on your data, that is the decision-layer assessment described there. The benchmark post has the measured results, and the provider abstraction starter has a general version of the interface above.

Get started

Want this wired into
your pipeline?

Book a 30-minute call to scope a decision-layer assessment: your calls audited, the best candidates tested on your data, and a clear recommendation.