Decision¶
A typed answer instead of generated text. A caller sends a state and a set of typed questions to a decision alias and receives one typed answer per question — a probability, one of the options it declared, or a level of the rubric it gave — with the distribution that answer was taken from. Code acts on the answer directly; nothing parses prose.
The endpoint¶
POST /v1/decisions carries exactly three fields. An unknown field is
refused by name, as everywhere else in this gateway.
{
"model": "decision-model",
"state": "Help! My payouts have been failing for 3 days.",
"questions": {
"is_urgent": {"type": "noul", "instructions": "Does this convey urgency?"},
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Payments, invoicing, refunds",
"technical": "Bugs, outages, integrations",
"sales": "Pricing, upgrades, new accounts"
}
},
"severity": {
"type": "score",
"instructions": "How severe is this for the customer?",
"criteria": ["cosmetic", "annoying", "blocking", "outage"]
}
}
}
state is a string or any other JSON value; an object travels to the model
as its pretty-printed JSON. Every question is answered against that one
state, in one provider call.
The three primitives¶
| Type | Declares | Answers |
|---|---|---|
noul | instructions, and no criteria | noul, the probability the question is true, and confidence |
choice | criteria as an object of option labels, each described by a string or null | choice, one of those labels, with probabilities over all of them and confidence |
score | criteria as an ordered array of levels, lowest first | score, the probability-weighted level index, with probabilities, confidence and the legend it was graded against |
The limits are what one call can carry honestly: at most thirty-two questions, each choice or score declaring between two and thirty-two labels, instructions and rubric lines at most four kilobytes, a question key at most one hundred and twenty-eight bytes, and a state at most one hundred and twenty-eight kilobytes of JSON. Every one of them is checked before any provider is contacted, and the refusal names the question it is about.
The answer¶
One real answer, served through openrouter/openai/gpt-4o-mini:
{
"model": "decision-model",
"route": "openrouter/openai/gpt-4o-mini",
"engine": "chat-distribution",
"answers": {
"is_urgent": {"noul": 1.0, "confidence": 1.0},
"department": {
"choice": "billing",
"probabilities": {"billing": 0.7, "sales": 0.1, "technical": 0.2},
"confidence": 0.7
},
"severity": {
"score": 2.1,
"probabilities": {"0": 0.0, "1": 0.2, "2": 0.5, "3": 0.3},
"confidence": 0.5,
"legend": ["cosmetic", "annoying", "blocking", "outage"]
}
},
"usage": {"input_tokens": 247, "output_tokens": 106, "latency_ms": 2514.29}
}
model is the alias the caller named, route the destination that
answered it, and engine which of the two below did the answering. The
answer document has the same shape either way: a caller cannot tell the
engines apart from the answers themselves.
Two engines¶
typesafe-systemone — the provider speaks the decision wire itself.
TypeSafe AI's System One model, Jev, takes a state and typed questions in
this vocabulary and returns typed answers, and Brama passes them through
after checking that every question was answered and that no answer names an
option its question never declared. That provider generates no text at all,
so it carries no chat path: a chat completion naming typesafe/jev-latest
is refused rather than sent to an endpoint that does not exist.
chat-distribution — any chat route. Brama asks the model for the one
thing a decision is made of, the probability mass over the labels each
question declares, and computes the typed answer from that itself:
- a
noulis the mass ontrue, and its confidence the larger of the two sides; - a
choiceis the label with the most mass, its distribution normalized to sum to one, and its confidence that mass; - a
scoreis the probability-weighted level index, over the same normalized distribution, with the rubric as its legend.
The model is never asked to assert the choice or the score, only the
numbers, so it cannot contradict its own distribution. A label is read without
the whitespace around it, because a text model writes a label the way its
tokenizer spells it (" acknowledgement"), and a label the model leaves
out carries no mass. The complete reply must be one JSON object, with no prose
or code fence. A malformed reply is refused as
decision_contract_violated with the JSON parser's reason and position.
An answer naming an undeclared question or label, naming one label twice, or
carrying no probability mass is refused by name. Probabilities, confidences
and scores are returned as computed, with no precision imposed on them. A
native decision provider must return an answers object; a missing or
non-object answers is a contract refusal, not a bare answer.
Its typed answer must name a declared choice or a score inside the rubric.
Optional probability maps must use declared labels and carry mass; malformed
confidence or probability fields are refused with the question and field
rather than silently discarded.
The shape of that answer travels with the request. Brama sends the chat
route a JSON Schema in which every question is a required key whose value
holds exactly that question's labels as numbers and nothing else; on the
OpenAI chat wire it is response_format of type
json_schema, strict, under the name decision; on the
Responses wire (Codex) it is text.format of type
json_schema, strict, under the same name; on the Anthropic Messages
wire (Claude) it is the input schema of one tool named decision that
the request makes the model call, and that call's input is read back as the
answer text. A provider that decodes under a schema then cannot write a label of
one question under another, which a small model told only in the prompt did.
Kimi's coding endpoint, whose accepted fields are fixed, and the Google wire,
which has no such field here, are sent the prompt alone. The schema does not replace the
check above: whatever a route answers is still read against what the caller
declared.
The two aliases¶
model accepts exactly two names, for the same reason the embeddings
endpoint accepts exactly one: the name is the caller's promise of a typed
answer, and which model keeps it is the deployment's business.
| Alias | Destination |
|---|---|
decision-model | one canonical provider/model route this deployment pays for, native or chat. best is refused here: delegation is what the other name is for. |
best-decision-model | a route, or best — which hands the choice to subscription dispatch, where the caller's own signed identity selects the subscription that pays. |
Both are known to the gateway by name and required of no deployment. A gateway that serves no decisions declares no route for them, and a caller that asks anyway is told exactly that, with the repair in the sentence:
503 {"error":{"code":"alias_unserviceable",
"message":"alias `decision-model` is a decision alias this gateway knows but
no route is declared for it; declare one with `brama routes set decision-model
<provider/model>` or `PUT /v1/admin/routes`",
"details":{"state":"no_route","route":null}}}
Declare one with brama routes set or
PUT /v1/admin/routes, read the state back with
brama aliases, and ask one from a shell with
brama decide.
Not to be confused with¶
- A chat completion. A decision alias is refused on
POST /v1/chat/completionsby name, because the typed answer is the whole promise and that endpoint cannot make it. - A selector.
any,any-vision-capableandtask:<name>rank subscription models per caller; a decision alias names one destination like every other alias.
See aliases, the route registry, the HTTP API, or errors.