Decisions
POST /v1/decisions, typed decisions (choice, score, yes/no) over one input in one request, with the request and response shapes, limits, billing, and error codes.
Answer typed questions about a piece of input with a classifier instead of a
generator. You send a state (the text or JSON to decide about) and a set of
questions; each answer comes back as a label or a score with its
probabilities and a confidence. Nothing is generated, so only input tokens are
billed.
POST https://inference.openrelay.inc/v1/decisionsThe model that serves this family is openrelay/laya, built on
Laya by Convai Innovations (Apache
2.0): an encoder-only decision model with an English checkpoint and a
multilingual one. Check a model's supported_apis on
GET /v1/models for decisions; a model that does not
list it answers 400 unsupported_api. Decisions are online only: the
Batch API does not accept /v1/decisions.
Question types
Every question has one of three types:
type | criteria | The answer |
|---|---|---|
choice | The options: an object of label: description, or an array of labels. | choice, the most likely label, and probabilities over every label. |
score | An array of levels, ordered from lowest to highest. | score, the expected level index from 0 to n - 1 (fractional), a legend from index to level, and probabilities per index. |
noul | Optional. An object with false and/or true descriptions. | noul, the probability that the answer is yes. |
Request
| Field | Type | Description |
|---|---|---|
model | string (required) | openrelay/laya. |
state | string, object, or array (required) | The input to decide about. Objects and arrays are serialized to JSON text (with a space after each , and :) before the model reads them. |
questions | object (required) | Question id to question. Between 1 and 64 questions. |
Each question:
| Field | Type | Description |
|---|---|---|
type | string (required) | choice, score, or noul. |
instructions | string, object, or array (required) | What to decide. An object or array is JSON-encoded. |
criteria | object or array | The options, per the table above. Required for choice and score. |
Any other field is rejected with 400 unknown_field, at the top level, inside a
question, or inside noul criteria. That includes checkpoint, task, and
lang: there is no way to pick a checkpoint in the request. stream is dropped:
a decision is always a single JSON response.
Use question ids made of ASCII letters, digits, _, and -. They come back as
the keys of answers, and an error names them in its param field (see
Errors).
Object key order is not preserved
The gateway re-encodes the request body, which sorts the keys of every JSON
object alphabetically. That applies to an object state and to choice
criteria given as an object. If the order of your input matters (for example
because the most important text must come first, see Limits), send
state as a string. answers are keyed by question id, so their order
carries no meaning.
Checkpoint routing
Each request is routed by the script and language of its state. Text in a
non-Latin script goes to the multilingual checkpoint, and so does Latin-script
text detected as a language other than English. Everything else goes to
english: English text, a state with no letters at all, and Latin-script text
whose language is not identified and which has too few non-English letters to
tell. The response reports which checkpoint answered in routing.checkpoint,
and why in routing.reason. The reason is text for your logs; do not parse it.
Because the option limit depends on the checkpoint, read routing.checkpoint
rather than assuming it from the input language.
Response
{
"model": "openrelay/laya",
"answers": {
"department": {
"type": "choice",
"choice": "billing",
"probabilities": { "billing": 0.9731, "technical": 0.0269 },
"confidence": 0.8217,
"action": { "act_probability": 1.0 }
},
"urgency": {
"type": "score",
"score": 1.6993,
"legend": { "0": "not urgent", "1": "soon", "2": "blocking" },
"probabilities": { "0": 0.0499, "1": 0.2009, "2": 0.7492 },
"confidence": 0.3735,
"action": { "act_probability": 1.0 }
},
"churn_risk": {
"type": "noul",
"noul": 0.8244,
"confidence": 0.8244,
"action": { "act_probability": 1.0 }
}
},
"routing": { "checkpoint": "english", "reason": "English Latin text" },
"usage": { "prompt_tokens": 173, "total_tokens": 173, "truncated_tokens": 0 }
}| Field | Description |
|---|---|
model | The public model id you requested. |
answers | One entry per question, keyed by the ids you sent. |
answers.<id>.type | The question's type. |
answers.<id>.choice | choice only: the label with the highest probability. |
answers.<id>.score | score only: the probability-weighted level index, from 0 to n - 1. |
answers.<id>.legend | score only: level index to the level you sent. |
answers.<id>.noul | noul only: the probability that the answer is yes. |
answers.<id>.probabilities | choice and score: the distribution over labels or level indices. |
answers.<id>.confidence | choice and score: one minus the normalized entropy of probabilities (0 is a uniform guess, 1 is certain). noul: the larger of noul and 1 - noul. |
answers.<id>.action.act_probability | The output of Laya's separate action head, from 0 to 1, passed through unchanged. The model's documentation does not describe or calibrate it; evaluate it on your own data before you act on it. |
routing.checkpoint | english or multilingual: the checkpoint that answered. |
routing.reason | Why the router picked it. Informational. |
usage.prompt_tokens | The tokens the model read, summed over questions, which is what you are billed. |
usage.total_tokens | Equal to prompt_tokens. There is no completion_tokens. |
usage.truncated_tokens | The state tokens the model did not read, for the question that read the fewest: the end of the state past the room that the longest type, instructions, and options leave in the per-question token limit. 0 when every question read the whole state, which is the common case (see Limits). Tokens not read are not billed. |
Limits
| Limit | Value | Over the limit |
|---|---|---|
| Questions per request | 64 | 400 too_many_questions (zero is 400 empty_questions) |
Options per choice or score question | (head_max_len - 16) / 4, rounded down: 44 on english (head_max_len 192), 60 on multilingual (256) | 400 too_many_options |
state size | 32 KB (32,768 bytes of serialized UTF-8) | 400 state_too_large |
| Tokens read per question | 8,192 on both checkpoints. For most text the 32 KB state limit comes first, but text that tokenizes densely (digits, base64) can reach 8,192 tokens inside 32 KB, for example a number-heavy JSON state, which routes to english | Not an error: the end of the state is cut, and usage.truncated_tokens says how much (below) |
| Request body | 10 MiB | 413 payload_too_large |
The option limit is checked after routing, so it depends on the checkpoint that will answer: a question with 50 options is accepted on multilingual input and rejected on English input.
How a question is read: each question is encoded as one sequence of the type and
instructions, then every option, then the state, capped at 8,192 tokens. The
instructions and options share a budget of head_max_len tokens (192 on
english, 256 on multilingual), and each option is cut at 48 tokens. When the
options do not fit that budget, every option is silently shortened to
(head_max_len - 16) / n tokens, where n is the number of options, and never
below 4. The instructions then get what is left of the budget, never less than 8
tokens, and are silently cut to it. The option limit above is the largest
count at which all options at 4 tokens each still fit the budget; under it, long
options and instructions are still cut without an error. The state gets the
tokens that remain after the instructions and options, up to about 8,000. That
holds most 32 KB states, but not one that tokenizes densely: a number-heavy JSON
state or base64 can pass it. A state that runs past it is cut at
the end; usage.truncated_tokens is how you see that it happened and how many
tokens went unread. Put what matters first anyway (in a string state; see the
note on key order above): the model reads long inputs, but its accuracy falls
past about 4,000 tokens (see Limits from the model's
authors). A long request, where the number of
questions times the tokens per question passes 65,536, runs as several forwards
over consecutive groups of its questions: the answers equal the one-forward
answers up to numerical noise, and usage is the same. To predict a split, count each
question as the whole instructions-and-options allowance (192 tokens on
english, 256 on multilingual) plus the state tokens plus 4, capped at
8,192: the server splits when that sum over the questions passes 65,536, whether
or not the heads use their full allowance. A request under it runs in one
forward, unchanged.
Billing
openrelay/laya costs $0.020 per 1M input tokens. It has no output rate,
because it generates nothing.
The state is encoded once per question, so a request with ten questions reads
the state about ten times, and bills for it. Asking several questions in one
request saves round trips, not tokens. A request with 64 questions over a
6,300-token state bills about 400,000 input tokens, or
$0.008 at this rate. Billable tokens per request are bounded by questions times
the per-question limit: at most 64 x 8,192 = 524,288 tokens, about $0.0105.
usage.prompt_tokens in the response is the number you are billed on; it is the
source of truth, not any estimate you compute from the request size. A request
that fails with a non-2xx status is not billed. See
Pricing & billing.
Errors
A request the model rejects gets a typed error with a documented code and, in
most cases, a param naming the field:
{
"error": {
"message": "too many options for one question",
"type": "invalid_request_error",
"code": "too_many_options",
"param": "questions.department.criteria"
}
}The codes this family can return, all 400 with type invalid_request_error:
code | message | param |
|---|---|---|
empty_questions | the request has no questions | questions |
too_many_questions | too many questions in one request | questions |
too_many_options | too many options for one question | questions.<id>.criteria |
invalid_question_type | unsupported question type | questions.<id>.type |
state_too_large | the request state is too large | state |
unknown_field | the request carries a field this API does not define | The field's path, e.g. checkpoint or questions.<id>.critera |
invalid_request | the request is not valid for this API | The field's path, when there is one. Covers every other shape error: a state that is not a string, object, or array, missing instructions, empty or non-string choice labels, or a score whose criteria is not a list. |
param is a dotted path into your own request, with [n] for array indices.
It is present only when that path exists in what you sent, so a missing field
(for example a question with no instructions) comes back without a param.
It is also omitted when the path is longer than 128 bytes or passes through a
question id that contains a dot, a space, or any character other than ASCII
letters, digits, _, and -.
Every other error is the gateway's, listed on Errors:
401 for the key, 402 for the balance, 404 for an unknown model,
400 unsupported_api for a model that does not serve decisions, 413 for the
body size, 429 when the model's queue is full, and 502 or 503 while the
model is restarting or has no ready capacity. Retry 429, 502, and 503 with
backoff (after the Retry-After header on a 429); do not retry a 400
unchanged.
Limits from the model's authors
The left column comes from the Laya README, not from our measurements:
| Limit | What it means for you |
|---|---|
| The base checkpoints are "near chance on typed-decisions zero-shot" (0.362 and 0.352 against a 0.318 random baseline). | Laya is a fast base to specialize, not a zero-shot decision engine. Measure accuracy on your own labeled data before you rely on it. The fine-tuned typed-decisions checkpoint is not served. |
| The multilingual checkpoint ships with no calibration (unit temperatures), and the authors report that both checkpoints are over-confident as shipped. | Treat probabilities and confidence as uncalibrated on either checkpoint. Pick any confidence threshold from your own labeled data. |
More than about 20 choice options degrades sharply at the default budgets. | The option limit (44 or 60) prevents the worst case, not the degradation. Split a large label set into a coarse question and a fine one, or shortlist the labels yourself before you call. |
| Options are silently truncated when they do not fit the budget. | Keep option labels and descriptions short and distinct in their first few tokens. See Limits. |
| On long inputs, 16 to 18 of 20 answers were correct with up to about 4,000 tokens before the relevant text, and 8 to 17 of 20 beyond that (multilingual checkpoint, the authors' long-context benchmark). | Long inputs are read, but past about 4,000 tokens the model can miss what it needs. In one probe of our own on the english checkpoint, it found a decisive sentence at the end of a 1,500-token and a 4,000-token state, and missed it at 7,000. Put what matters first, and check accuracy past about 4,000 tokens on your own data. |
Examples
curl https://inference.openrelay.inc/v1/decisions \
-H "Authorization: Bearer $OPENRELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openrelay/laya",
"state": {
"subject": "Duplicate charge on invoice #4411",
"body": "I was charged twice. If this is not fixed today I will cancel."
},
"questions": {
"department": {
"type": "choice",
"instructions": "Which department handles this?",
"criteria": { "billing": "invoices, refunds", "technical": "bugs, outages" }
},
"urgency": {
"type": "score",
"instructions": "How urgent?",
"criteria": ["not urgent", "soon", "blocking"]
},
"churn_risk": {
"type": "noul",
"instructions": "Does the user threaten to leave?"
}
}
}'