# Decisions

`celeris-1-decision` answers questions about text or images with probabilities.
Each answer comes back as a number or a choice you can threshold, rank, or
route on. Choose the API format that matches your integration:

| SDK | Endpoint |
| --- | --- |
| [Jev SDK](#system-one-api) | `POST /v1/systemone` |
| [OpenAI SDK](#openai-decisions-api) | `POST /v1/decisions` |

Both endpoints serve the same model and share its price and rate limits.
Each response is one JSON body. There is no streaming.

## System One API

Send `model: "celeris-1-decision"`, a `state` containing your data, and named
`questions` to `POST /v1/systemone`. It implements TypeSafe's System One API,
so code written for Jev with the
[Jev SDK](https://docs.typesafe.ai/sdk/python/) runs against it once you change
the base URL, key, and model name.

### A yes/no question

**cURL**

```bash
curl https://inference.celeris.ai/celeris-1-decision/v1/systemone \
  -H "Authorization: Bearer $CELERIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "celeris-1-decision",
    "state": "I was charged twice this month. Please refund one of the payments.",
    "questions": {
      "refund": {"type": "noul", "instructions": "The customer is asking for a refund."}
    }
  }'
```

**Jev SDK**

```python
# pip install typesafe-sdk

from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient(
    api_key=os.environ["CELERIS_API_KEY"],
    base_url="https://inference.celeris.ai/celeris-1-decision",
    model="celeris-1-decision",
)

response = client.system_one(
    state="I was charged twice this month. Please refund one of the payments.",
    questions={"refund": Noul(instructions="The customer is asking for a refund.")},
)
print(response.nouls["refund"].noul)  # 0.95
```

```json
{
  "model": "celeris-1-decision",
  "answers": {
    "refund": {"type": "noul", "noul": 0.95}
  },
  "usage": {"input_tokens": 122, "output_tokens": 0}
}
```

`noul` is the probability that the answer is yes, from `0` to `1`.
The responses shown on this page are examples; values can vary.

To switch existing code without editing it, set the SDK's environment
variables:

```bash
export TYPESAFE_BASE_URL=https://inference.celeris.ai/celeris-1-decision
export TYPESAFE_API_KEY=$CELERIS_API_KEY
export TYPESAFE_DEFAULT_MODEL=celeris-1-decision
```

### Question types

Each question needs a `type` and usually carries `instructions`. One request
can hold several questions of any mix of types, and `state` can be a string or
any other JSON value.

| `type` | Answer |
| --- | --- |
| `noul` | `noul`: the probability of yes. |
| `choice` | `choice`: the most likely option. `probabilities`: one per option. |
| `score` | `score`: the average level, weighted by probability. `probabilities`: one per level. `legend`: each level's description. |

For `noul`, `criteria` is optional. Use `{"true": ..., "false": ...}` to
describe each outcome. For `choice`, `criteria` is an object of at least two
options. Each key is an option name and each value describes it, or is
`null`. For `score`, `criteria` is an array of at least two levels, lowest
first. Levels are numbered from `0`.

`choice` and `score` answers also carry `confidence`, the top probability
rescaled so that `0` is an even split across the options and `1` is certainty.
Probabilities are rounded to two decimal places and sum to 1.

**cURL**

```bash
curl https://inference.celeris.ai/celeris-1-decision/v1/systemone \
  -H "Authorization: Bearer $CELERIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "celeris-1-decision",
    "state": {
      "subject": "Charged twice",
      "body": "I was charged twice this month and nobody has answered my emails for a week. Please refund one of the payments."
    },
    "questions": {
      "refund": {"type": "noul", "instructions": "The customer is asking for a refund."},
      "team": {
        "type": "choice",
        "instructions": "Which team should handle this ticket?",
        "criteria": {
          "billing": "Payments, invoices and refunds.",
          "technical": "Bugs and outages.",
          "sales": "Upgrades and quotes."
        }
      },
      "frustration": {
        "type": "score",
        "instructions": "How frustrated is the customer?",
        "criteria": ["Calm.", "Annoyed.", "Angry."]
      }
    }
  }'
```

**Jev SDK**

```python
from typesafe_sdk import Choice, Noul, Score

response = client.system_one(
    state={
        "subject": "Charged twice",
        "body": "I was charged twice this month and nobody has answered my emails for a week. Please refund one of the payments.",
    },
    questions={
        "refund": Noul(instructions="The customer is asking for a refund."),
        "team": Choice(
            instructions="Which team should handle this ticket?",
            criteria={
                "billing": "Payments, invoices and refunds.",
                "technical": "Bugs and outages.",
                "sales": "Upgrades and quotes.",
            },
        ),
        "frustration": Score(
            instructions="How frustrated is the customer?",
            criteria=["Calm.", "Annoyed.", "Angry."],
        ),
    },
)
print(response.nouls["refund"].noul)           # 0.98
print(response.choices["team"].choice)         # billing
print(response.scores["frustration"].score)    # 1.37
```

```json
{
  "model": "celeris-1-decision",
  "answers": {
    "refund": {"type": "noul", "noul": 0.98},
    "team": {
      "type": "choice",
      "choice": "billing",
      "probabilities": {"billing": 0.97, "technical": 0.02, "sales": 0.01},
      "confidence": 0.96
    },
    "frustration": {
      "type": "score",
      "score": 1.37,
      "probabilities": {"0": 0.03, "1": 0.57, "2": 0.40},
      "confidence": 0.36,
      "legend": {"0": "Calm.", "1": "Annoyed.", "2": "Angry."}
    }
  },
  "usage": {"input_tokens": 231, "output_tokens": 0}
}
```

### Celeris extensions

Celeris adds options that System One does not have. They go in one optional
top-level object, `x_celeris`. The Jev SDK sends it through `extra_body`.

#### Explanations

Set `explain` to `true` to get one sentence per question on why the model
picked its answer. The sentences come back in `x_celeris.explanations`, keyed
by question name. Explanations make the request slower.

**cURL**

```bash
curl https://inference.celeris.ai/celeris-1-decision/v1/systemone \
  -H "Authorization: Bearer $CELERIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "celeris-1-decision",
    "state": "I was charged twice this month and nobody has answered my emails for a week.",
    "questions": {
      "team": {
        "type": "choice",
        "instructions": "Which team should handle this ticket?",
        "criteria": {
          "billing": "Payments, invoices and refunds.",
          "technical": "Bugs and outages.",
          "sales": "Upgrades and quotes."
        }
      }
    },
    "x_celeris": {"explain": true}
  }'
```

**Jev SDK**

```python
from typesafe_sdk import Choice, SystemOneResponse

# The SDK's own response type drops fields it does not declare.
class Explained(SystemOneResponse):
    x_celeris: dict | None = None

response = client.system_one(
    state="I was charged twice this month and nobody has answered my emails for a week.",
    questions={
        "team": Choice(
            instructions="Which team should handle this ticket?",
            criteria={
                "billing": "Payments, invoices and refunds.",
                "technical": "Bugs and outages.",
                "sales": "Upgrades and quotes.",
            },
        ),
    },
    extra_body={"x_celeris": {"explain": True}},
    response_model=Explained,
)
print(response.x_celeris["explanations"]["team"])
```

```json
{
  "model": "celeris-1-decision",
  "answers": {
    "team": {
      "type": "choice",
      "choice": "billing",
      "probabilities": {"billing": 0.97, "technical": 0.02, "sales": 0.01},
      "confidence": 0.96
    }
  },
  "usage": {"input_tokens": 128, "output_tokens": 15},
  "x_celeris": {
    "explanations": {
      "team": "The customer reports being charged twice, which is a payments and billing issue."
    }
  }
}
```

#### Images

Attach images in `images`, an object that maps a name to a
[base64 data URL](/images#encoding-the-image). Images are part of the state,
and your state and questions can refer to one by its name.

`max_soft_tokens` sets the [resolution](/images#image-resolution) the model
reads every image in the request at. It takes the values listed there.

**cURL**

```bash
IMAGE_B64=$(base64 < receipt.png | tr -d '\n')

curl https://inference.celeris.ai/celeris-1-decision/v1/systemone \
  -H "Authorization: Bearer $CELERIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "celeris-1-decision",
    "state": {"claim": {"amount": 13.50, "category": "meals"}},
    "questions": {
      "matches": {"type": "noul", "instructions": "The receipt total matches the claimed amount."}
    },
    "x_celeris": {
      "images": {"receipt": "data:image/png;base64,'"$IMAGE_B64"'"},
      "max_soft_tokens": 560
    }
  }'
```

**Jev SDK**

```python

from typesafe_sdk import Noul

with open("receipt.png", "rb") as f:
    image_b64 = base64.b64encode(f.read()).decode()

response = client.system_one(
    state={"claim": {"amount": 13.50, "category": "meals"}},
    questions={
        "matches": Noul(instructions="The receipt total matches the claimed amount."),
    },
    extra_body={
        "x_celeris": {
            "images": {"receipt": f"data:image/png;base64,{image_b64}"},
            "max_soft_tokens": 560,
        }
    },
)
print(response.nouls["matches"].noul)
```

## OpenAI Decisions API

Send your data as `input` and a list of `questions` to `POST /v1/decisions`.
The endpoint uses the [OpenAI Decisions API format](https://developers.openai.com/api/reference/python/resources/decisions/methods/create).
Point the OpenAI SDK at the Celeris base URL, supply your Celeris API key, and
set `model` to `celeris-1-decision`.

### A yes/no question
**cURL**

```bash
curl https://inference.celeris.ai/celeris-1-decision/v1/decisions \
  -H "Authorization: Bearer $CELERIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "celeris-1-decision",
    "input": "I was charged twice this month. Please refund one of the payments.",
    "questions": [
      {
        "type": "predicate",
        "name": "refund",
        "instructions": "The customer is asking for a refund."
      }
    ]
  }'
```

**OpenAI Python SDK**

```python
# pip install 'openai>=3.26.0'

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["CELERIS_API_KEY"],
    base_url="https://inference.celeris.ai/celeris-1-decision/v1",
)

response = client.decisions.create(
    model="celeris-1-decision",
    input="I was charged twice this month. Please refund one of the payments.",
    questions=[
        {
            "type": "predicate",
            "name": "refund",
            "instructions": "The customer is asking for a refund.",
        }
    ],
)
print(response.answers[0].probability)  # 0.95
```

```json
{
  "model": "celeris-1-decision",
  "answers": [
    {"type": "predicate", "name": "refund", "probability": 0.95}
  ],
  "usage": {
    "input_tokens": 122,
    "input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
    "output_tokens": 0,
    "output_tokens_details": {"reasoning_tokens": 0},
    "total_tokens": 122
  }
}
```

`probability` is the chance that the statement is true, from `0` to `1`.
The responses shown on this page are examples; values can vary.

The OpenAI Python SDK needs version `3.26.0` or later for `client.decisions`.
Include `/v1` in its `base_url`.

### Question types
One request can mix question types. Give each question `instructions` that
describe what to evaluate. A `name` is optional; when supplied, it must be
nonempty and unique within the request. Answers follow the order of your
questions and echo any names you supplied.

| `type` | Answer |
| --- | --- |
| `predicate` | `probability`: the probability that the statement is true. |
| `choice` | `choice`: the most likely value. `probabilities`: a list of values and their probabilities. |
| `score` | `score`: the average level index, weighted by probability. `probabilities`: a list with each level's numeric `value`, `label`, and probability. |

A `choice` question needs `choices`, at least two objects with a `value` and
optional `description`. A `score` question needs `levels`, at least two
objects with a `label` and optional `description`, lowest first.

Score levels are numbered from `0`. `choice` and `score` answers also carry
`confidence`, the top probability rescaled so that `0` is an even split
across the options and `1` is certainty.

Choice values can be strings or booleans, and the response preserves their
types. Values must be unique. A single question cannot include both `true`
and `"true"`, or both `false` and `"false"`.

**cURL**

```bash
curl https://inference.celeris.ai/celeris-1-decision/v1/decisions \
  -H "Authorization: Bearer $CELERIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "celeris-1-decision",
    "input": "I was charged twice this month and nobody has answered my emails for a week. Please refund one of the payments.",
    "questions": [
      {"type": "predicate", "name": "refund", "instructions": "The customer is asking for a refund."},
      {
        "type": "choice",
        "name": "team",
        "instructions": "Which team should handle this ticket?",
        "choices": [
          {"value": "billing", "description": "Payments, invoices and refunds."},
          {"value": "technical", "description": "Bugs and outages."},
          {"value": "sales", "description": "Upgrades and quotes."}
        ]
      },
      {
        "type": "score",
        "name": "frustration",
        "instructions": "How frustrated is the customer?",
        "levels": [{"label": "Calm"}, {"label": "Annoyed"}, {"label": "Angry"}]
      }
    ]
  }'
```

**OpenAI Python SDK**

```python
response = client.decisions.create(
    model="celeris-1-decision",
    input="I was charged twice this month and nobody has answered my emails for a week. Please refund one of the payments.",
    questions=[
        {
            "type": "predicate",
            "name": "refund",
            "instructions": "The customer is asking for a refund.",
        },
        {
            "type": "choice",
            "name": "team",
            "instructions": "Which team should handle this ticket?",
            "choices": [
                {"value": "billing", "description": "Payments, invoices and refunds."},
                {"value": "technical", "description": "Bugs and outages."},
                {"value": "sales", "description": "Upgrades and quotes."},
            ],
        },
        {
            "type": "score",
            "name": "frustration",
            "instructions": "How frustrated is the customer?",
            "levels": [{"label": "Calm"}, {"label": "Annoyed"}, {"label": "Angry"}],
        },
    ],
)
print(response.answers[0].probability)  # 0.98
print(response.answers[1].choice)       # billing
print(response.answers[2].score)        # 1.37
```

The `answers` list for this request looks like this:

```json
[
  {"type": "predicate", "name": "refund", "probability": 0.98},
  {
    "type": "choice",
    "name": "team",
    "choice": "billing",
    "probabilities": [
      {"value": "billing", "probability": 0.97},
      {"value": "technical", "probability": 0.02},
      {"value": "sales", "probability": 0.01}
    ],
    "confidence": 0.96
  },
  {
    "type": "score",
    "name": "frustration",
    "score": 1.37,
    "probabilities": [
      {"value": 0, "label": "Calm", "probability": 0.03},
      {"value": 1, "label": "Annoyed", "probability": 0.57},
      {"value": 2, "label": "Angry", "probability": 0.40}
    ],
    "confidence": 0.36
  }
]
```

### Celeris extensions
Celeris options go in the optional top-level `x_celeris` object. The OpenAI
SDK sends it through `extra_body`.

#### Explanations
Set `x_celeris.explain` to `true` to get one sentence per question explaining
the answer. The response adds `x_celeris.explanations`, a list in the same
order as `answers`. Explanations make the request slower.

**cURL**

```bash
curl https://inference.celeris.ai/celeris-1-decision/v1/decisions \
  -H "Authorization: Bearer $CELERIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "celeris-1-decision",
    "input": "I was charged twice this month. Please refund one of the payments.",
    "questions": [
      {"type": "predicate", "name": "refund", "instructions": "The customer is asking for a refund."}
    ],
    "x_celeris": {"explain": true}
  }'
```

**OpenAI Python SDK**

```python
response = client.decisions.create(
    model="celeris-1-decision",
    input="I was charged twice this month. Please refund one of the payments.",
    questions=[
        {
            "type": "predicate",
            "name": "refund",
            "instructions": "The customer is asking for a refund.",
        }
    ],
    extra_body={"x_celeris": {"explain": True}},
)
print(response.model_extra["x_celeris"]["explanations"][0])
```

The added response field looks like this:

```json
{
  "x_celeris": {
    "explanations": ["The customer explicitly asks for one of the payments to be refunded."]
  }
}
```

#### Images
For images, send `input` as a list of `user` messages with `input_text` and
`input_image` content parts. An image's `image_url` must be a
[base64 data URL](/images#encoding-the-image). External URLs and file IDs are
not supported.

**cURL**

```bash
IMAGE_B64=$(base64 < receipt.png | tr -d '\n')

curl https://inference.celeris.ai/celeris-1-decision/v1/decisions \
  -H "Authorization: Bearer $CELERIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "celeris-1-decision",
    "input": [{
      "role": "user",
      "content": [
        {"type": "input_text", "text": "The expense claim is for $13.50 in meals."},
        {"type": "input_image", "image_url": "data:image/png;base64,'"$IMAGE_B64"'"}
      ]
    }],
    "questions": [
      {"type": "predicate", "name": "matches", "instructions": "The receipt total matches the claimed amount."}
    ],
    "x_celeris": {"max_soft_tokens": 560}
  }'
```

**OpenAI Python SDK**

```python

with open("receipt.png", "rb") as f:
    image_b64 = base64.b64encode(f.read()).decode()

response = client.decisions.create(
    model="celeris-1-decision",
    input=[
        {
            "role": "user",
            "content": [
                {"type": "input_text", "text": "The expense claim is for $13.50 in meals."},
                {"type": "input_image", "image_url": f"data:image/png;base64,{image_b64}"},
            ],
        }
    ],
    questions=[
        {
            "type": "predicate",
            "name": "matches",
            "instructions": "The receipt total matches the claimed amount.",
        }
    ],
    extra_body={"x_celeris": {"max_soft_tokens": 560}},
)
print(response.answers[0].probability)
```

`x_celeris.max_soft_tokens` sets the [resolution](/images#image-resolution)
for every image in the request. The image `detail` field accepts `low`,
`high`, `auto`, or `original`, but does not change the resolution.

### Input and compatibility
`input` accepts a string or a list of `user` messages. A message's `content`
can be a string or a list of text and image parts. For structured data,
serialize it to a string, such as `input=json.dumps(ticket)` in Python.
Other message roles, tools, audio, and files are not supported.

The model reads all text parts in order, joined with blank lines. It reads
images separately in the order you sent them, named `image_1`, `image_2`,
and so on. With several images, refer to those names in your questions.
Text next to an image is not kept attached to it.

`safety_identifier` accepts a string or `null` and has no effect on the
request. Unknown fields, including `stream`, return a validation error.

## Request limits

- Up to 512 questions per request, or 64 with `x_celeris.explain: true`.
- Up to 512 options per `choice` question.
- Up to 64 levels per `score` question.
- Up to 8 images per request, each at most 25 megapixels, or 25,000,000 pixels.
- Up to 100,000 JSON values per request body.

Long question names can require smaller batches, even within these counts.
These limits apply to both endpoints. On `/v1/decisions`, the option limit
applies to `choices` and the score level limit applies to `levels`.
Exceeding a limit returns [`400 invalid_request`](/errors#decisions-validation)
on `/v1/decisions`, or a [`422`](/errors#422) with details in `msg` on
`/v1/systemone`.
The maximum request body size is 8 MiB; larger bodies return a
[`413`](/errors#413).

## Usage and cost

The `usage` block reports `input_tokens` and `output_tokens`. Input tokens are
billed at the [`celeris-1-decision` rate](/pricing#rates), and output tokens are
free. Images count as input tokens.

On `/v1/decisions`, `usage` also includes `total_tokens`, the sum of input
and output tokens. Its cache and reasoning token detail fields are present
and set to zero.

`input_tokens` covers everything the model reads for the request, which is more
than your state and questions alone. Read it from the response instead of
estimating it from your payload. The yes/no request above reported 122 input
tokens, costing US$0.00000488.

Use the [`Server-Timing` header](/measuring-latency) to measure server
processing time.

## Errors

On `/v1/decisions`, an invalid request returns
[`400 invalid_request`](/errors#decisions-validation) in the OpenAI error
envelope. The message identifies the problem and `error.param` names the
top-level field, or is `null` for a problem with the whole body. The OpenAI
Python SDK raises `BadRequestError` for this status.

On `/v1/systemone`, an invalid request returns a [`422`](/errors#422) with a
`detail` array. This includes malformed JSON, an unrecognised field such as
`stream`, or a `model` other than `celeris-1-decision`.

See [Errors](/errors#400) for `400 request_rejected` and how to correct the
request. Statuses such as `401`, `402`, `413`, `429`, and `502` behave as
described in [Errors](/errors), and [rate limits](/rate-limits) apply to both
endpoints as for any other model.
