Engineering research / llm

From Probabilities to Actions With the Decisions API: Predicate Thresholds, Choice Versus Score, Batched Questions and Calibration

The OpenAI Decisions API reached general availability on 22 October 2026. A practical guide to what its answers contain, how to turn a probability into approve, review or deny with a labelled sample and a cost model, why a score must not be rounded, and how to batch and cache questions safely.

By Sandeep Belgavi · Published 2026-10-26

Agent and AI engineering · Practical technical guide · 26 October 2026

The OpenAI Decisions API reached general availability on 22 October 2026. A practical guide to what its answers contain, how to turn a probability into approve, review or deny with a labelled sample and a cost model, why a score must not be rounded, and how to batch and cache questions safely.

What the API is, and what changed at GA

The Decisions API is a separate endpoint, POST /v1/decisions, for questions with typed answers rather than generated text. The OpenAI changelog for 22 October 2026 lists it as generally available with gpt-6-luna, which the guide says is currently the only supported model. Our research digest of 9 October covered the public beta, which arrived on 6 October; this guide is about using the GA version in an application.

The guide lists what changed since the beta: prompt caching for shared input, images inside individual question instructions, structured evidence passed as JSON with an input_json part, up to 400 questions per request instead of 200, hosted image URLs as well as inline data URLs, conversation history that may include function calls, custom tool calls and their outputs, organisation usage reporting, a native GPT-Live integration and Private Link. The guide also states the boundary clearly: use Decisions when you need one of its answer types, and Structured Outputs or function calling when you need the model to generate an object or request a tool call.

Three question types and what comes back

Question typeYou supplyAnswer containsUse it for
predicateInstructionsprobability, from 0 to 1, that the condition is trueYes or no checks: is the charge a duplicate
choiceInstructions and 2 to 255 unique choicesthe chosen value, a confidence, and a probability for every valueUnordered categories: which department
scoreInstructions and ordered levelsa probability for every level and a score, the probability-weighted average of level indicesOrdered levels: issue severity
any of themtype refusal with the question name, instead of an answerMust be handled for every question

Answers come back in question order and carry the name you gave each question, so name every question and match by name, not position. A refusal affects only its own question; the other answers in the same response are still valid. Choice values are typed, so a string true and a boolean true are different values. None of these answers is an action. The guide is explicit that your application runs whatever action an answer selects, which is exactly where the engineering work lies.

Turning a probability into approve, review or deny

A single cut at 0.5 treats a probability as if it were already the right decision boundary. It rarely is, for two reasons: the costs of the two kinds of mistake differ, and a model's probabilities are not guaranteed to match how often it is right. Both are checked against a labelled sample of your own traffic before any threshold is trusted.

The companion example uses a synthetic sample of 200 duplicate-charge cases with a deliberately overconfident model, and costs of 40 for wrongly approving a refund, 25 for wrongly denying one, and 3 for a human review. Its reliability table shows the overconfidence: cases the model put in the top bin averaged 0.962 but only 0.894 were real duplicates, and the bottom bin averaged 0.039 with 0.093 real. A single 0.5 cut makes 20 wrong approvals and 13 wrong denials, a cost of 1,125. The cheapest band on the same 200 items denies at or below 0.10, approves at or above 0.90 and sends the 54 cases in between to a person, for a cost of 412.

Python from the companion example, condensed; standard library only.

def band_cost(items, low, high, cost):
    """Approve at or above high, deny at or below low, review the rest."""
    t = {"wrong_approve": 0, "wrong_deny": 0, "reviewed": 0}
    for x in items:
        if x["probability"] >= high:
            t["wrong_approve"] += not x["label"]
        elif x["probability"] <= low:
            t["wrong_deny"] += x["label"]
        else:
            t["reviewed"] += 1
    t["cost"] = (t["wrong_approve"] * cost["wrong_approve"]
                 + t["wrong_deny"] * cost["wrong_deny"]
                 + t["reviewed"] * cost["review"])
    return t

