Skip to main content

Decisions

Preview

Decision models and the two decision endpoints are in public preview. Availability, and any change to the request or response shapes before general availability, will be announced in the release notes.

Decision models answer typed questions about an input with calibrated probabilities instead of generated text. You send a ticket, a message or a document together with questions such as "is this a refund request?", "which team should handle it?" or "how urgent is it, from low to high?", and you get back a probability, a choice or a score for each one, ready to drive your code.

EndpointsPOST /v1/decisions (the format of OpenAI's Decisions API), POST /v1/systemone (Jev's format)
Modelsorion-compass, orion-compass-flash, d3 (see Models)
Question typespredicate (yes or no), choice (one of several options), score (a level on an ordered scale)
InputText; also images on orion-compass-flash and d3 (see Images)
BillingInput tokens only, images included; output is not billed
AccessSerenity Edge API keys (see Access)

When to use decision models​

Use a decision model when the result of a model call is a decision your code acts on, not text a person reads:

  • Routing: pick the team, queue or model that should handle a request (choice).
  • Classification: label content with one of a fixed set of categories (choice), or with several yes or no tags at once (predicate).
  • Gating: let an action go ahead (publish a comment, send an email, call a tool) only when a predicate probability clears a threshold.
  • Triage: place a ticket or an alert on an urgency or severity scale (score).
  • Scoring: grade an answer or a document against a rubric (score), for example to evaluate the output of another model.
Decision modelsChat completions
ResultTyped answers: a probability, a choice or a score per questionGenerated text, or JSON with structured outputs
ProbabilitiesFor every option or levelNot returned
Invalid labelsImpossible: the answer is always one of your optionsPossible; validate the output
BillingInput tokens onlyInput and output tokens
StreamingNoYes

Use chat completions when you need text: answers, summaries, explanations, free-form extraction, conversation or tool calling.

The probabilities are calibrated: across many answers, an answer given with probability 0.8 should be right about 80% of the time, which is what makes a threshold meaningful. Calibration is tuned on general data, so check it on a sample of your own data before you rely on a threshold.

Models​

Three decision models are in preview, two in the Orion line and one open-weight:

  • orion-compass when the question needs more depth: nuanced inputs, fine-grained options or detailed rubrics.
  • orion-compass-flash for the lowest latency and price at volume: high-volume routing, filtering and triage. It also reads images (see Images).
  • d3 (alias vllm-sr/d3), the open-weight 27B decision model of vLLM Semantic Router under the Apache-2.0 licence, first on the text board of the Jev Decision Index. It also reads images (see Images). Its context is shorter: 24,576 tokens.

The Orion ids are aliases that follow the current version of the model; the versioned id each points to (such as orion-compass-26.1) pins one version. d3 is served from its published weights. The model field of every reply is the versioned id that answered. Decision models are served only on the decision endpoints, and chat models cannot be used on them.

The table is read from GET /v1/models?format=openai when this page is published and again when you open it. That list marks each decision model with an endpoints array and what it reads (input_modalities); the default catalogue of GET /v1/models lists chat models only.

Model idContextReadsInputOutputEndpoints
d3also vllm-sr/d324,576textimage$0.042Free/v1/decisions/v1/systemone
orion-compassalias of orion-compass-26.132,000text$0.034Free/v1/decisions/v1/systemone
orion-compass-flashalias of orion-compass-flash-26.132,000textimage$0.017Free/v1/decisions/v1/systemone

Read from the API (GET /v1/models?format=openai) when this page was published, on 6 November 2026, 19:49 UTC.

Quick start​

All examples send the same ticket and the same three questions. Replace $SERENITY_EDGE_API_KEY with a Serenity Edge API key.

curl​

