# OpenAI Decisions API > POST /v1/decisions: GPT-6 Luna Decisions answers predicate, choice and score questions about text and images with probabilities, through OpenAI's own SDKs. Request shape, limits, refusals and prices. Source: https://zurelay.com/docs/openai-decisions 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` | Model | ID | Input, per 1M tokens | OpenAI’s price | Output | | --- | --- | --- | --- | --- | | [GPT-6 Luna Decisions](https://zurelay.com/models/gpt-6-luna-decisions) | `gpt-6-luna-decisions` | $0.04 | $0.10 | Free | 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)](https://zurelay.com/docs/decisions). ## Ask questions **Python** (`triage.py`): ```python # 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") ``` **Node.js** (`triage.mjs`): ```javascript // npm install openai@latest (7.30.0 or later) import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.zurelay.com/v1", apiKey: process.env.ZURELAY_API_KEY, }); const decision = await 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 (const answer of decision.answers) { if (answer.type === "refusal") console.log(answer.name, "refused"); else if (answer.type === "predicate") console.log(answer.name, answer.probability); else if (answer.type === "choice") console.log(answer.name, answer.choice, answer.confidence); else console.log(answer.name, answer.score, answer.confidence); } console.log(decision.usage.input_tokens, "input tokens"); ``` **cURL**: ```bash curl https://api.zurelay.com/v1/decisions \ -H "Authorization: Bearer $ZURELAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "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" }] } ] }' ``` 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: ```json { "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 - `model` (string, required): `gpt-6-luna-decisions`, or `gpt-6-luna` on this endpoint. - `input` (string or array, required): 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. - `questions` (array, required): 1 to 64 questions, answered in the order you list them. - `safety_identifier` (string): Optional: an opaque id for the end user the request is for, such as a hash of your own user id. - `prompt_cache_key` (string): Accepted and ignored, as is `prompt_cache_options`: caching is automatic, and every input token costs the same. **Each question** - `type` (string, required): `predicate`, `choice` or `score`. - `instructions` (string, required): The question, as text. Images go in the input, not here. - `name` (string): Optional, and unique within the request: comes back on the question’s answer, so you can read answers by name. - `choices` (array): 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. - `levels` (array): 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 | Type | Asks | Answer | | --- | --- | --- | | `predicate` | How likely a statement about the input is true | `probability`, from 0 to 1 | | `choice` | Which of your 2 to 255 values fits | `choice`, a `confidence`, and the `probabilities` of every value | | `score` | Where it sits on your 2 to 10 ordered levels | `score`, 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: ```json { "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: ```json { "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: ```json { "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. **Python** (`returns.py`): ```python 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) ``` **Node.js** (`returns.mjs`): ```javascript import { readFile } from "node:fs/promises"; import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.zurelay.com/v1", apiKey: process.env.ZURELAY_API_KEY }); const photo = `data:image/jpeg;base64,${(await readFile("return-photo.jpg")).toString("base64")}`; const decision = await 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" }, ], }, ], }); console.log(decision.answers); ``` **cURL**: ```bash curl https://api.zurelay.com/v1/decisions \ -H "Authorization: Bearer $ZURELAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "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": "https://example.com/returns/4471.jpg", "detail": "low" } ] } ], "questions": [ { "type": "predicate", "name": "damaged", "instructions": "Does the photo show visible damage to the product?" } ] }' ``` 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: ```json { "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](https://zurelay.com/docs/key-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 | Status | Code | When | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, or uses something the API doesn’t take. The message says what and where. | | 400 | `invalid_input` | An image link didn’t download, or isn’t a PNG, JPEG, WebP or GIF image. | | 400 | `context_length_exceeded` | The input and questions come to more than 1.05M tokens. Send a shorter input. | | 503 | `model_unavailable` or `service_unavailable` | The 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](https://zurelay.com/docs/errors). OpenAI’s SDKs retry the ones worth retrying. > **Try it first:** In the dashboard’s [Playground](https://zurelay.com/app/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.