BBramaDocs
Browse documentation

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

TypeDeclaresAnswers
noulinstructions, and no criterianoul, the probability the question is true, and confidence
choicecriteria as an object of option labels, each described by a string or nullchoice, one of those labels, with probabilities over all of them and confidence
scorecriteria as an ordered array of levels, lowest firstscore, 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 noul is the mass on true, and its confidence the larger of the two sides;
  • a choice is the label with the most mass, its distribution normalized to sum to one, and its confidence that mass;
  • a score is 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.

AliasDestination
decision-modelone 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-modela 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/completions by name, because the typed answer is the whole promise and that endpoint cannot make it.
  • A selector. any, any-vision-capable and task:<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.