Decisions

OpenAI Decisions API

Ask GPT-6 Luna Decisions classification and scoring questions about text, images or JSON, and get typed answers your code can act on: how likely a statement is true, which of your values fits, or where it sits on your scale, each with its probabilities. Many questions in one request, usually in under a second, through OpenAI’s own Decisions API and SDKs.

POST/v1/decisions
ModelIDInput, per 1M tokensOpenAI’s priceOutput
GPT-6 Luna Decisionsgpt-6-luna-decisions$0.04$0.10Free

GPT-6 Luna Decisions is OpenAI’s decision model. It reads your input and answers the questions you set about it, and it writes no text, so it isn’t a chat model: send it to /v1/decisions, not /v1/chat/completions. On this endpoint gpt-6-luna works as the model ID too, as in OpenAI’s own SDK docs, so OpenAI’s sample code runs unchanged; the reply’s model says gpt-6-luna-decisions. For Jev, TypeSafe’s decision model, see Decisions (Jev).

Ask questions

# pip install --upgrade openai
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.zurelay.com/v1",
api_key=os.environ["ZURELAY_API_KEY"],
)
decision = client.decisions.create(
model="gpt-6-luna-decisions",
input="My headphones stopped charging two days after they arrived. I want a refund.",
questions=[
{
"type": "predicate",
"name": "refund",
"instructions": "Is the customer asking for a refund?",
},
{
"type": "choice",
"name": "topic",
"instructions": "What is the ticket about?",
"choices": [
{"value": "billing", "description": "Charges and invoices"},
{"value": "hardware", "description": "A device that is broken or faulty"},
{"value": "shipping", "description": "Delivery and tracking"},
],
},
{
"type": "score",
"name": "sentiment",
"instructions": "How does the customer feel?",
"levels": [
{"label": "very negative"},
{"label": "negative"},
{"label": "neutral"},
],
},
],
)
# One answer per question, in the order asked
for answer in decision.answers:
if answer.type == "refusal":
print(answer.name, "refused")
elif answer.type == "predicate":
print(answer.name, answer.probability)
elif answer.type == "choice":
print(answer.name, answer.choice, answer.confidence)
else:
print(answer.name, answer.score, answer.confidence)
print(decision.usage.input_tokens, "input tokens")

Point OpenAI’s SDK at https://api.zurelay.com/v1, with /v1 as for every OpenAI model on Zurelay, use your Zurelay key, and call client.decisions.create. In Node.js that needs openai 7.30.0 or later; in Python, an openai package recent enough to have client.decisions (3.41 has it). Any HTTP client works the same way: POST the JSON with your key as a Bearer token.

The answer

Response
{
"id": "req_3Fh8sLq0vW2nTc7yRb5kZp1M",
"model": "gpt-6-luna-decisions",
"answers": [
{ "type": "predicate", "name": "refund", "probability": 1 },
{
"type": "choice",
"name": "topic",
"choice": "hardware",
"confidence": 1,
"probabilities": [
{ "value": "billing", "probability": 0 },
{ "value": "hardware", "probability": 1 },
{ "value": "shipping", "probability": 0 }
]
},
{
"type": "score",
"name": "sentiment",
"score": 0.18,
"confidence": 0.85,
"probabilities": [
{ "value": 0, "label": "very negative", "probability": 0.82 },
{ "value": 1, "label": "negative", "probability": 0.18 },
{ "value": 2, "label": "neutral", "probability": 0 }
]
}
],
"usage": {
"input_tokens": 325,
"input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
"output_tokens": 0,
"output_tokens_details": { "reasoning_tokens": 0 },
"total_tokens": 325,
"cost": 0.000013
}
}

answers has one answer per question, in the order you asked them, each with its type and, when you gave one, its name. usage is what the request was billed on: input_tokens, with output_tokens always 0, and cost what it cost in US dollars. The reply’s x-request-id header finds it in your request log, and x-zurelay-input-tokens repeats what it was billed on.

Parameters

modelstringrequired
gpt-6-luna-decisions, or gpt-6-luna on this endpoint.
inputstring or arrayrequired
What to decide about: a non-empty text, or an array of user messages, { "role": "user", "content": ... } ("type": "message" is optional). A message’s content is a text or an array of parts, below.
questionsarrayrequired
1 to 64 questions, answered in the order you list them.
safety_identifierstring
Optional: an opaque id for the end user the request is for, such as a hash of your own user id.
prompt_cache_keystring
Accepted and ignored, as is prompt_cache_options: caching is automatic, and every input token costs the same.
Each question
typestringrequired
predicate, choice or score.
instructionsstringrequired
The question, as text. Images go in the input, not here.
namestring
Optional, and unique within the request: comes back on the question’s answer, so you can read answers by name.
choicesarray
For a choice (required): 2 to 255 values, each { "value": ..., "description": ... }. A value is text or true/false, and each must be unique; the description is optional.
levelsarray
For a score (required): 2 to 10 levels, lowest first, each { "label": ..., "description": ... }, the description optional.
Parts of a message’s content
input_text{ text }
Text.
input_image{ image_url, detail }
A PNG, JPEG, WebP or GIF image: a base64 data URL, or a public https link that Zurelay downloads. detail is optional: low, high, auto or original.
input_json{ data }
An object or an array, such as a record or your app’s state. The model reads it as compact JSON text, and it’s counted as that text.

Question types

TypeAsksAnswer
predicateHow likely a statement about the input is trueprobability, from 0 to 1
choiceWhich of your 2 to 255 values fitschoice, a confidence, and the probabilities of every value
scoreWhere it sits on your 2 to 10 ordered levelsscore, a confidence, and the probabilities of every level