curl https://api.serenityedge.ai/v1/decisions \
-H "Authorization: Bearer $SERENITY_EDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "orion-compass",
"input": "Ticket #4821. Customer: You charged my card twice for the same order last week. Refund the duplicate payment today or I will dispute it with my bank.",
"questions": [
{"type": "predicate", "name": "refund", "instructions": "Is the customer asking for a refund?"},
{
"type": "choice",
"name": "team",
"instructions": "Which team should handle this ticket?",
"choices": [
{"value": "billing", "description": "Payments, charges and refunds"},
{"value": "shipping", "description": "Delivery and tracking"},
{"value": "technical", "description": "Bugs and sign-in problems"}
]
},
{
"type": "score",
"name": "urgency",
"instructions": "How urgent is this ticket?",
"levels": [
{"label": "low", "description": "Can wait a week"},
{"label": "medium", "description": "Within two days"},
{"label": "high", "description": "Today, or the customer escalates"}
]
}
]
}'

Python​

OpenAI's Python library has a decisions resource (client.decisions.create). It is not yet in every released version: upgrade with pip install --upgrade openai and, if your version still has no client.decisions, use the library's generic client.post shown below, which works with any recent release.

from openai import OpenAI

client = OpenAI(
base_url="https://api.serenityedge.ai/v1",
api_key="YOUR_SERENITY_EDGE_API_KEY",
)

decision = client.decisions.create(
model="orion-compass",
input=(
"Ticket #4821. Customer: You charged my card twice for the same order last week. "
"Refund the duplicate payment today or I will dispute it with my bank."
),
questions=[
{"type": "predicate", "name": "refund", "instructions": "Is the customer asking for a refund?"},
{
"type": "choice",
"name": "team",
"instructions": "Which team should handle this ticket?",
"choices": [
{"value": "billing", "description": "Payments, charges and refunds"},
{"value": "shipping", "description": "Delivery and tracking"},
{"value": "technical", "description": "Bugs and sign-in problems"},
],
},
{
"type": "score",
"name": "urgency",
"instructions": "How urgent is this ticket?",
"levels": [
{"label": "low", "description": "Can wait a week"},
{"label": "medium", "description": "Within two days"},
{"label": "high", "description": "Today, or the customer escalates"},
],
},
],
)

answers = {answer.name: answer for answer in decision.answers}
print(answers["refund"].probability) # 0.97
print(answers["team"].choice, answers["team"].confidence) # billing 0.91
print(answers["urgency"].score) # 1.72
print(decision.usage.input_tokens)

With a released version that has no decisions resource yet, send the same body with the library's generic post method and read the JSON yourself:

import httpx

response = client.post("/decisions", cast_to=httpx.Response, body={"model": "orion-compass", "input": "...", "questions": [...]})
decision = response.json()

Node.js​

The request is plain JSON over HTTPS, so fetch (Node.js 18 or later) is enough:

const response = await fetch("https://api.serenityedge.ai/v1/decisions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SERENITY_EDGE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "orion-compass",
input:
"Ticket #4821. Customer: You charged my card twice for the same order last week. " +
"Refund the duplicate payment today or I will dispute it with my bank.",
questions: [
{ type: "predicate", name: "refund", instructions: "Is the customer asking for a refund?" },
{
type: "choice",
name: "team",
instructions: "Which team should handle this ticket?",
choices: [
{ value: "billing", description: "Payments, charges and refunds" },
{ value: "shipping", description: "Delivery and tracking" },
{ value: "technical", description: "Bugs and sign-in problems" },
],
},
{
type: "score",
name: "urgency",
instructions: "How urgent is this ticket?",
levels: [
{ label: "low", description: "Can wait a week" },
{ label: "medium", description: "Within two days" },
{ label: "high", description: "Today, or the customer escalates" },
],
},
],
}),
});

if (!response.ok) {
const { error } = await response.json();
throw new Error(`HTTP ${response.status} ${error.code}: ${error.message}`);
}

const decision = await response.json();
const answers = Object.fromEntries(decision.answers.map((answer) => [answer.name, answer]));
console.log(answers.refund.probability, answers.team.choice, answers.urgency.score);

The reply​

