Updated 2026-11-12, corrected the same day. OpenAI's Decisions API is generally available: up to 400 typed questions to POST /v1/decisions on gpt-6-luna, answered as probabilities, choices and scores [1]. Its rates are in a guide, not on the price list, and output is free. What moves by a factor of 400 is how you ask.
POST /v1/decisions
gpt-6-luna
# Copy-paste starter: what one decision costs at the published token rates. # Swap in the token counts you measured and the tier you actually call. python3 cost_per_decision.py --input-tokens 900 --output-tokens 40
The docs are unusually clear about the shape, so start there rather than with a framework:
predicate
probability
choice
score
refusal
type: "refusal"
name
The refusal case is not an edge you can ignore, because every code sample in the guide branches on it before it reads a value, in each language: if (answer.type === "refusal"), if answer.type == "refusal", DecisionAnswerRefusal, answer.isRefusal(). A refusal carries no probability or score, so a loop that reads answer["probability"] without checking the type will fail on exactly the requests you most want logged.
if (answer.type === "refusal")
if answer.type == "refusal"
DecisionAnswerRefusal
answer.isRefusal()
answer["probability"]
The documentation adds the distinction that decides which one you use: "Both choice and score return probabilities over discrete options. Use choice for categories without an order, such as departments. Use score for ordered levels, such as severity" [1]. Getting that wrong is the difference between a number you can average and a number you cannot.
The rest of the surface, quoted: "Send requests to POST /v1/decisions using gpt-6-luna, the only currently supported model"; "The response contains an answers array. Give each question a unique name to identify its answer; the API echoes that name in the response"; "Ask up to 400 questions per request, up from 200 during the public beta"; prompt caching "to reuse shared input across requests and receive cache discounts"; images accepted in question instructions, as inline base64 data URLs or publicly accessible HTTP or HTTPS URLs; structured data through an input_json content part; and conversation history, so messages and tool calls can be evaluated together [1]. One hard limit is worth knowing before you design around file uploads: "This endpoint doesn't support file_id inputs" [1].
answers
input_json
file_id
The maturity claim is theirs and it is about latency, not price: "returns typed answers about 10x faster than the Responses API" [1].
These are the fragments the documentation publishes. A predicate question with instructions long enough to matter looks like this [1]:
{ "type": "predicate", "name": "visible_damage", "instructions": "Does the product have visible damage, such as a crack, tear, or dent? Ignore shadows and damage to the packaging." }
The evidence you send sits in typed content parts, and this is the part people get wrong first, because the difference between text and an image is one string [1]:
{ "type": "input_text", "text": "Evaluate the ticket using the order evidence." }
{ "type": "input_image", "image_url": "data:image/png;base64,$IMAGE_BASE64" }
And the answer comes back keyed by the name you chose, with the probability attached [1]:
{ "name": "visible_damage", "probability": 0.92 }
That 0.92 is where the work starts, not where it ends. The documentation says as much in its own words: "The probability is the model's estimate that the condition is true. Use it to flag photos for review based on a threshold you choose" [1].
0.92
I read the pricing page looking for this endpoint and it is not there. The word "decision" appears zero times: no row, no tab, no footnote, no anchor. That matters, because it is the page a buyer opens to compare costs, and it is where every other OpenAI price lives [2].
The price does exist, in the guide, under its own heading: "With gpt-6-luna, uncached input costs $0.10 per 1M tokens. Prompt cache hits receive discounted input pricing; cache writes are charged. Cache-read and cache-write rates match the standard gpt-6-luna prompt caching rates. Output tokens are not charged." The same paragraph adds that "These rates apply to /v1/decisions; other requests using gpt-6-luna follow the applicable model and processing-tier pricing", and warns that "Regional processing premiums and long-context input pricing multipliers apply" [1]. So the endpoint has its own rate card, and it is not the one on the pricing page:
/v1/decisions
An earlier version of this article priced the endpoint off the pricing page's model tiers, which charge $0.50 per million output tokens, and quoted the batch tier as an option. Both were wrong for this endpoint: output is free here, and /v1/decisions is not on the Batch API's endpoint list [6]. Correcting that changed every number below, which is why the script reads its rates from the guide rather than from the page you find first.
Google shipped the same idea one day before this was written, and did the opposite thing with the paperwork: it put the endpoint on its pricing page. The Gemini Decisions API, announced 2026-11-11, has a pricing row for gemini-decision-1 at $0.10 per 1M input tokens with zero output tokens billed, its own rate-limit table, and a published error body. Its guide states the mechanism in one sentence: "Your shared input is billed once per request no matter how many questions you evaluate (each question adds only its own instruction and criteria tokens). There are no output-token charges, variable thinking-token bills, or separate audio transcription and OCR costs" [5]. It takes up to 1,000 questions in a single pass over text, images, audio, video and PDFs, and quotes 150 to 250 ms end-to-end latency [5].
gemini-decision-1
So the two now agree on the two things that decide the bill: input is billed once per request, and output is free. Where they differ is the ceiling and the paperwork. Google takes 1,000 questions per request against OpenAI's 400, and a buyer comparing costs finds Google's rate on the pricing page and OpenAI's in a guide [1][5]. Both sets of figures are each vendor's own claims about its own model, published a day apart, and I have not measured either on live traffic.
Two things follow, and the first is not the one I first wrote down. The number exists and is easy to apply once found; the problem is where it lives, because a page that prices a model in service tiers and says nothing about the endpoint is exactly where a cost comparison goes wrong. The second is that a decision still has no fixed unit price even now: input is billed once per request and shared across every question in it, so the same 400 decisions cost very different amounts depending on how they are asked. That is what the next script measures.
The script prices a request from the token counts you measured, at the rates the guide gives for this endpoint, and divides by the number of decisions that request carried. It never adds an output charge, because there is not one.
#!/usr/bin/env python3 """cost_per_decision.py - what one decision costs, at the endpoint's own rates. The Decisions guide states the rates for /v1/decisions: uncached input at $0.10 per 1M tokens, cache reads at the standard prompt-caching rate, cache writes charged, and no charge for output tokens. The pricing page, which is where you would look first, has no Decisions row at all. """ import argparse, json UNCACHED_INPUT = 0.10 # USD per 1M input tokens, the rate the guide gives for /v1/decisions CACHED_INPUT = 0.01 # USD per 1M, the standard prompt-caching read rate def main(): p = argparse.ArgumentParser() p.add_argument("--input-tokens", type=float, required=True) p.add_argument("--cached-input-tokens", type=float, default=0.0) p.add_argument("--decisions-per-request", type=int, default=1) a = p.parse_args() if a.decisions_per_request < 1: raise SystemExit("--decisions-per-request must be at least 1") if a.cached_input_tokens > a.input_tokens: raise SystemExit("--cached-input-tokens cannot exceed --input-tokens") fresh = a.input_tokens - a.cached_input_tokens request_cost = (fresh / 1e6) * UNCACHED_INPUT + (a.cached_input_tokens / 1e6) * CACHED_INPUT per_decision = request_cost / a.decisions_per_request print(json.dumps({ "request_usd": round(request_cost, 8), "per_decision_usd": per_decision, "per_million_decisions_usd": round(per_decision * 1e6, 3), "output_tokens_charged": 0, }, indent=2)) if __name__ == "__main__": main()
Run it three ways and the levers appear. One question in a request, with the evidence it needs:
per million decisions: $90.000
The same evidence, asked 400 questions in one request:
per million decisions: $0.225
And with four fifths of that input served from cache, still 400 decisions:
per million decisions: $0.063
Read those together, because the spread is the point. $90 per million decisions becomes $0.225 per million when 400 questions share one request, and $0.063 when most of that input is a cache read. The ratio between the first and second is exactly 400, and it is not a discount OpenAI granted: it is the same rate card, applied to a request where the evidence is paid for once and divided 400 ways instead of once per decision. A quote of "cost per decision" that does not say how many questions shared the request is not a price, it is a question.
There is a practical catch, and it is the honest half of this recipe: putting 400 questions in one request assumes the questions share their evidence. If each decision needs its own document, you have 400 inputs and the advantage is gone. There is also no overnight route to a discount here, because /v1/decisions is not among the endpoints the Batch API documents [6]. So the number to measure on your own traffic is not the token rate you can read off a page, it is the ratio of shared input to per-decision input.
Two things go wrong when this moves from a script to production: questions get long enough that the request stops being one request, and somebody ships the default threshold. Both are testable before deployment.
The packer turns a CSV of questions into request payloads, and it will not let an invalid question through:
#!/usr/bin/env python3 """build_questions.py - turn a CSV of questions into request payloads (limit 400).""" import csv, json, sys MAX_QUESTIONS = 400 VALID = {"predicate", "choice", "score"} def load(path): rows = list(csv.DictReader(open(path))) if not rows: raise SystemExit(f"{path}: no questions; nothing to send") out = [] for r in rows: t = r["type"].strip().lower() if t not in VALID: raise ValueError(f"question {r['name']!r}: unknown type {t!r}; expected one of {sorted(VALID)}") q = {"type": t, "name": r["name"], "instructions": r["instructions"]} opts = [o.strip() for o in (r.get("options") or "").split("|") if o.strip()] if t in ("choice", "score") and not opts: raise ValueError(f"question {r['name']!r}: type {t} needs options, separated by |") if t == "choice": q["choices"] = opts elif t == "score": q["levels"] = opts out.append(q) return out def main(path="questions.csv"): qs = load(sys.argv[1] if len(sys.argv) > 1 else path) batches = [qs[i:i + MAX_QUESTIONS] for i in range(0, len(qs), MAX_QUESTIONS)] # one request each for n, batch in enumerate(batches, 1): with open(f"batch-{n}.json", "w") as fh: json.dump({"model": "gpt-6-luna", "input": "REPLACE WITH YOUR EVIDENCE", "questions": batch}, fh, indent=2) print(f" questions: {len(qs)} batches: {len(batches)} per batch: " f"{[len(b) for b in batches]}") if __name__ == "__main__": main()
The documented limit is the one it splits on, so a 401-question list becomes two requests rather than an error from the API:
questions: 401 batches: 2 per batch: [400, 1]
Then the threshold. A predicate gives you a probability, and the 0.5 you would reach for is a convention rather than a calibrated cutoff for your task. The sweep reads labeled rows and prints what each cutoff does:
#!/usr/bin/env python3 """threshold_sweep.py - pick a cutoff from labeled rows, not from the default.""" import csv, sys def sweep(rows, thresholds): out = [] for t in thresholds: predicted = [r for r in rows if r["probability"] >= t] tp = sum(1 for r in predicted if r["truth"] == "yes") fn = sum(1 for r in rows if r["truth"] == "yes") - tp out.append({"threshold": t, "flagged": len(predicted), "precision": round(tp / len(predicted), 3) if predicted else None, "missed_events_pct": round(fn / max(1, tp + fn) * 100, 1)}) return out def main(path="labeled.csv"): path = sys.argv[1] if len(sys.argv) > 1 else path rows = list(csv.DictReader(open(path))) if not rows: raise SystemExit(f"{path}: no labeled rows") for r in rows: r["probability"] = float(r["probability"]) if r["truth"] not in ("yes", "no"): raise ValueError(f"row {r['id']}: truth must be yes or no, got {r['truth']!r}") print(" threshold flagged precision missed events") for r in sweep(rows, [0.2, 0.3, 0.5, 0.7]): print(f" {r['threshold']:<10} {r['flagged']:<8} {str(r['precision']):<10} {r['missed_events_pct']}%") if __name__ == "__main__": main()
On a small synthetic labeled file, which is an illustration of the trade-off rather than a measurement of anything, it prints:
threshold flagged precision missed events 0.2 107 0.252 10.0% 0.3 96 0.271 13.3% 0.5 73 0.288 30.0% 0.7 31 0.194 80.0%
The shape is what to take from it: dropping the cutoff from 0.7 to 0.3 triples the events you catch while the precision of what you flag barely moves. Where that curve sits is task-specific, which is exactly why it has to be measured on your own labeled rows rather than inherited from a default.
For a real number on how badly the default can behave, the most useful published measurement is on a sibling model rather than this one. An AWS solutions architect measured a decision model on security-camera frames and reported that at the 0.5 default it "leaned hard toward 'no' and missed 43% of actual events", with a useful range of 0.2 to 0.3, and 85.2% accuracy against 88.0% for the best vision LLMs on the same set [3]. That is a different model from gpt-6-luna and it is not a claim about this endpoint, but it is the same failure mode, and it is the reason the threshold belongs in a script you can rerun rather than in a constant nobody revisits.
The guards in the packer and the sweep, run rather than described:
ValueError: question 'x': unknown type 'noul'; expected one of ['choice', 'predicate', 'score']
ValueError: question 'dept': type choice needs options, separated by |
ValueError: row f1: truth must be yes or no, got 'maybe'
empty.csv: no labeled rows
cost_per_decision.py: error: the following arguments are required: --output-tokens
The reference also documents no endpoint-specific error codes for /v1/decisions, and the error page does not mention the endpoint, so the general table is what you code against [1].
And the authentication error, which is the one you will meet first. An unauthenticated POST /v1/decisions from this machine returned HTTP 401 with this body:
{ "error": { "message": "Invalid runtime agent assertion.", "type": null, "code": "invalid_agent_instance_assertion", "param": null }, "status": 401 }
Treat that message as what it is, a body returned to a request that carried no credentials from this particular network path, and read your own client's error rather than trusting a transcript. The status field and the nested error object are the shape to code against.
status
error
The endpoint is one model, gpt-6-luna, so there is no cheaper model to fall back to inside the API, and no file_id inputs, which rules out the workflow where you upload a document once and ask questions of it later [1]. It answers typed questions about an input; it does not summarise, draft or explain. If your task needs an explanation the customer can read, this is the wrong shape and you want a text model.
Two independent measurements exist for this endpoint family, and they disagree in a useful way.
The first is on tagging: an engineer who built a labeling product measured the OpenAI decisions API against alternatives on a hand-labeled dataset of 3,671 holdout items, and reported 0.766 macro F1 at $19.20 per million billed decisions, against 0.91 for a 4B fine-tune at $6.10 and 0.894 for a 33M distilled classifier at $0.09 [7]. His measurement also reports 231 of those 3,671 items refused and scored as uniform guesses, and an expected calibration error of 0.089, which he labels overconfident [7]. Those are his numbers on his dataset, not a general result, and his own conclusion is that labeling your data beats choosing a model.
One cross-check that the two sources agree on the rate card: $19.20 per million decisions at $0.10 per million input tokens implies about 192 input tokens per decision, which is the right order for the short classification prompts he used. My $90 figure above comes from a 900-token document per decision, and a third party measuring small prompts should land near $19, not near $90. It does.
The second measurement is on images rather than text, and it is about a different model, so treat it as the shape of the failure rather than a result about gpt-6-luna: on security-camera frames, a decision model at the 0.5 default "leaned hard toward 'no' and missed 43% of actual events", with a useful range of 0.2 to 0.3, and 85.2% accuracy against 88.0% for the best vision LLMs [3]. That author also recorded large gains from fine-tuning with no published image fine-tune to copy.
Then the quality caveat, which belongs here as much as in a cost article. A decision model is not automatically as good as the general model it replaces, and what the measurements above show consistently is that a bare zero-shot decision model loses to a model that learned your task, whether that is a fine-tune, a distilled classifier or just a few hundred labeled examples [3][7]. If your task is high-volume and narrow and you have labels, that gap is a trade worth making. If your task is low-volume and judgement-heavy, the token bill was never your problem.
Finally, the latency claim of "about 10x faster" is the vendor's own and is not a promise about your network path, your image sizes or your question count [1]. Measure it on your traffic, the same way the cost needs measuring.
The useful question for an operator is not which endpoint is cheap, it is which of your recurring decisions is a closed choice with stable options. A decision API turns that question into a testable one: pack the questions, run them against labeled rows, look at the curve, and only then look at the bill. The bill will be small at high volume and shared evidence, and the accuracy will be a number you chose a cutoff for rather than hoped for.
That pattern is the same one behind replacing a general model with a decision model, and it sits next to routing between models and what an agent costs when nobody reviews it. The organisational half matters as much as the technical half: deciding what an agent may approve without a person and where it fits among the automation you already run is the part that decides whether the cheaper endpoint ever reaches production.
We audit exactly that before writing anything: which recurring decisions are closed choices, what the labeled set looks like, and what accuracy you are willing to trade for. A written intake describing the task and its volume is enough to start.
[1] OpenAI, Decisions API guide, fetched 2026-11-12. Source of the generally-available statement, the endpoint and model, the three question types and their semantics, the answers array, the 400-question limit, prompt caching, image and JSON inputs, the file_id limitation, the "about 10x faster" claim, and the request and response fragments quoted above.
[2] OpenAI, API pricing, fetched 2026-11-12. Source of the gpt-6-luna token rates in the standard, batch and fast tiers, and of the absence of any Decisions row, verified by reading the page rather than inferred from the guide.
[3] Nick Ihm, "Classifying images with Strands Decider 2B decision model", AWS Builder Center, published 2026-11-10, fetched 2026-11-11. Source of the 85.2% against 88.0% comparison, the 74.6% at the 0.5 default, the 43% miss, the 0.2 to 0.3 range, and the statement that no image fine-tune was published. About a different model and not a claim about this endpoint, used here for the threshold failure mode it documents. Carries the author's own disclaimer.
[5] Google, "Gemini Decisions API: Fast, calibrated multimodal decisions", 2026-11-11, by Min Choi and Felix Yu of Google DeepMind, fetched 2026-11-12. Source of the once-per-request billing rule, the $0.10 input and $0 output pricing, the 1,000-question limit, the 150 to 250 ms latency figure, and the multimodal input list. Vendor's own claims about its own model.
[7] sao.dev, "Decision Models Need Labels", page dated 2026.10.15, fetched 2026-11-12. Independent measurement of the OpenAI decisions API on a hand-labeled tagging dataset: 0.766 macro F1 at $19.20 per million billed decisions against a 4B LoRA at 0.91 and $6.10 and a distilled 33M classifier at 0.894 and $0.09, 231 of 3,671 holdout items refused, ECE 0.089. The author's dataset and conclusion, not a general result.
[6] OpenAI, Batch API guide, fetched 2026-11-12. Read to test whether a batch rate could be applied to this endpoint: /v1/decisions is absent from the endpoint list and the word "decisions" does not appear in the guide.
[4] Strands Agents, "Amazon saves millions by replacing Sonnet 4.6 with Strands Decider", 2026-11-09, fetched 2026-11-11. The self-hosted counterpart, used for the comparison of what a decision costs when you pay for an instance by the hour.
Maxpertise is an AI-native engineering company. We embed native AI engineers inside your team, live in about 10 days. Please enable JavaScript to view the site, or email contact@maxpertise.net.