def choose_band(items, cost, step=0.05):
    grid = [round(i * step, 2) for i in range(int(1 / step) + 1)]
    pairs = [(lo, hi) for lo in grid for hi in grid if hi > lo]
    lo, hi = min(pairs, key=lambda b: (band_cost(items, *b, cost)["cost"], b[1] - b[0]))
    return lo, hi

The same example also shows why the sample size matters. Fitted on only the first 100 items, the cheapest band never approves automatically and denies at or below 0.05; applied to the other 100, it costs 244 against 720 for the 0.5 cut. With a hundred labels there is not enough evidence to trust any automatic approval, and the search correctly says so. Fit the band on one sample, check it on another, and refit whenever the model, the question instructions or the mix of inputs changes.

Choice versus score, and why a score is not a level

Use choice when the options have no order and score when they do. The distinction matters because a score answer returns a weighted average that can fall between levels. In the example's score answer, the four severity levels have probabilities 0.1, 0.3, 0.5 and 0.1, and the score is 1.6. Rounding that to level 2 and paging on high severity looks reasonable, but the quantity you actually care about is the probability that severity is high or worse, which is 0.6, and that is what should be compared with a paging threshold. For a choice answer, gate on the probability of the chosen value rather than treating the choice as certain: the example sends a choice whose top value has probability 0.71 to review.

Batching questions and caching shared evidence

One request can ask up to 400 questions about the same evidence, which is cheaper and faster than one request per question when they share a long input such as a conversation or a document. The API reference adds prompt caching controls: a prompt_cache_key to partition reuse, prompt_cache_options whose only TTL value is currently 30 minutes, and explicit cache breakpoints on content parts. The usage object reports cached_tokens and cache_write_tokens, so you can confirm that caching is actually happening instead of assuming it.

Two cautions apply. Answers to many questions over the same evidence are not independent: if the evidence is misleading, they can all be wrong together, so do not treat agreement across questions as confirmation. And keep each question's instructions stable, because changing the wording of a question changes what its probability means, which invalidates the thresholds you fitted.

Evidence, conversations and untrusted input

The input can be a string or an ordered list of messages, including function calls and their outputs matched by call_id. JSON evidence goes in an input_json part, which is only accepted in a user message; the guide notes that keys such as role or image_url inside that data are ordinary data and do not create messages or fetch images. That is helpful, but it does not make the evidence trustworthy. Text inside a ticket or a tool output can still argue for an answer, so keep the question instructions in the developer message, treat the evidence as data, and keep the action under your own code, which the API's design already encourages.

Failure modes and verification

  • Thresholds without labels. A threshold chosen by intuition encodes neither your costs nor the model's calibration. Keep a labelled sample per question and refit on a schedule.
  • Refusals mapped to a default. Treating a refusal as no, or as zero, quietly turns a declined answer into a denial. Route refusals to review and count them.
  • Silent drift. The model, the instructions or your traffic can change the meaning of a probability. Track the share of items in the review band and the agreement between automatic decisions and later human outcomes.
  • Scores rounded to levels. Compare level probabilities with your threshold, as above.
  • Assuming one question per request is required. It is not, and it costs more; batch related questions over shared evidence.

The companion example, decision_threshold_calibration in Sandeep-BA/agent-decision-lab, has six tests covering the reliability table, the band, the holdout, the score threshold and refusal routing. It never calls the API; its response fixture was written by hand in the documented shape.

When the Decisions API is the wrong tool

If you need extracted fields, an explanation or any generated object, use Structured Outputs. If a deterministic rule decides the case, such as an amount under a fixed limit, do not ask a model at all. And if no one will ever label outcomes, you cannot fit or check thresholds, so the probabilities are decoration; fix that before automating anything on them.

Sources and scope

Checked against these sources on 26 October 2026. Figures come from the offline companion example with a synthetic labelled sample; no provider call was made.

Treat a Decisions answer as evidence, not an action: check calibration on labelled examples, choose an approve, review and deny band from your own error costs, compare level probabilities rather than a rounded score, route refusals to review, and refit whenever the model, the question or the input mix changes.