Skip to navigation

Decisions API

Calibrated probabilities for yes/no, choice, score, and tool questions about text, images, audio, and video

Decision models answer typed questions about content you send. You send a state (text, JSON, images, audio, or video) and a set of questions, and each answer comes back as probabilities with a confidence value. Decision models never generate text.

Decision models are available through the API at POST /v1/decisions, and aplomb-1 also answers the chat formats: OpenAI Chat Completions and Responses, Anthropic Messages, and Google Gemini.

Endpoint

POST https://api.empiriolabs.ai/v1/decisions

Authenticate with your API key in the Authorization header:

Authorization: Bearer $EMPIRIOLABS_API_KEY

The full request and response schema is in the API reference.

Models

ModelInputContext
aplomb-1Text, JSON, images, audio, videoUp to 1,000,000 tokens

aplomb-1 accepts every input type in the same state and supports the not-in-the-state answer.

Your first request

This request asks three questions about one support message: whether it is urgent, which team should handle it, and how frustrated the customer is.

curl https://api.empiriolabs.ai/v1/decisions \
-H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "aplomb-1",
"state": {
"channel": "email",
"message": "I was charged twice for my subscription this month. This is the third time I have written about it. Please refund the duplicate charge today."
},
"questions": {
"is_urgent": {
"type": "noul",
"instructions": "Does the customer need a resolution today?"
},
"department": {
"type": "choice",
"instructions": "Which team should handle this message?",
"criteria": {
"billing": "Charges, refunds, and invoices",
"technical": "Bugs, errors, and outages",
"sales": "Plans, pricing, and upgrades"
}
},
"frustration": {
"type": "score",
"instructions": "How frustrated is the customer?",
"criteria": ["Calm", "Frustrated", "Very angry"]
}
}
}'
Response
{
"model": "aplomb-1",
"answers": {
"is_urgent": {"type": "noul", "noul": 0.9412, "confidence": 0.8824},
"department": {
"type": "choice",
"choice": "billing",
"probabilities": {"billing": 0.88, "technical": 0.11, "sales": 0.01},
"confidence": 0.82
},
"frustration": {
"type": "score",
"score": 1.05,
"legend": {"0": "Calm", "1": "Frustrated", "2": "Very angry"},
"probabilities": {"0": 0.02, "1": 0.91, "2": 0.07},
"confidence": 0.865
}
},
"usage": {
"input_tokens": 212,
"output_tokens": 0,
"input_tokens_details": {"text_tokens": 212, "image_tokens": 0, "audio_tokens": 0, "video_tokens": 0}
},
"long_context": "full"
}

Request

FieldTypeRequiredDescription
modelstringYesaplomb-1. An unknown ID returns 404 model_not_found.
statestring, object, or arrayYesThe content to decide about. Media nodes can appear anywhere inside it.
questionsobjectYes1 to 128 questions, keyed by an ID you choose.
long_contextstringNofast (default) or full: how a state longer than 131,072 tokens is read. See Long states.

Question IDs. Answers come back under the same IDs you send. IDs are not shown to the model, so name them for your own code. An ID cannot contain control characters.

Instructions. Every question has a type and instructions. instructions is usually a string. It can also be an object or an array that holds the question in one field and the data it refers to in others.

Independent answers. Each question is answered independently of the other questions in the request.

Question types

Yes or no (noul)

noul asks a yes/no question. The answer’s noul field is the probability that the answer is yes. Add criteria with true and false descriptions when the line between yes and no needs defining:

"refund_request": {
"type": "noul",
"instructions": "Is the customer asking for a refund?",
"criteria": {
"true": "The message asks for money back.",
"false": "The message reports a problem without asking for money back."
}
}
Answer
"refund_request": {"type": "noul", "noul": 0.9721, "confidence": 0.9442}

Choice (choice)

choice picks one of 2 to 255 named options. criteria maps each option name to a description, or to null when the name is enough. The answer’s choice is the most likely option, and probabilities holds the probability of every option. Option names are returned unchanged.

A choice between exactly two yes/no options, such as yes and no, returns the same probability as the equivalent noul question, so the two never disagree.

Score (score)

score rates the state on an ordered scale of 2 to 10 levels, listed lowest first in criteria. Levels are numbered from 0 in that order. The answer’s score is the expected level (the probability-weighted average of the level numbers), legend maps each level number to its description, and probabilities holds the probability of each level.