{
"answers": [
{ "type": "predicate", "name": "refund", "probability": 0.97 },
{
"type": "choice",
"name": "team",
"choice": "billing",
"confidence": 0.91,
"probabilities": [
{ "value": "billing", "probability": 0.94 },
{ "value": "shipping", "probability": 0.02 },
{ "value": "technical", "probability": 0.04 }
]
},
{
"type": "score",
"name": "urgency",
"score": 1.72,
"confidence": 0.625,
"probabilities": [
{ "value": 0, "label": "low", "probability": 0.03 },
{ "value": 1, "label": "medium", "probability": 0.22 },
{ "value": 2, "label": "high", "probability": 0.75 }
]
}
],
"model": "orion-compass-26.1",
"usage": {
"input_tokens": 412,
"input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
"output_tokens": 0,
"output_tokens_details": { "reasoning_tokens": 0 },
"total_tokens": 412
}
}

The probabilities in these examples are rounded for readability; the API returns them unrounded.

Request​

FieldRequiredDescription
modelYesA decision model id, such as orion-compass, orion-compass-flash or d3.
inputYesWhat the questions are about: a string, an array of user messages, or a whole conversation (see below).
questionsYesOne to 256 questions. The answers come back in the same order.
safety_identifierNoA stable identifier of your end user, as in OpenAI's API. Accepted for compatibility; like the rest of the body, it is never logged.

input can also be an array of user messages, each with a string or an array of input_text parts. Their text is read in order, separated by a blank line:

"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "Order 1182, delivered on 2 October." },
{ "type": "input_text", "text": "Customer: The box arrived crushed and the lamp inside is broken." }
]
}
]

orion-compass-flash and d3 also read images, as input_image parts: see Images. orion-compass reads text only and refuses an input_image part with HTTP 400.

Every question has a type, instructions (the question in natural language) and an optional name. Names must be unique within a request; each answer repeats the name of its question, and has none when the question had none. Write the instructions as you would ask a careful colleague, and describe each option or level when its meaning is not obvious from its value: the model reads the descriptions.

Conversations​

input can also hold a whole conversation, in the format of OpenAI's Decisions API, so you can ask questions about it: whether an agent followed its instructions, used its tools well or solved the user's problem. Besides user messages, it takes:

  • system and developer messages, with a string or input_text parts;
  • assistant messages, with a string or output_text and refusal parts, and an optional phase (commentary or final_answer);
  • function_call items (name, call_id, arguments) and function_call_output items (call_id, output: a string or input_text parts);
  • custom_tool_call items (name, call_id, input) and custom_tool_call_output items (call_id, output);
  • input_json parts in user messages, whose data is an object or an array of objects.

The whole conversation is evidence: the model reads it as the input your questions are about. System and developer messages do not change how the model answers, since the questions' instructions do that, so you can ask whether an agent followed its own system prompt. Tool calls and their outputs are read too. Every item counts as input tokens.

{
"model": "orion-compass",
"input": [
{ "role": "system", "content": "You are a support agent. Never promise a refund before checking the order." },
{ "role": "user", "content": "You charged me twice for order 4821." },
{ "type": "function_call", "call_id": "call_1", "name": "get_order", "arguments": "{\"id\":\"4821\"}" },
{ "type": "function_call_output", "call_id": "call_1", "output": "{\"status\":\"paid twice\"}" },
{ "role": "assistant", "content": "I see the duplicate payment. I have refunded it." }
],
"questions": [
{ "type": "predicate", "name": "checked_first", "instructions": "Did the agent check the order before promising a refund?" }
]
}

The model reads the conversation as a transcript: one block per item, in order, separated by a blank line, each under a label. The request above reads:

[system]
You are a support agent. Never promise a refund before checking the order.

[user]
You charged me twice for order 4821.

[function_call get_order call_1]
{"id":"4821"}

[function_call_output call_1]
{"status":"paid twice"}

[assistant]
I see the duplicate payment. I have refunded it.

An assistant phase is part of its label ([assistant final_answer]), a namespace prefixes a tool's name (billing.refund), input_json is read as compact JSON, and a refusal part reads Refusal: followed by its text. An assistant message without text, one that only called tools, has no block. An input of user messages only has no labels: it is read as described above.

