# Decisions

URL: https://docs.hanzo.ai/docs/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:

```text
POST https://api.hanzo.ai/v1/decisions
```

> Looking for multimodal evaluation (vision, audio, video) and adaptive reasoning budgets? See the [Kai Early Access Guide](https://docs.hanzo.ai/docs/decisions/early-access) or [join the preview waitlist](https://hanzo.ai/research-access?track=kai).

The body is the one Jev takes on OpenRouter's Decisions API
(`POST https://openrouter.ai/api/alpha/decisions`): a client written for it
works with its base URL set to `https://api.hanzo.ai/v1` and a Hanzo key.
OpenAI's Decisions body (`input` and a list of questions) is refused.
[Migrate from Jev](https://docs.hanzo.ai/docs/guides/migrate/jev) lists every field that changes,
with the same request sent both ways.

## Request

```bash
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 (a string as is, anything else as its JSON text); see [Limits](https://docs.hanzo.ai/docs/decisions#limits) |
| `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.

## Limits

- **Questions**: 1 to 100 a request; 101 is refused with `422`.
- **State**: Kai was trained on 1,024 tokens of state beside each question
  (`routing.trained`). It reads up to 32,768 and marks a longer read
  `routing.extrapolated: true`; those answers are unreliable: in a
  3,982-token state, one fact read P(true) 0.985 at the start, 0.663 in the
  middle and 0.507 at the end. Past 32,768 tokens the request is refused with
  `422` and code `state_too_long`.
- **Time**: one yes/no question takes 38 ms of server time at p50 in
  production, on CPU. 100 yes/no questions in one request take 13.9 s.
  Each response reports its own `latency_ms`.

## 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.

```json
"team": {"type": "choice", "instructions": "Which team should handle it?",
         "criteria": {"account": "logins and profiles",
                      "payments": "charges, invoices and refunds",
                      "product": "features and bugs"}}
```

```json
"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. kai-1.2 accepts
`criteria` and does not read them: on JevBench's 74 described yes/no items
its P(true) is the same with the sides as written, negated or swapped.

```json
"is_bug": {"type": "noul", "instructions": "Is this a product defect?",
           "criteria": {"true": "a defect", "false": "working as intended"}}
```

```json
"is_bug": {"type": "noul", "noul": 0.12}
```

`noul` is P(true), and the answer holds nothing else.

### `score`: an ordinal level

`criteria` is the list of levels, lowest first. Every level needs a
description.

```json
"urgency": {"type": "score", "instructions": "How urgent is it?",
            "criteria": ["later", "this week", "today", "now"]}
```

```json
"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

```json
{
  "id": "dec_5f0c2a9e7b1d4c38a6e2f90b1c7d3e44",
  "model": "kai",
  "provider": "Hanzo",
  "answers": {
    "is_bug":  {"type": "noul", "noul": 0.12},
    "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.2",
    "sha256": "<SHA-256 of the weights>",
    "calibration": "cal_<16 hex digits>",
    "device": "cpu",
    "reach": 32768,
    "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 `choice` and `score` answer: `p_max`, the calibrated probability of the reported answer                                                                                                                                                                                                                                                                                                       |
| `routing`           | the backend (`kai` or `openrouter`), the checkpoint (the served Kai build, `kai-1.2`), 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 the read ran past `trained`), 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](https://docs.hanzo.ai/docs/models/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 40960 tokens; kai-0834a74f2d14 reads at most 32768`        |
| `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](https://docs.hanzo.ai/docs/models/kai) covers the model, how it was measured, and Decision
Programs: questions composed into a graph that policy governs.

## Guides, SDK Examples & Decision Recipes

- **[Open Source Repository on GitHub](https://github.com/hanzoai/kai)**: Canonical open-source repo with multi-language SDK examples and production recipes.
- **Multi-Language SDK Examples**:
  - [Python Client (`examples/python/kai_client.py`)](https://github.com/hanzoai/kai/tree/main/examples/python)
  - [TypeScript Client (`examples/typescript/kai_client.ts`)](https://github.com/hanzoai/kai/tree/main/examples/typescript)
  - [Go Client (`examples/go/main.go`)](https://github.com/hanzoai/kai/tree/main/examples/go)
  - [Rust Client (`examples/rust/Cargo.toml`)](https://github.com/hanzoai/kai/tree/main/examples/rust)
  - [cURL Script (`examples/curl/decisions.sh`)](https://github.com/hanzoai/kai/tree/main/examples/curl)
- **[Modular Rust Recipes Crate (`kai-recipes`)](https://github.com/hanzoai/kai/tree/main/recipes/rust)**: Standalone Rust crate for typed question building and distribution scoring.
- **[20 Enterprise Blueprints (`recipes/`)](https://github.com/hanzoai/kai/tree/main/recipes)**: Retail inventory audit, equipment readiness gates, returns triage, and agent control loops.
- **[Cookbook & Pipelines](https://docs.hanzo.ai/docs/guides/cookbook)**: End-to-end integration patterns combining Enso routing, Kai structured decisions, and Zen reasoning.