In the first request above, frustration returned a score of 1.05, with most of the probability on level 1 (Frustrated).

Tool (tool)

tool chooses which function to call. tools is an array of 1 to 254 function definitions in the same format as tools on Chat Completions.

  • Function choice. The answer’s tool is the most likely function, and probabilities holds the probability of each. Function names are returned unchanged, including none.
  • No function. With allow_none set to true (the default), tool can be null. Its probability is probabilities.none, unless a supplied function is named none; in that case the no-function probability is returned separately as none_probability. The function probabilities and any separate none_probability sum to 1. With allow_none: false, only the supplied functions are candidates.
  • Arguments. Every enum and boolean argument of every function gets its own distribution under arguments: which value the argument should take if that function is called. enum arguments return probabilities and boolean arguments return probability_true.
  • Open arguments. Other arguments, such as free text, are listed under open_arguments. Decision models do not fill them; produce them separately if you need them.
Request
{
"model": "aplomb-1",
"state": "Customer: Can you move my Thursday appointment to Friday morning?",
"questions": {
"next_action": {
"type": "tool",
"instructions": "Which function should handle this request?",
"tools": [
{
"type": "function",
"function": {
"name": "reschedule_appointment",
"description": "Move an existing appointment to a new day and time.",
"parameters": {
"type": "object",
"properties": {
"day": {"type": "string", "enum": ["monday", "tuesday", "wednesday", "thursday", "friday"]},
"time_of_day": {"type": "string", "enum": ["morning", "afternoon", "evening"]},
"notify_by_sms": {"type": "boolean"},
"note": {"type": "string"}
},
"required": ["day", "time_of_day"]
}
}
},
{
"type": "function",
"function": {
"name": "cancel_appointment",
"description": "Cancel an existing appointment.",
"parameters": {"type": "object", "properties": {}}
}
}
]
}
}
}
Answer
"next_action": {
"type": "tool",
"tool": "reschedule_appointment",
"probabilities": {"reschedule_appointment": 0.96, "cancel_appointment": 0.01, "none": 0.03},
"confidence": 0.94,
"arguments": {
"reschedule_appointment": {
"day": {
"value": "friday",
"probabilities": {"monday": 0.001, "tuesday": 0.002, "wednesday": 0.007, "thursday": 0.04, "friday": 0.95},
"confidence": 0.9375
},
"time_of_day": {
"value": "morning",
"probabilities": {"morning": 0.93, "afternoon": 0.05, "evening": 0.02},
"confidence": 0.895
},
"notify_by_sms": {"value": false, "probability_true": 0.12, "confidence": 0.76}
}
},
"open_arguments": {"reschedule_appointment": ["note"]}
}

A tool question counts once toward the 128-question limit. Its functions can have at most 64 enum and boolean arguments in total. A request can evaluate at most 512 questions in total, where each tool question counts as one plus one per enum or boolean argument.

Not in the state (abstain)

Add "abstain": true to a noul, choice, or score question to let the model say that the state does not contain what the question needs. The answer then includes abstain, the probability of that outcome. Every other field is the answer given that the state does contain it, so noul is still the probability of yes and probabilities still sum to 1. A choice with abstain takes at most 254 options.

Request
{
"model": "aplomb-1",
"state": {"order": {"id": "B-44120", "status": "shipped", "payment": "unpaid"}},
"questions": {
"paid": {"type": "noul", "instructions": "Has order B-44120 been paid?", "abstain": true},
"carrier": {
"type": "choice",
"instructions": "Which carrier is delivering order B-44120?",
"criteria": {"ups": null, "fedex": null, "dhl": null},
"abstain": true
}
}
}
Answers
"paid": {"type": "noul", "noul": 3.47e-06, "confidence": 0.99999306, "abstain": 5.09e-06},
"carrier": {
"type": "choice",
"choice": "fedex",
"probabilities": {"ups": 0.22482937, "fedex": 0.55308475, "dhl": 0.22208588},
"confidence": 0.32962713,
"abstain": 0.99998646
}

The record says the order is unpaid, so paid is a confident no with a low abstain. It does not name a carrier, so carrier has a high abstain: treat the choice as unanswered rather than acting on it.

Confidence