These are refused with HTTP 400 and the item at fault in param:

  • a tool output whose call_id does not match an earlier call of the same kind;
  • images anywhere but in user messages, or on a model that reads text only;
  • other item types, such as reasoning, and parts a role does not take, such as an input_image in a system message.

Question types​

Predicate​

A yes or no question. The answer is the probability that the answer is yes.

{ "type": "predicate", "name": "refund", "instructions": "Is the customer asking for a refund?" }
{ "type": "predicate", "name": "refund", "probability": 0.97 }

Compare probability with a threshold that matches the cost of a mistake: a high one before an irreversible action, a lower one to flag content for review. Several predicates in one request are independent, so they work as multi-label tags.

Choice​

Pick one of several options. Each option has a value, a string or a boolean that is returned as is, and an optional description.

{
"type": "choice",
"name": "team",
"instructions": "Which team should handle this ticket?",
"choices": [
{ "value": "billing", "description": "Payments, charges and refunds" },
{ "value": "shipping", "description": "Delivery and tracking" },
{ "value": "technical", "description": "Bugs and sign-in problems" }
]
}
{
"type": "choice",
"name": "team",
"choice": "billing",
"confidence": 0.91,
"probabilities": [
{ "value": "billing", "probability": 0.94 },
{ "value": "shipping", "probability": 0.02 },
{ "value": "technical", "probability": 0.04 }
]
}
  • choice is the value of the most likely option, typed as in the request; on a tie, the option listed first.
  • probabilities lists every option in the order of choices, and they add up to 1.
  • confidence says how far the most likely option stands above chance: (p_max - 1/n) / (1 - 1/n) for n options, 0 when all options are equally likely and 1 when one takes all the probability.
  • A question takes at most 255 options. Values must be distinct, also as text: a string and a boolean that read the same ("true" and true) cannot both be offered.

Add an option such as "other" or "none" when the input may fit none of the real ones; otherwise the model has to pick the closest.

Score​

Place the input on an ordered scale of 2 to 10 levels, from the first to the last. Each level has a distinct label and an optional description.

{
"type": "score",
"name": "urgency",
"instructions": "How urgent is this ticket?",
"levels": [
{ "label": "low", "description": "Can wait a week" },
{ "label": "medium", "description": "Within two days" },
{ "label": "high", "description": "Today, or the customer escalates" }
]
}
{
"type": "score",
"name": "urgency",
"score": 1.72,
"confidence": 0.625,
"probabilities": [
{ "value": 0, "label": "low", "probability": 0.03 },
{ "value": 1, "label": "medium", "probability": 0.22 },
{ "value": 2, "label": "high", "probability": 0.75 }
]
}
  • score is the expected position on the scale: the probability-weighted mean of the positions, where the first level is 0. Here 0 × 0.03 + 1 × 0.22 + 2 × 0.75 = 1.72, between "medium" and "high" and closer to "high".
  • probabilities lists every level with its position (value) and label, in the order of levels.
  • confidence is computed as for a choice, over the levels.

Use score when the order matters (sorting a queue, a threshold such as "1.5 or more goes to the on-call team"), and the most likely level in probabilities when you need one label.

Images​

orion-compass-flash and d3 read images together with the text: a scanned invoice, a screenshot, a photo of a damaged parcel. Send each image as an input_image part holding a base64 data URL:

"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "Customer photo of the delivered parcel, order 1182." },
{ "type": "input_image", "image_url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." }
]
}
]

To build the data URL from a file in Python:

import base64

with open("parcel.jpg", "rb") as f:
image_url = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()
  • Formats. PNG, JPEG or WebP, as base64 data URLs (data:image/png;base64,...). File ids and web URLs are refused: the API fetches nothing.
  • Limits. At most 4 images per request, 8 MiB in total once decoded, and 16 megapixels per image. The request body may be up to 12 MiB on these two models.
  • Order. The model sees the images at the start of the message, before its text, whatever the order of the parts.
  • Billing. The tokens the images expand to are input tokens: usage.input_tokens counts them, and usage.input_tokens_details splits it into text_tokens and image_tokens. Like the text, the images are read with each question: a request with one image and ten questions bills that image's tokens ten times.
  • Not supported. Videos (input_video parts, or videos on POST /v1/systemone) are refused. So is text that holds the model's own image tokens, in the input, the instructions or the options.
  • Other models. orion-compass reads text only, and refuses an image with HTTP 400 (422 on POST /v1/systemone).

