On October 6, 2026, OpenAI put the Decisions API into public beta: a POST /v1/decisions endpoint that reads text, images or both and returns typed answers — a probability, a pick from your options, or a numeric score — instead of generated text. OpenAI’s guide says it is about 10x faster than the Responses API.
Five days earlier Cloudflare released Clef, open-weight decision models under Apache 2.0, so there is now an open counterpart. I did not call the API for this post: what follows is what the docs say, plus the harness I would use to measure what they do not.
1. The launch facts
OpenAI’s API changelog lists the release under October 6, 2026 with the line “Turn text and images into typed answers 10x faster than the Responses API.” The guide marks the endpoint as public beta, expects general availability “in the coming weeks”, and names gpt-6-luna as the only supported model.
On OpenAI’s models page, gpt-6-luna is described as its most efficient model for focused, high-volume tasks, with Chat Completions, Responses and Batch as its listed endpoints. /v1/decisions is not on that list, so I would not assume that chat-model prices or rate limits carry over to it.
Spec sheet
- • Status: public beta since Oct 6, 2026 (OpenAI changelog); GA “in the coming weeks” per the guide
- • Endpoint: POST /v1/decisions · model: gpt-6-luna, the only supported value
- • Questions: predicate (probability), choice (one of your values + probabilities + confidence), score (probability-weighted level index)
- • Price: $0.10 per 1M input tokens; no output, cache-read or cache-write charges; regional-processing and long-context multipliers apply
- • Input: text, or text + images as inline base64 data URLs; there is no free-text output
- • Data: Zero Data Retention and HIPAA for eligible customers; US and EEA + Switzerland residency
2. What it changes versus chat + Structured Outputs
Structured Outputs and function calling make a chat model write JSON that matches your schema or tool arguments. OpenAI’s guide contrasts Decisions with both: you declare the questions, the endpoint scores your options, and you get typed answers with probabilities. There is no text generation, no tool call and no reasoning trace.
My read, not OpenAI’s: the gain is architectural. A chat call decodes tokens one by one and bills output; a decision call reads the input once, answers every question against it and bills input only. If your step is “which of these N things is it”, you were paying for a paragraph you then threw away. If you read my Jev posts the shape will look familiar (yes/no, choice and score over shared input, with Jev calling the yes/no type noul), but vocabulary is similar, not identical, and vendors, limits and contracts differ, so I would not treat them as interchangeable.
The three question kinds
predicate
A probability that a statement about the input is true. The answer carries probability only, with no confidence field.
choice
Pick one of 2–255 unique values you define (strings or booleans). Returns the choice, a probability per option and a confidence.
score
Ordered levels, lowest first. Returns the probability-weighted average of the level indexes (from 0), plus the distribution and a confidence.
Where it beats chat + structured outputs
- • Closed answer space: routing, triage, moderation gates, relevance filters, yes/no policy checks
- • High volume, short inputs: only input tokens are billed, and OpenAI claims ~10x lower latency than Responses
- • Thresholds: you get probabilities to branch on, not a bare label or a number the model wrote as text
- • Several judgments over one input in a single request (up to 200 questions per the API reference)
- • A choice can only be one of your values: a type guarantee, though not a correctness guarantee
Where it does not
- • Anything that must explain, draft, plan, call tools or reason: there is no free-text output
- • Field extraction (names, amounts, dates) is still Structured Outputs; exact arithmetic and date logic belong in code
- • Open answer space or variable schemas: if you cannot enumerate options, there is nothing to score
- • Determinism, calibration, endpoint rate limits and absolute latency are not documented in the guide, so you have to measure them
- • One vendor, one model id, a beta contract: portability is your job
Golden rule
If you can list the valid answers before you see the input, reach for a decision endpoint. If you cannot, you need a model that writes. Thresholds, permissions and side effects stay in code either way.
3. Quickstart: ticket routing in one call
Same ticket, three questions: does the customer ask for a refund (predicate), which team owns it (choice, with an other option as the guide recommends) and how urgent it is (score). The request and response shapes follow the guide and the API reference; the question names and levels are mine.
Request: three questions over one input
Plain HTTP with the documented body: model, input, questions. Set OPENAI_API_KEY. I did not run this.
import os
import requests
URL = "https://api.openai.com/v1/decisions"
HEADERS = {
"Authorization": "Bearer " + os.environ["OPENAI_API_KEY"],
"Content-Type": "application/json",
}
QUESTIONS = [
{"type": "predicate", "name": "refund_requested",
"instructions": "Does the customer explicitly ask for a refund?"},
{"type": "choice", "name": "team",
"instructions": "Which team should handle this ticket?",
"choices": [
{"value": "billing", "description": "Payments, invoices, refunds."},
{"value": "technical", "description": "Bugs, errors, outages."},
{"value": "shipping", "description": "Delivery and damaged items."},
{"value": "other", "description": "None of the above fits."},
]},
{"type": "score", "name": "urgency",
"instructions": "How urgent is this ticket?",
"levels": [
{"label": "Can wait", "description": "No time pressure."},
{"label": "This week", "description": "Needs a reply soon."},
{"label": "Today", "description": "Customer is blocked now."},
]},
]
def decide(text, timeout=10):
body = {"model": "gpt-6-luna", "input": text, "questions": QUESTIONS}
r = requests.post(URL, headers=HEADERS, json=body, timeout=timeout)
r.raise_for_status()
return {a["name"]: a for a in r.json()["answers"]}Response shape (a sketch, not real output)
Answers come back in question order. Every value below is a placeholder, not a measured result.
{
"answers": [ // same order as your questions
{ "type": "predicate", "name": "refund_requested", "probability": <0..1> },
{ "type": "choice", "name": "team", "choice": "<one of your values>",
"confidence": <0..1>,
"probabilities": [ { "value": "billing", "probability": <0..1> }, ... ] },
{ "type": "score", "name": "urgency", "score": <weighted level index, from 0>,
"confidence": <0..1>,
"probabilities": [ { "value": 0, "label": "Can wait", "probability": <0..1> }, ... ] }
// any question may instead return { "type": "refusal", "name": "..." }
],
"model": "<string>",
"usage": { "input_tokens": <n>, "output_tokens": <n>, "total_tokens": <n>, ... }
}Policy gate in your code
The thresholds are placeholders until section 4 sets them from your labelled data.
def route(text, t_refund=0.85, t_team=0.80, t_urgent=1.5):
a = decide(text)
refund, team, urgency = a["refund_requested"], a["team"], a["urgency"]
if "refusal" in (refund["type"], team["type"], urgency["type"]):
return "human_review" # a question can be refused
if team["choice"] == "other" or team["confidence"] < t_team:
return "human_review" # closed set did not fit, or unsure
if refund["probability"] >= t_refund:
return "refund_checks" # code verifies amount, identity, limits
if urgency["score"] >= t_urgent:
return team["choice"] + ":priority"
return team["choice"]Two details from the reference worth wiring in on day one: a question can come back as type refusal while the others are answered normally, and names are optional, so name every question and match answers by name instead of position.
Cost back-of-envelope
Cost is input tokens × $0.10 / 1M. If a ticket plus the question definitions weighs about 500 input tokens (my assumption), that is $0.00005 per ticket, around $50 per million tickets before regional or long-context multipliers. Read usage.input_tokens from real responses before trusting that estimate.
4. Evaluate it before you trust it
OpenAI’s guide says to set thresholds for routing, filtering or review from labelled examples of your own application, weighing false positives against false negatives. It publishes no accuracy comparison against the Responses API, so the evaluation is yours: a few hundred labelled tickets, a frozen question set and the measurements below.
Run the same labelled set through every candidate (rules baseline, your current chat + Structured Outputs call, this endpoint, and Clef if you want an open option). Compare per-class precision and recall at your operating threshold, calibration, p50/p95 latency and review volume. Accuracy alone hides the failures that cost money.
Confusion matrix and latency
Labelled JSONL in, per-class precision and recall out. Refusals count as misses, which is how you should treat them.
import json
import time
from collections import Counter, defaultdict
def collect(path):
rows, lat = [], []
for line in open(path, encoding="utf-8"):
ex = json.loads(line) # {"text": ..., "team": ..., "refund": true/false}
t0 = time.perf_counter()
rows.append((ex, decide(ex["text"])))
lat.append((time.perf_counter() - t0) * 1000)
lat.sort()
print("p50 ms", lat[len(lat) // 2], "p95 ms", lat[int(len(lat) * 0.95)])
return rows
def confusion(rows, labels):
m = defaultdict(Counter)
for ex, a in rows:
t = a["team"]
m[ex["team"]][t["choice"] if t["type"] == "choice" else "REFUSAL"] += 1
for gold in labels:
tp = m[gold][gold]
fn = sum(m[gold].values()) - tp
fp = sum(m[g][gold] for g in labels if g != gold)
p = tp / (tp + fp) if tp + fp else 0.0
r = tp / (tp + fn) if tp + fn else 0.0
print(f"{gold:10s} precision={p:.2f} recall={r:.2f} n={tp + fn}")
return mThreshold sweep and calibration error
Pick the lowest threshold that meets your precision target on one half, then report on the held-out half.
def sweep(rows, target_precision=0.95):
best = None
for k in range(1, 20):
t = k / 20
tp = fp = fn = 0
for ex, a in rows:
q = a["refund_requested"]
hit = q["type"] == "predicate" and q["probability"] >= t
tp += hit and ex["refund"]
fp += hit and not ex["refund"]
fn += (not hit) and ex["refund"]
p = tp / (tp + fp) if tp + fp else 1.0
r = tp / (tp + fn) if tp + fn else 0.0
print(f"t={t:.2f} precision={p:.2f} recall={r:.2f}")
if best is None and p >= target_precision:
best = t
return best # lowest threshold that meets your precision target
def ece(pairs, bins=10): # pairs: (stated probability, was it right?)
buckets = [[] for _ in range(bins)]
for conf, ok in pairs:
buckets[min(int(conf * bins), bins - 1)].append((conf, ok))
total = 0.0
for b in buckets:
if b:
gap = abs(sum(c for c, _ in b) / len(b) - sum(o for _, o in b) / len(b))
total += len(b) / len(pairs) * gap
return total
# pairs for the choice question, built from rows:
# (max(p["probability"] for p in a["team"]["probabilities"]), a["team"]["choice"] == ex["team"])Measurement checklist
- 1. Split the labelled set: tune thresholds on one half, report only on the other.
- 2. Read the confusion matrix for team, including the other class and refusals; that is where routing errors hide.
- 3. Sweep the refund threshold and pick the point set by the cost of a wrong refund versus a missed one.
- 4. Compute calibration error on the top choice probability and on confidence separately; both are returned and they are not the same number.
- 5. Send every example twice and diff the answers: the guide documents neither determinism nor calibration, so measure both.
Do not skip the baseline
A keyword rule set plus a small supervised classifier often matches a hosted judge on narrow, stable categories. If the rules baseline is inside your error budget, the endpoint is a cost, not an upgrade.
5. Beta limits, fallbacks and the open counterpart
Beta means the contract can move. The guide expects GA in the coming weeks, the model id is a single value, and I found no rate limit documented for this endpoint. The gpt-6-luna page lists per-tier RPM and TPM, but it does not list /v1/decisions among its endpoints, so confirm your own limits page before a load test.
Honest limits
- • Beta: shapes and limits may change before GA; pin your client and log the response model field.
- • Single vendor, single model id: no open weights and no fine-tuning documented in the guide.
- • No free text, so no explanation to show a reviewer; log the probabilities instead.
- • Images: the guide says inline base64 only, while the TypeScript reference also mentions public HTTP(S) URLs. I would send base64 and wait for GA to settle it.
- • Probabilities are estimates: the guide does not claim calibration or determinism.
- • A question can be refused, so your code needs a refusal branch on every call.
Fallback ladder
- 1. Short timeout and bounded retries with backoff on 429 and 5xx; honor Retry-After when present (OpenAI’s help center says its SDKs do).
- 2. Circuit breaker: after a few failures, skip the call for a minute instead of hammering a struggling endpoint.
- 3. A secondary decision model behind an adapter: Clef on Workers AI or self-hosted, Jev, or a gateway that normalizes them (Vercel documents an OpenAI-compatible /decisions endpoint with confidence-based fallbacks).
- 4. A deterministic rules baseline for the cheap, obvious cases.
- 5. A human review queue as the last rung: a fallback that blocks is worse than a slow one.
Bounded retries and a circuit breaker
A sketch of the first two rungs. The rest are your adapters, and the caller treats None as “go down the ladder”.
import random
import time
import requests
RETRYABLE = {429, 500, 502, 503, 504}
breaker = {"fails": 0, "until": 0.0}
def decide_resilient(text, tries=3):
if time.time() < breaker["until"]:
return None # breaker open: skip the call
for attempt in range(tries):
try:
answers = decide(text, timeout=5)
breaker["fails"] = 0
return answers
except requests.HTTPError as e:
if e.response.status_code not in RETRYABLE:
raise # 400/401/403: fix the request, do not retry
wait = e.response.headers.get("Retry-After", "")
time.sleep(float(wait) if wait.isdigit() else 2 ** attempt + random.random())
except (requests.Timeout, requests.ConnectionError):
time.sleep(2 ** attempt + random.random())
breaker["fails"] += 1
if breaker["fails"] >= 3:
breaker.update(fails=0, until=time.time() + 60)
return None # caller falls to the next rung: secondary model, rules, humanCloudflare Clef: the open-weight counterpart
- • Released Oct 1, 2026: Clef (larger) and Clef-flash (latency-focused); weights on Hugging Face under Apache 2.0.
- • Hosted on Workers AI as @cf/cloudflare/clef; the model page lists $0.24 per 1M input tokens, output not charged.
- • Clef is built on Qwen3.8-27B per Cloudflare’s post, with a vision encoder and a 64k context window.
- • Request: a state plus a questions object keyed by id, up to 64 questions and 4 images; remote image URLs are not accepted.
- • Cloudflare’s latency table (median 209.3 ms for Clef, 38.8 ms for Clef-flash across 43 benchmarks) comes from its own Jev Decision Index run: vendor-run, so re-measure.
Same idea, different contract. Cloudflare’s model page names the yes/no type noul, not predicate, and answers come back keyed by question id, so this is an adapter job, not a base-URL swap. Cloudflare also says it does not read, store or train on your requests or responses, except for customers who opt into fine-tuning.
Data handling, as documented
The guide says the Decisions API supports Zero Data Retention and HIPAA use for eligible customers, and that data residency and regional processing are available in the United States and Europe (EEA + Switzerland), with regional-processing premiums. Eligibility and required agreements live in OpenAI’s data controls guide, which I did not reproduce. The reference also accepts an optional safety_identifier for end users. My advice, not OpenAI’s: redact ticket PII you do not need before sending, as you would for any hosted model.
Verdict
As documented, the Decisions API is a clean primitive: three question kinds, typed answers, no text to parse and billing on input tokens only. For closed-answer, high-volume judgments it is an endpoint built for exactly that job. The 10x claim is plausible for a call that never decodes text, but OpenAI publishes no absolute latency or accuracy figure.
My call: put it behind an adapter, run the harness against your rules baseline and your current Structured Outputs call, ship in shadow mode with humans on the uncertain band, and re-check when GA lands. Keep chat models for everything that has to write, and treat Clef or Jev as your exit ramp.
Also on this blog
Sources for this post
Primary documentation