Every answer includes confidence, from 0 to 1, which measures how concentrated the distribution is. 0 means every option is equally likely; 1 means all probability is on one option. For n options whose highest probability is p, confidence is (n * p - 1) / (n - 1). For a noul answer it is |2p - 1|.

Use confidence as a threshold: act automatically on high-confidence answers and send the rest for review.

Probabilities are rounded to at most 8 decimal places and sum to 1.

Media input

Put images, audio, and video anywhere inside state as media nodes. A media node is an object with a type of image, audio, or video, and either a public https:// URL in url, or the file as base64 in data with its MIME type in mime:

{"type": "image", "url": "https://example.com/listing-photo.png"}
{"type": "image", "data": "<base64>", "mime": "image/png"}
{"type": "audio", "url": "https://example.com/support-call.wav"}
{"type": "video", "url": "https://example.com/dashcam.mp4"}
Request with an image
{
"model": "aplomb-1",
"state": {
"listing_title": "Three-seat leather sofa, like new",
"photo": {"type": "image", "url": "https://example.com/listing-photo.png"}
},
"questions": {
"matches_title": {"type": "noul", "instructions": "Does the photo show the item described in listing_title?"},
"condition": {"type": "score", "instructions": "Rate the condition of the item in the photo.", "criteria": ["Damaged", "Worn", "Good", "Like new"]}
}
}

aplomb-1 accepts images, audio, and video, in any mix, in the same state.

MediaPer requestPer item
ImageUp to 1620 MB
AudioUp to 825 MB and 10 minutes
VideoUp to 4100 MB and 10 minutes

A media URL must be publicly reachable over https://. Other URLs return 400 invalid_media_url; send the file as data instead.

Long states (long_context)

For a state longer than 131,072 tokens, long_context sets how the state is read:

  • fast (default). The model answers in seconds: about 3 seconds for a 1M-token state, against about 111 seconds for a full read. On our long-context test sets (164 documents of 131K to 1M tokens, 525 decisions), fast mode answered every decision correctly, against 95.2% for a full read.
  • full. The model reads every token of the state. Use it when an answer depends on the whole document, such as a count, the overall tone, or confirming that something never appears.

Shorter states. A state of 131,072 tokens or fewer is always read in full, whichever mode you send.

Many questions. A request that evaluates more than 8 questions is read in full. Each tool question counts as one plus one per enum or boolean argument, as in Limits.

Media. Images, audio, and video in the state are read in full in both modes.

Which mode was used. The response’s long_context field is fast or full. A fast request can also come back full.

Usage. The whole state is counted in both modes, at the same price.

Request
{
"model": "aplomb-1",
"state": "<the full text of a long contract>",
"long_context": "full",
"questions": {
"auto_renews": {"type": "noul", "instructions": "Does the contract renew automatically?"}
}
}

Any value other than fast or full returns 422 invalid_request.

Limits

  • Questions. 1 to 128 per request. A tool question counts once toward that limit.
  • Evaluated questions. At most 512 per request, where each tool question counts as one plus one per enum or boolean argument. More returns 422 too_many_questions.
  • Tokens. The state plus the longest question can be up to 1,000,000 tokens.
  • Choice options. 2 to 255 per choice question.
  • Score levels. 2 to 10 per score question.
  • Functions. 1 to 254 per tool question, with at most 64 enum and boolean arguments in total.
  • Media. See the table in Media input.

Usage and billing

Decision requests are billed on input tokens only. usage.input_tokens counts what you send:

  • The state, once per request, however many questions refer to it, and in full in both long_context modes.
  • The text of each question.
  • Each media item: at most 640 tokens per image, at most 256 tokens per video frame, and about 13 tokens per second of audio.

A tool question counts its own instructions and tools once, however many arguments it evaluates.

Nothing else is added to the count, and output_tokens is always 0. input_tokens_details splits input_tokens into text_tokens, image_tokens, audio_tokens, and video_tokens.

Because the state is counted once per request, asking several questions about the same state in one request costs less than sending one request per question. Current rates are on each model page and on the pricing page.

Data retention

Zero data retention is on by default: we do not retain the content of your decision requests or responses.

Errors

Errors return a JSON body with a code and a message. Validation messages name the offending field:

{"error": {"code": "too_many_questions", "message": "At most 128 questions are accepted per request."}}
StatuscodeMeaning
400invalid_media_urlA media URL was refused. Use a publicly reachable https:// URL, or send the file as data.
400unsupported_mediaThe model does not accept a media type in the state.
401missing_api_keyNo API key was sent. Send it as Authorization: Bearer <key>.
401invalid_api_keyThe API key is malformed, inactive, expired, or not found.
402insufficient_creditsThe account balance is too low for the request.
404model_not_foundThe model ID is unknown.
413request_too_largeThe request body is too large. Send large media as url instead of data.
422invalid_requestThe request does not match the schema. The message names the field.
422state_too_largeThe state plus the longest question is over 1,000,000 tokens.
422too_many_questionsThe request has more than 128 questions, or evaluates more than 512.
422invalid_mediaA media item is invalid or over a limit in Media input.
429rate_limit_exceededAn account rate limit was reached. Retry after the number of seconds in Retry-After.
503model_unavailableThe model is temporarily unavailable. Retry after the number of seconds in Retry-After; the request is safe to retry.

See Errors and Status Codes for how the API reports errors across endpoints.

Chat formats

aplomb-1 also answers requests in the OpenAI Chat Completions, OpenAI Responses, Anthropic Messages, and Google Gemini formats, so you can call it from those SDKs and from tools built on them. Use the base URL https://api.empiriolabs.ai/v1 (for Gemini clients, https://api.empiriolabs.ai) and the model ID aplomb-1.

A chat request becomes one decision about the whole conversation:

RequestQuestionReply
Messages onlyThe last user message, as a yes or no questionYes (84%) or No (91%)
answer_type and options (extra body fields)The last user message, as a choice among options, or a score with options as the levels from lowest to highest (1 to 5 when empty). options takes an array or a comma-separated string.billing (97%), 4 out of 5 (62%) or Annoyed (96%)
toolsWhich function to call nextA tool call with the most likely value of every enum and boolean argument
response_format with a JSON schema (structured outputs)One question per property, from its description: a boolean is yes or no, an enum is a choice, and an integer with a minimum and maximum 2 to 10 values apart is a scoreA JSON object with each property’s most likely value
questions (an extra body field)Your questions, exactly as in /v1/decisionsThe answers object, as JSON
  • Images, audio, and video in the messages are part of the state.
  • Every reply carries the full answer, with probabilities, under message.decision. With "logprobs": true, a yes or no reply also returns the probabilities as log probabilities.
  • A JSON schema property that also allows null (for example "type": ["string", "null"], "nullable": true, or null in its enum) comes back null when the input doesn’t hold the answer.
  • Send "abstain": true with a yes or no request to allow the reply The input doesn't say.
  • Free-text tool arguments are not included in the tool call, and a JSON schema property must be a boolean, an enum, or an integer range.
  • Gemini responseSchema and the Responses API’s text.format work the same way as response_format.
  • Streaming is supported. Usage reports the input tokens as prompt_tokens and 0 completion tokens, billed the same as /v1/decisions.
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.empiriolabs.ai/v1", api_key=os.environ["EMPIRIOLABS_API_KEY"])
reply = client.chat.completions.create(
model="aplomb-1",
messages=[{"role": "user", "content": "Has order B-44120 been paid? Order B-44120: status shipped, payment unpaid."}],
)
print(reply.choices[0].message.content) # No (>99.9%)
triage = client.chat.completions.create(
model="aplomb-1",
messages=[{"role": "user", "content": "I was charged twice for order B-44120. Please refund the extra charge."}],
response_format={"type": "json_schema", "json_schema": {"name": "triage", "schema": {"type": "object", "properties": {
"team": {"type": "string", "enum": ["billing", "technical", "sales"], "description": "Which team should handle it?"},
"anger": {"type": "integer", "minimum": 1, "maximum": 5, "description": "How upset is the customer, 1 to 5?"},
"carrier": {"type": ["string", "null"], "enum": ["ups", "fedex", "dhl", None], "description": "Which carrier delivered it?"},
}}}},
)
print(triage.choices[0].message.content) # {"team": "billing", "anger": 2, "carrier": null}

Every Decisions API feature also works from a chat SDK: send a /v1/decisions question map as questions, for example extra_body={"questions": {...}} with the OpenAI SDK.

MCP

MCP clients can call the Decisions API as the decide tool on the EmpirioLabs MCP server.