On the Jev-compatible route, send the same data URLs as images, a list beside state; usage.input_tokens counts the images, and usage.text_tokens and usage.image_tokens split it:

{
"model": "orion-compass-flash",
"state": { "order": 1182, "note": "Customer photo of the delivered parcel." },
"images": ["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."],
"questions": {
"damaged": { "type": "noul", "instructions": "Does the parcel look damaged?" }
}
}

Response​

FieldDescription
answersOne answer per question, in question order, with the question's type and name.
modelThe versioned id of the model that answered, such as orion-compass-26.1.
usage.input_tokensTokens the model read to answer, images included. This is what you are billed for.
usage.output_tokensAlways 0: the answer is read from the model's probabilities and no text is generated.
usage.input_tokens_detailscached_tokens and cache_write_tokens, both reported as 0. With images, text_tokens and image_tokens, which add up to input_tokens.
usage.output_tokens_detailsreasoning_tokens, always 0.
usage.total_tokensEqual to input_tokens.

The model reads the input together with each question, so usage.input_tokens grows with both the length of the input and the number of questions. Asking several questions in one request saves round trips, not tokens.

Jev-compatible route​

POST /v1/systemone serves the same models in the format of Jev's POST /v1/systemone (TypeSafe), for callers and tools migrating from Jev. Point your client at https://api.serenityedge.ai/v1/systemone with a Serenity Edge API key and a decision model id. For new integrations, prefer POST /v1/decisions.

POST /v1/decisionsPOST /v1/systemone
Inputinput: a string, user messages or a conversationstate: any JSON value
QuestionsAn array; optional nameAn object keyed by ids of your choice
Yes or nopredicatenoul
One of severalchoice with choices: [{value, description}]choice with criteria: {name: description} or [names]
Ordered scalescore with levels: [{label, description}]score with criteria: the levels in order
Imagesinput_image parts in inputimages: a list of data URLs
AnswersAn array in question orderAn object keyed by question id
CalibrationAlways calibratedCalibrated; "calibration": "raw" for uncalibrated probabilities
Refused questionHTTP 400HTTP 422, as Jev
curl https://api.serenityedge.ai/v1/systemone \
-H "Authorization: Bearer $SERENITY_EDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "orion-compass",
"state": {
"ticket_id": 4821,
"channel": "email",
"message": "You charged my card twice for the same order last week. Refund the duplicate payment today or I will dispute it with my bank."
},
"questions": {
"refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"},
"team": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Payments, charges and refunds",
"shipping": "Delivery and tracking",
"technical": "Bugs and sign-in problems"
}
},
"urgency": {"type": "score", "instructions": "How urgent is this ticket?", "criteria": ["low", "medium", "high"]}
}
}'
{
"model": "orion-compass-26.1",
"answers": {
"refund": { "type": "noul", "noul": 0.97 },
"team": {
"type": "choice",
"choice": "billing",
"confidence": 0.91,
"probabilities": { "billing": 0.94, "shipping": 0.02, "technical": 0.04 }
},
"urgency": {
"type": "score",
"score": 1.72,
"confidence": 0.625,
"legend": { "0": "low", "1": "medium", "2": "high" },
"probabilities": { "0": 0.03, "1": 0.22, "2": 0.75 }
}
},
"usage": { "input_tokens": 398, "output_tokens": 3 },
"latency_ms": 84.6,
"calibration": { "applied": true, "temperatures": { "noul": 1.21, "choice": 1.08, "score": 1.14 } }
}
  • A score answer lists its levels by position: legend maps each position to its level and probabilities gives the probability of each position.
  • usage.output_tokens follows Jev's format (one per answer) and is not billed; as on /v1/decisions, only usage.input_tokens is, images included (usage.text_tokens and usage.image_tokens split it for a request with images).
  • calibration.applied is false when the request asked for "calibration": "raw".
  • Errors use the same error object as every other route (see Errors); a question the model cannot read is refused with HTTP 422 instead of 400.