One request can mix all three. The input is read once for all of them, so asking several questions in one request costs less than asking each in a request of its own.

Predicate

A statement or yes/no question about the input. The answer’s probability is how likely it is true: near 1 for yes, near 0 for no, and in between when the input doesn’t say clearly.

Question, then answer
{ "type": "predicate", "name": "refund", "instructions": "Is the customer asking for a refund?" }
{ "type": "predicate", "name": "refund", "probability": 1 }

Choice

Picks one of the values you list, and gives the probability of each. A description says what a value means, which helps when the value alone is a code. Values can be true and false too, and the text "true" and the boolean true are different values.

Question
{
"type": "choice",
"name": "language",
"instructions": "Which language is the message written in?",
"choices": [
{ "value": "en", "description": "English" },
{ "value": "es", "description": "Spanish" },
{ "value": "other", "description": "Any other language" }
]
}

Score

Places the input on your levels, lowest first. score is the expected level: each level’s index, counting from 0, weighted by its probability. So it can land between two levels: in the example response above, 0.18 is mostly the first level with a little of the second. Each entry in probabilities has the level’s index as value and its label.

Question
{
"type": "score",
"name": "relevance",
"instructions": "How well does the passage answer the search query?",
"levels": [
{ "label": "irrelevant" },
{ "label": "related", "description": "On the topic, but doesn't answer it" },
{ "label": "answers it" }
]
}

Images

Send images in the input, as parts of a user message beside any text. Give a base64 data URL, or a public https link, which Zurelay downloads and sends on: PNG, JPEG, WebP or GIF. Send big images as links.

import base64, os
from openai import OpenAI
client = OpenAI(base_url="https://api.zurelay.com/v1", api_key=os.environ["ZURELAY_API_KEY"])
with open("return-photo.jpg", "rb") as file:
photo = "data:image/jpeg;base64," + base64.b64encode(file.read()).decode()
decision = client.decisions.create(
model="gpt-6-luna-decisions",
input=[
{
"role": "user",
"content": [
{"type": "input_text", "text": "Photo sent with a return request for a blender."},
{"type": "input_image", "image_url": photo, "detail": "low"},
],
}
],
questions=[
{
"type": "predicate",
"name": "damaged",
"instructions": "Does the photo show visible damage to the product?",
},
{
"type": "choice",
"name": "product",
"instructions": "What product is in the photo?",
"choices": [
{"value": "blender"},
{"value": "kettle"},
{"value": "other", "description": "Anything else, or no product"},
],
},
],
)
print(decision.answers)

An image adds tokens by its size and detail: a 1200 × 630 picture is about 750 tokens at high or auto, and about 150 at low. Use low when the question is about the whole picture rather than fine print. Images go in the input only: a question’s instructions take text.

JSON input

Structured data can go in as it is, in an input_json part, beside text or images. The model reads it as compact JSON text, and it’s billed as that text.

Input with JSON
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "A sign-up from our app:" },
{
"type": "input_json",
"data": { "name": "Free Gift Card Giveaway", "email": "win-now@example.net", "bio": "Click my link" }
}
]
}
]

Refusals

Any question can come back as a refusal instead of an answer, while the others in the request are still answered. Check each answer’s type before you read its fields. In our tests, the input “Refund requested 38 hours ago.” with the predicate “Page on-call?” came back refused.

A refused question
{ "type": "refusal", "name": "page_on_call" }

A refusal comes in a normal reply, whose usage says what the request was billed on. Ask again with a clearer question, or send the case to a person.

Limits

  • Up to 1.05M tokens a request: the input and every question together. An input of 918K tokens worked in our tests.
  • 1 to 64 questions. A choice takes 2 to 255 unique values and a score 2 to 10 levels.
  • Only user messages: system, developer and assistant messages, and function or tool call items, are refused. Put context in a user message, or in a question’s instructions.
  • No streaming: stream is refused, and the answers come back whole.
  • A malformed request, or one with a field the API doesn’t take, is refused with a 400 that names the field (such as questions[2].choices) before it reaches the model, and costs nothing.
  • A key limited to some models needs gpt-6-luna-decisions on its list, also for requests that say gpt-6-luna. See Key limits and rules.

Speed

Answers usually come back in 0.3 to 1 second, and in about a second with an image, in our tests. A 900K-token input took about 10 seconds.

Paying for decisions

GPT-6 Luna Decisions costs $0.04 per 1M input tokens, and its answers are free. The tokens are your input, counted once, plus about 110 to 160 of the model’s own for each question: one short predicate on a short text is about 160 tokens in all, two about 320, and a 2,600-token input with four questions about 3,200. Images add their tokens as above. So a one-question check on a short text costs about $0.0000064, and a million of them about $6.40.

Each reply’s usage and x-zurelay-input-tokens header say what it was billed on, every request is in your request log, and requests that fail cost nothing.

Errors

StatusCodeWhen
400invalid_requestThe request is malformed, or uses something the API doesn’t take. The message says what and where.
400invalid_inputAn image link didn’t download, or isn’t a PNG, JPEG, WebP or GIF image.
400context_length_exceededThe input and questions come to more than 1.05M tokens. Send a shorter input.
503model_unavailable or service_unavailableThe model is briefly unavailable. Retry after the retry-after header’s seconds.

Keys, credit, budgets and rate limits give the same errors as every other endpoint: see Errors. OpenAI’s SDKs retry the ones worth retrying.

Try it first

In the dashboard’s Playground, pick GPT-6 Luna Decisions, paste the content or attach an image, and set your questions beside it. Its Get code button writes the same request with OpenAI’s SDK for Python or Node.js, or as cURL.

Questions, or something missing? Ask our support team and we’ll answer by email, usually within a minute.