# Decisions

> Typed, calibrated answers to typed questions with the Orion Compass and d3 decision models: POST /v1/decisions, in the format of OpenAI's Decisions API, and the Jev-compatible POST /v1/systemone.

Source: https://docs.serenityedge.ai/docs/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](https://docs.serenityedge.ai/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.

|  |  |
| --- | --- |
| Endpoints | `POST /v1/decisions` (the format of OpenAI's Decisions API), `POST /v1/systemone` (Jev's format) |
| Models | `orion-compass`, `orion-compass-flash`, `d3` (see [Models](https://docs.serenityedge.ai/docs/decisions/#models)) |
| Question types | `predicate` (yes or no), `choice` (one of several options), `score` (a level on an ordered scale) |
| Input | Text; also images on `orion-compass-flash` and `d3` (see [Images](https://docs.serenityedge.ai/docs/decisions/#images)) |
| Billing | Input tokens only, images included; output is not billed |
| Access | Serenity Edge API keys (see [Access](https://docs.serenityedge.ai/docs/decisions/#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 models | [Chat completions](https://docs.serenityedge.ai/docs/api/edge/create-chat-completion/) |
| --- | --- | --- |
| Result | Typed answers: a probability, a choice or a score per question | Generated text, or JSON with [structured outputs](https://docs.serenityedge.ai/docs/structured-outputs/) |
| Probabilities | For every option or level | Not returned |
| Invalid labels | Impossible: the answer is always one of your options | Possible; validate the output |
| Billing | Input tokens only | Input and output tokens |
| Streaming | No | Yes |

Use [chat completions](https://docs.serenityedge.ai/docs/api/edge/create-chat-completion/) when you need text: answers, summaries, explanations, free-form extraction, conversation or [tool calling](https://docs.serenityedge.ai/docs/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](https://docs.serenityedge.ai/docs/decisions/#images)).
-   `d3` (alias `vllm-sr/d3`), the open-weight 27B decision model of [vLLM Semantic Router](https://github.com/vllm-project/semantic-router) under the Apache-2.0 licence, first on the text board of the Jev Decision Index. It also reads images (see [Images](https://docs.serenityedge.ai/docs/decisions/#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 id | Context | Reads | Input | Output | Endpoints |
| --- | --- | --- | --- | --- | --- |
| `d3`, also `vllm-sr/d3` | 24,576 | text, image | $0.042 | Free | /v1/decisions, /v1/systemone |
| `orion-compass`, alias of `orion-compass-26.1` | 32,000 | text | $0.034 | Free | /v1/decisions, /v1/systemone |
| `orion-compass-flash`, alias of `orion-compass-flash-26.1` | 32,000 | text, image | $0.017 | Free | /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](https://docs.serenityedge.ai/docs/decisions/#access).

### curl

```bash
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.

```python
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:

```python
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:

```javascript
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

```json
{
  "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

| Field | Required | Description |
| --- | --- | --- |
| `model` | Yes | A decision model id, such as `orion-compass`, `orion-compass-flash` or `d3`. |
| `input` | Yes | What the questions are about: a string, an array of user messages, or a whole conversation (see below). |
| `questions` | Yes | One to 256 questions. The answers come back in the same order. |
| `safety_identifier` | No | A 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:

```json
"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](https://docs.serenityedge.ai/docs/decisions/#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.

```json
{
  "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:

```text
[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.

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

```json
{ "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`.

```json
{
  "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" }
  ]
}
```

```json
{
  "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`.

```json
{
  "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" }
  ]
}
```

```json
{
  "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:

```json
"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:

```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](https://docs.serenityedge.ai/docs/decisions/#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:

```json
{
  "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

| Field | Description |
| --- | --- |
| `answers` | One answer per question, in question order, with the question's `type` and `name`. |
| `model` | The versioned id of the model that answered, such as `orion-compass-26.1`. |
| `usage.input_tokens` | Tokens the model read to answer, images included. This is what you are billed for. |
| `usage.output_tokens` | Always 0: the answer is read from the model's probabilities and no text is generated. |
| `usage.input_tokens_details` | `cached_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_details` | `reasoning_tokens`, always 0. |
| `usage.total_tokens` | Equal 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/decisions` | `POST /v1/systemone` |
| --- | --- | --- |
| Input | `input`: a string, user messages or a conversation | `state`: any JSON value |
| Questions | An array; optional `name` | An object keyed by ids of your choice |
| Yes or no | `predicate` | `noul` |
| One of several | `choice` with `choices: [{value, description}]` | `choice` with `criteria`: `{name: description}` or `[names]` |
| Ordered scale | `score` with `levels: [{label, description}]` | `score` with `criteria`: the levels in order |
| Images | `input_image` parts in `input` | `images`: a list of data URLs |
| Answers | An array in question order | An object keyed by question id |
| Calibration | Always calibrated | Calibrated; `"calibration": "raw"` for uncalibrated probabilities |
| Refused question | HTTP 400 | HTTP 422, as Jev |

```bash
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"]}
    }
  }'
```

```json
{
  "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](https://docs.serenityedge.ai/docs/decisions/#errors)); a question the model cannot read is refused with HTTP 422 instead of 400.

See [Create decision (Jev format)](https://docs.serenityedge.ai/docs/api/edge/create-system-one-decision/) 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](https://docs.serenityedge.ai/docs/access/enterprise/) or write to [contact@substrate.ai](mailto:contact@substrate.ai).

-   Keys from a [Serenity Star account](https://docs.serenityedge.ai/docs/access/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](https://docs.serenityedge.ai/release-notes/).
-   Decision models are not part of the [OpenRouter onboarding](https://docs.serenityedge.ai/docs/access/openrouter/), which covers the chat models only.

## Errors

Errors use the same [error object](https://docs.serenityedge.ai/docs/errors-and-rate-limits/#error-object) as chat completions, on both endpoints.

| Status | `code` | When | What to do |
| --- | --- | --- | --- |
| 400 | `invalid_request_error` | Malformed body, missing `model`, an unknown question type, an invalid option or level, a conversation item that is not accepted (see [Conversations](https://docs.serenityedge.ai/docs/decisions/#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. |
| 400 | `context_length_exceeded` | The input and a question do not fit the model's context. Nothing is truncated. | Shorten the input. |
| 422 | `invalid_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. |
| 401 | `invalid_api_key` | Missing, malformed or revoked API key. | Check the `Authorization` header and the key. |
| 403 | `permission_denied` | The key cannot use the decision endpoints, such as a key from a Serenity Star account (see [Access](https://docs.serenityedge.ai/docs/decisions/#access)). | Use a Serenity Edge API key. |
| 404 | `model_not_found` | The model is not a decision model, for example a chat model id. | Check `GET /v1/models?format=openai`. |
| 404 | `not_found` | The endpoint is not enabled yet (see the preview note at the top). | Watch the release notes. |
| 413 | `request_too_large` | Request body larger than 4 MB, or 12 MiB on `orion-compass-flash` and `d3`. | Reduce the input, or send smaller images. |
| 429 | `rate_limit_exceeded` | Your 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, 503 | `upstream_error`, `timeout` (500), `upstream_unavailable` (503) | As for [chat completions](https://docs.serenityedge.ai/docs/errors-and-rate-limits/#http-status-codes). | Retry with backoff; check the [status page](https://status.serenityedge.ai/) 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](https://docs.serenityedge.ai/docs/decisions/#models) and returned by `GET /v1/models?format=openai` (`pricing.input`; `pricing.output` is 0). See also [Pricing and limits](https://docs.serenityedge.ai/docs/pricing-and-limits/#decision-models).
-   **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](https://docs.serenityedge.ai/docs/errors-and-rate-limits/#handling-429-correctly).
-   **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](https://docs.serenityedge.ai/docs/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

-   [Create decision](https://docs.serenityedge.ai/docs/api/edge/create-decision/) and [Create decision (Jev format)](https://docs.serenityedge.ai/docs/api/edge/create-system-one-decision/) in the API reference.
-   [Models](https://docs.serenityedge.ai/docs/models/) and [Pricing and limits](https://docs.serenityedge.ai/docs/pricing-and-limits/) for the chat models.
-   [Errors and rate limits](https://docs.serenityedge.ai/docs/errors-and-rate-limits/) for retries and backoff.