See Create decision (Jev format) for every field.

Access​

Decision models can be used only with Serenity Edge API keys, the keys Substrate AI issues directly to customers and partner platforms. Today they are issued on request: see Enterprise or write to [email protected].

  • Keys from a Serenity Star account get HTTP 403 with code: "permission_denied" on the decision endpoints for now. Support for them is coming and will be announced in the release notes.
  • Decision models are not part of the OpenRouter onboarding, which covers the chat models only.

Errors​

Errors use the same error object as chat completions, on both endpoints.

StatuscodeWhenWhat to do
400invalid_request_errorMalformed body, missing model, an unknown question type, an invalid option or level, a conversation item that is not accepted (see Conversations), or an image the model cannot read (a model that reads text only, more than 4 images, more than 8 MiB once decoded, more than 16 megapixels, a format other than PNG, JPEG or WebP, a broken file). On /v1/systemone, only a body that is not a JSON object or has no model. message says what to fix and param points at the parameter, such as questions[1].choices[2].value.Fix the request. Do not retry as is.
400context_length_exceededThe input and a question do not fit the model's context. Nothing is truncated.Shorten the input.
422invalid_request_error or context_length_exceeded/v1/systemone only: the model refused a question or the state, for the reasons above.Fix the request. Do not retry as is.
401invalid_api_keyMissing, malformed or revoked API key.Check the Authorization header and the key.
403permission_deniedThe key cannot use the decision endpoints, such as a key from a Serenity Star account (see Access).Use a Serenity Edge API key.
404model_not_foundThe model is not a decision model, for example a chat model id.Check GET /v1/models?format=openai.
404not_foundThe endpoint is not enabled yet (see the preview note at the top).Watch the release notes.
413request_too_largeRequest body larger than 4 MB, or 12 MiB on orion-compass-flash and d3.Reduce the input, or send smaller images.
429rate_limit_exceededYour rate limit is exceeded, or the model is temporarily at capacity. The Retry-After header says how many seconds to wait.Wait for Retry-After, then retry with backoff.
500, 503upstream_error, timeout (500), upstream_unavailable (503)As for chat completions.Retry with backoff; check the status page if it persists.

Decisions are idempotent: retrying a request that failed with 429 or a 5xx is safe.

Pricing and limits​

  • Input tokens only. You are billed on usage.input_tokens, images included, at the model's input price; output is not billed. Prices are in US dollars per million input tokens, shown live in the table above and returned by GET /v1/models?format=openai (pricing.input; pricing.output is 0). See also Pricing and limits.
  • Context. The input and each question must fit the model's context length, shown in the same table. A longer request is refused (HTTP 400, or 422 on /v1/systemone) with code: "context_length_exceeded"; nothing is truncated.
  • Rate limits and capacity. Requests are admitted per model as on chat completions. Over your limit or when the model is at capacity, the API answers HTTP 429 with Retry-After; handle it as described in Errors and rate limits.
  • Request size. At most 4 MB per request body, or 12 MiB on orion-compass-flash and d3, for images.
  • Data. The same data policy as chat completions applies (see Data residency): request and response bodies are never written to disk or logs and never used for training.

Current limitations​

  • Images on orion-compass-flash and d3 only. orion-compass reads text only. Images must be base64 data URLs, and videos are not supported.
  • No streaming. The reply is a single JSON object.
  • Token reporting. output_tokens is always 0 and cached tokens are reported as 0, so there is no cached-input discount on decisions.
  • Questions, options and levels. At most 256 questions per request, on both endpoints; at most 255 options per choice question; 2 to 10 levels per score question. A request over a limit is refused with HTTP 400 (422 on POST /v1/systemone) and the parameter at fault in param.
  • No per-question refusal. A request the model cannot answer is refused as a whole with an error; there is no refusal answer for a single question.
  • Access routes. Serenity Edge API keys only: not yet with keys from a Serenity Star account, and not part of the OpenRouter onboarding.

Next steps​