Decisions
POST /v1/decisions: typed questions about a state, answered with calibrated distributions instead of generated text. Kai, or Jev through the same wire.
After this page you can ask typed questions about any state, read the distributions that come back, and choose between Kai and Jev with one field.
A decision is a bounded question: which team, is this a bug, how urgent. The
answer space is known in advance, so the model returns a probability over it
and generates nothing; usage.output_tokens is 0 for Kai. Hanzo Decision
is the endpoint that serves these questions:
POST https://api.hanzo.ai/v1/decisionsThe wire is OpenRouter's Decisions API. A client written for Jev works
unchanged: set its base URL to https://api.hanzo.ai/v1 and send a Hanzo key.
Migrate from Jev lists every field that changes,
with the same request sent both ways.
Request
curl https://api.hanzo.ai/v1/decisions \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kai",
"state": {"message": "I was charged twice for my March invoice and the second charge is still pending."},
"questions": {
"is_bug": {"type": "noul", "instructions": "Is this a product defect?",
"criteria": {"true": "a defect", "false": "working as intended"}},
"team": {"type": "choice", "instructions": "Which team should handle it?",
"criteria": {"account": "logins and profiles",
"payments": "charges, invoices and refunds",
"product": "features and bugs"}},
"urgency": {"type": "score", "instructions": "How urgent is it?",
"criteria": ["later", "this week", "today", "now"]}
}
}'| Field | Type | Limit |
|---|---|---|
model | kai, typesafe/jev-1.13 or ~typesafe/jev-latest | required |
state | a string, object or array: whatever the questions are about | required; Kai reads each question with the state beside it, at most 1,024 tokens together (a string as is, anything else as its JSON text) |
questions | {name: question}; answers come back under the same names, in the same order | required; 1 to 100 |
session_id, user | strings | optional; at most 256 characters each |
provider, trace | any JSON | optional; forwarded to OpenRouter on a Jev request |
A question is {type, instructions, criteria}. instructions is the text the
model answers. criteria depends on the type.
The three types
choice: one label of many
criteria maps each label to its description. A list of labels is also
accepted, for labels that need no description.
"team": {"type": "choice", "instructions": "Which team should handle it?",
"criteria": {"account": "logins and profiles",
"payments": "charges, invoices and refunds",
"product": "features and bugs"}}"team": {"type": "choice", "choice": "payments", "confidence": 0.91,
"probabilities": {"account": 0.03, "payments": 0.94, "product": 0.03},
"answer_confidence": 0.94}choice is the most probable label. probabilities covers every label, in
the order the question gave them.
noul: does a statement hold
criteria is optional: {"true": …, "false": …}, either side or both,
describes what each side means. Any other key is refused.
"is_bug": {"type": "noul", "instructions": "Is this a product defect?",
"criteria": {"true": "a defect", "false": "working as intended"}}"is_bug": {"type": "noul", "noul": 0.12, "confidence": 0.76, "answer_confidence": 0.88}noul is P(true). confidence is |2p − 1|: 0 at even odds, 1 at certainty
either way. answer_confidence is max(p, 1 − p).
score: an ordinal level
criteria is the list of levels, lowest first. Every level needs a
description.
"urgency": {"type": "score", "instructions": "How urgent is it?",
"criteria": ["later", "this week", "today", "now"]}"urgency": {"type": "score", "score": 1.95, "confidence": 0.4667,
"legend": {"0": "later", "1": "this week", "2": "today", "3": "now"},
"probabilities": {"0": 0.05, "1": 0.15, "2": 0.6, "3": 0.2},
"answer_confidence": 0.6}score is the expected level, Σ i·pᵢ. legend and probabilities are keyed
by level index. The most probable level is the key of the largest probability,
here "2", today.
The numbers in these examples show the shape and the arithmetic. They are not a recorded answer.
Response
{
"id": "dec_5f0c2a9e7b1d4c38a6e2f90b1c7d3e44",
"model": "kai",
"provider": "Hanzo",
"answers": {
"is_bug": {"type": "noul", "noul": 0.12, "confidence": 0.76, "answer_confidence": 0.88},
"team": {"type": "choice", "choice": "payments", "confidence": 0.91,
"probabilities": {"account": 0.03, "payments": 0.94, "product": 0.03},
"answer_confidence": 0.94},
"urgency": {"type": "score", "score": 1.95, "confidence": 0.4667,
"legend": {"0": "later", "1": "this week", "2": "today", "3": "now"},
"probabilities": {"0": 0.05, "1": 0.15, "2": 0.6, "3": 0.2},
"answer_confidence": 0.6}
},
"usage": {"input_tokens": 212, "output_tokens": 0},
"routing": {
"backend": "kai",
"checkpoint": "kai-1",
"sha256": "<SHA-256 of the weights>",
"calibration": "cal_<16 hex digits>",
"device": "cpu",
"reach": 1024,
"trained": 1024,
"positions": 8192,
"extrapolated": false,
"reason": "explicit model='kai'"
},
"state_hash": "sha256:<64 hex digits>"
}Every response also carries latency_ms. The example leaves it out because it
is a measurement, not part of the shape.
id, model, provider, answers and usage are the Decisions API's
fields, and so are an answer's choice, noul, score, confidence,
probabilities and legend. confidence, on a choice or score, is
(n·p_max − 1)/(n − 1) over n options: 0 when all are equally likely, 1 when
one takes all the mass, so one threshold means the same thing for 3 options or
30. Kai's probabilities are not rounded.
Hanzo adds four fields, which a Decisions client ignores:
| Field | Meaning |
|---|---|
answer_confidence | on each answer: p_max, the calibrated probability of the reported answer |
routing | the backend (kai or openrouter), the checkpoint (the served Kai build, kai-1), the SHA-256 of its weights, the hash of the calibration tables, the device, the tokens a question and its state may reach together (reach, beside the length the checkpoint was trained at and the encoder's positions; extrapolated when reach passes them), and why the request went there |
state_hash | SHA-256 of the state as the model read it, so two decisions over one input are provably over one input |
latency_ms | wall time of the decision, in milliseconds |
A caller acts on an answer whose confidence clears its threshold and
escalates the rest, to a person or a generative model, rather than trusting a
number the model does not stand behind.
Models
model | Served by | Answers |
|---|---|---|
kai | Hanzo, in process (Kai) | provider: "Hanzo"; usage.output_tokens 0; routing names the checkpoint, weights hash and calibration |
typesafe/jev-1.13, ~typesafe/jev-latest | TypeSafe's Jev, through OpenRouter's Decisions API | provider as upstream reports it; usage as upstream bills it; routing.upstream is the upstream generation id |
A Jev request goes upstream as it came in, provider, session_id, user and
trace included. The answer is checked before it is returned: every question
must come back answered, with its own type and, for a choice, a label the
question has; a choice's probabilities are put back in label order. An answer
that fails the check is a 502.
Pricing
A decision is billed to the org of the key that asked, once per answered call,
and each call is a row in the org's usage (GET /v1/billing/usage, model
kai).
model | Price |
|---|---|
kai | $0.021 per million input tokens (usage.input_tokens); output is free |
typesafe/jev-1.13, ~typesafe/jev-latest | $0.042 per million input tokens; output is free |
Each rate is its row in the model catalogue every model is billed from:
GET https://api.hanzo.ai/v1/models lists kai with pricing.input_per_million
0.021 (pricing.prompt "0.000000021" per token) and pricing.output_per_million
0, and typesafe/jev-1.13 with 0.042 and 0. A refused or failed call is not
billed.
Errors
The gateway every model endpoint shares answers first:
| Status | When |
|---|---|
401 | no Authorization: Bearer <key>, or a key that does not resolve |
402 | the org has no plan that covers paid models (code plan_required, with a link to pick one), or its balance cannot cover the call |
403 | a publishable key (pk-): a decision takes a secret key (sk-) |
429 | the key sent requests faster than its per-minute rate (Retry-After gives the seconds to wait), or a plan's usage window is full (the message says when it resets) |
Their bodies are the gateway's, in one of two shapes ({"status": "error", "msg": "…"} or {"error": {"message", "type", "code"}}); the status is the
contract. Past the gateway, every error is {"error": {"code", "message"}}:
code is the status, or a name where one says more (below), and the message
names what to fix, from the field it is about.
| Status | When | Example message |
|---|---|---|
400 | the body is not JSON, or repeats a key | body: … |
400 | the model is not one of the ids above | unknown model "gpt-5"; use one of hanzo/kai, kai, typesafe/jev-1.13, ~typesafe/jev-latest |
422 | a required field is missing | questions: Field required |
422 | more than 100 questions | questions: 'questions' takes at most 100 questions, got 101 |
422 | a question is malformed | questions.team.choice.criteria: a choice needs at least 2 labels, got 0 |
422 | state is a number or a boolean | state: Input should be text, an object or an array |
422, code state_too_long, question_too_long, option_too_long or request_too_long | a question and its state, a question, an option or the body is longer than Kai reads | the state and question "q" take 8903 tokens; kai-0834a74f2d14 reads at most 1024 |
429 | Kai's queue for the model is full | … has a full queue; retry in … ms, with Retry-After |
529 | the work queued ahead would not start within 10 seconds | … is overloaded; retry in … ms, with Retry-After |
503 | the model is known but not served by this deployment | model "kai" is not served here: … |
503, code upstream_unavailable | Jev's upstream refused Hanzo's account | typesafe/jev-1.13 is temporarily unavailable, with Retry-After |
upstream 400, 404, 422 | Jev refused the request; its status and message pass through | upstream 422: … |
502 | Jev failed, could not be reached, refused with another status, or answered in a shape that fails the check | upstream 502: answer "team" is missing |
500 | a fault in the runtime | decision failed; the detail stays in the operator's log |
Kai covers the model, how it was measured, and Decision Programs: questions composed into a graph that policy governs.