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/decisions

The 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"]}
    }
  }'
FieldTypeLimit
modelkai, typesafe/jev-1.13 or ~typesafe/jev-latestrequired
statea string, object or array: whatever the questions are aboutrequired; 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 orderrequired; 1 to 100
session_id, userstringsoptional; at most 256 characters each
provider, traceany JSONoptional; 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:

FieldMeaning
answer_confidenceon each answer: p_max, the calibrated probability of the reported answer
routingthe 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_hashSHA-256 of the state as the model read it, so two decisions over one input are provably over one input
latency_mswall 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

modelServed byAnswers
kaiHanzo, in process (Kai)provider: "Hanzo"; usage.output_tokens 0; routing names the checkpoint, weights hash and calibration
typesafe/jev-1.13, ~typesafe/jev-latestTypeSafe's Jev, through OpenRouter's Decisions APIprovider 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).

modelPrice
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:

StatusWhen
401no Authorization: Bearer <key>, or a key that does not resolve
402the org has no plan that covers paid models (code plan_required, with a link to pick one), or its balance cannot cover the call
403a publishable key (pk-): a decision takes a secret key (sk-)
429the 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.

StatusWhenExample message
400the body is not JSON, or repeats a keybody: …
400the model is not one of the ids aboveunknown model "gpt-5"; use one of hanzo/kai, kai, typesafe/jev-1.13, ~typesafe/jev-latest
422a required field is missingquestions: Field required
422more than 100 questionsquestions: 'questions' takes at most 100 questions, got 101
422a question is malformedquestions.team.choice.criteria: a choice needs at least 2 labels, got 0
422state is a number or a booleanstate: Input should be text, an object or an array
422, code state_too_long, question_too_long, option_too_long or request_too_longa question and its state, a question, an option or the body is longer than Kai readsthe state and question "q" take 8903 tokens; kai-0834a74f2d14 reads at most 1024
429Kai's queue for the model is full… has a full queue; retry in … ms, with Retry-After
529the work queued ahead would not start within 10 seconds… is overloaded; retry in … ms, with Retry-After
503the model is known but not served by this deploymentmodel "kai" is not served here: …
503, code upstream_unavailableJev's upstream refused Hanzo's accounttypesafe/jev-1.13 is temporarily unavailable, with Retry-After
upstream 400, 404, 422Jev refused the request; its status and message pass throughupstream 422: …
502Jev failed, could not be reached, refused with another status, or answered in a shape that fails the checkupstream 502: answer "team" is missing
500a fault in the runtimedecision 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.

Was this page useful?
Last updated Oct 9, 2026