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 | POST /v1/systemone |
| OpenAI SDK | 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 runs against it once you change
the base URL, key, and model name.
A yes/no question
- cURL
- Jev SDK
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."}
}
}'
# pip install typesafe-sdk
import os
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
{
"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:
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
- Jev SDK
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."]
}
}
}'
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
{
"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
- Jev SDK
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}
}'
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"])
{
"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 are part of the state,
and your state and questions can refer to one by its name.
max_soft_tokens sets the resolution the model
reads every image in the request at. It takes the values listed there.
- cURL
- Jev SDK
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
}
}'
import base64
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.
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
- OpenAI Python SDK
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."
}
]
}'
# pip install 'openai>=3.26.0'
import os
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
{
"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
- OpenAI Python SDK
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"}]
}
]
}'
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:
[
{"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
- OpenAI Python SDK
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}
}'
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:
{
"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. External URLs and file IDs are
not supported.
- cURL
- OpenAI Python SDK
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}
}'
import base64
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
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
choicequestion. - Up to 64 levels per
scorequestion. - 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
on /v1/decisions, or a 422 with details in msg on
/v1/systemone.
The maximum request body size is 8 MiB; larger bodies return a
413.
Usage and cost
The usage block reports input_tokens and output_tokens. Input tokens are
billed at the celeris-1-decision rate, 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 to measure server
processing time.
Errors
On /v1/decisions, an invalid request returns
400 invalid_request 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 with a
detail array. This includes malformed JSON, an unrecognised field such as
stream, or a model other than celeris-1-decision.
See Errors for 400 request_rejected and how to correct the
request. Statuses such as 401, 402, 413, 429, and 502 behave as
described in Errors, and rate limits apply to both
endpoints as for any other model.