> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.milliseconds.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.milliseconds.ai/_mcp/server.

# Confidence routing

Most systems use one axis: the label. Confidence routing adds a second axis: how sure the model is.
The label says what to do. The confidence says who does it.

One router function covers every capability. Write it once, then reuse it per action with different numbers.

## The four bands

| Band     | Who acts                                  | Typical rule                                   |
| -------- | ----------------------------------------- | ---------------------------------------------- |
| Act      | Your code, silently                       | `probability >= 0.90` and `confidence >= 0.70` |
| Confirm  | Your code, with an undo shown to a person | `probability >= 0.75`                          |
| Review   | A person, before anything happens         | `probability >= 0.55`                          |
| Escalate | A larger model, or the customer           | Everything below                               |

The bands run in order and the first match wins. The numbers above are starting points, not
measurements. [Thresholds and confidence routing](/concepts/thresholds) explains why a threshold belongs
to the action. [Tuning thresholds](/evaluate/tuning-thresholds) turns a golden set into your own numbers.
This page is the code.

> **Note**
>
> Act and confirm both apply the decision. The difference is reversibility. Confirm needs an undo path in
> your product. Without one, fold confirm into review.

## Which field to route on

| Capability | Route on                 | Second axis                                                   |
| ---------- | ------------------------ | ------------------------------------------------------------- |
| `classify` | `probability`            | `confidence`                                                  |
| `rate`     | `score`                  | `confidence`                                                  |
| `yes-no`   | `probability`            | none                                                          |
| `answer`   | `probability`            | `answer` is `null` and `probability` is `0` when nothing fits |
| `entities` | `probability` per span   | none                                                          |
| `verify`   | `probability`            | `found[]`                                                     |
| `extract`  | no per-field probability | verify the fields you act on                                  |

`confidence` is `1 − normalized entropy` over the scores. It is 1 for one clear winner and 0 for a flat
distribution. Only `classify` and `rate` return it.

> **Warning**
>
> A low `confidence` means the scores spread across several options. Gate on both fields. Without the
> confidence gate, a near tie reaches the act band.

## Real responses, three bands

One `classify` call over three texts. The response below is real.

**`classify with texts[]`**

```json title="classify with texts[]"
{"results":[
  {"label":"billing","probability":1,"confidence":0.999,"scores":{"billing":1,"shipping":0,"account":0}},
  {"label":"account","probability":0.632,"confidence":0.378,"scores":{"billing":0.364,"shipping":0.005,"account":0.632}},
  {"label":"billing","probability":0.993,"confidence":0.96,"scores":{"billing":0.993,"shipping":0.007,"account":0}}
]}
```

Result 1 and result 3 land in act. Result 2 does not. Its text says only `"Can someone look at this? It
has been going on for a while now."` The winner `account` scores 0.632, and `billing` follows at 0.364.
The `confidence` of 0.378 reports that spread. Route this result to escalate and ask the customer one
question.

## The router

Make the call, then apply the bands to each result. The call itself carries no thresholds.

**`curl`**

```bash title="curl"
curl -s -X POST https://api.milliseconds.ai/v1/decision-machine-1/classify \
  -H "Content-Type: application/json" \
  -H "authorization: Bearer test_sk-..." \
  -d '{
    "texts": [
      "I was charged twice for my subscription this month and support has not replied.",
      "Can someone look at this? It has been going on for a while now.",
      "My package never arrived and I want my money back."
    ],
    "labels": {
      "billing": "a payment, invoice, refund or charge problem",
      "shipping": "a delivery, tracking or address problem",
      "account": "a login, password or profile problem"
    }
  }'
```

**`Python`**

```python title="Python"
import requests

URL = "https://api.milliseconds.ai/v1/decision-machine-1"

LABELS = {
    "billing": "a payment, invoice, refund or charge problem",
    "shipping": "a delivery, tracking or address problem",
    "account": "a login, password or profile problem",
}

tickets = [
    "I was charged twice for my subscription this month and support has not replied.",
    "Can someone look at this? It has been going on for a while now.",
    "My package never arrived and I want my money back.",
]  # up to 32 per call

# One band set per action. Copy and change the numbers, do not share one set.
AUTO_FILE = {"act": 0.90, "confirm": 0.75, "review": 0.55, "min_confidence": 0.70}


def band(result, rules):
    p = result["probability"]
    c = result.get("confidence", 1.0)
    if p >= rules["act"] and c >= rules["min_confidence"]:
        return "act"
    if p >= rules["confirm"]:
        return "confirm"
    if p >= rules["review"]:
        return "review"
    return "escalate"


r = requests.post(f"{URL}/classify", headers={"authorization": "Bearer test_sk-..."}, json={"texts": tickets, "labels": LABELS}, timeout=30)
r.raise_for_status()

for text, d in zip(tickets, r.json()["results"]):  # results follow input order
    b = band(d, AUTO_FILE)
    if b == "act":
        file_ticket(text, d["label"])
    elif b == "confirm":
        file_ticket(text, d["label"], undo=True)
    elif b == "review":
        queue_for_agent(text, d["label"])
    else:
        ask_the_customer(text)
```

**`TypeScript`**

```typescript title="TypeScript"
const URL = "https://api.milliseconds.ai/v1/decision-machine-1";

type Rules = { act: number; confirm: number; review: number; minConfidence: number };
type Result = { label: string; probability: number; confidence?: number };

// One band set per action. Copy and change the numbers, do not share one set.
const AUTO_FILE: Rules = { act: 0.9, confirm: 0.75, review: 0.55, minConfidence: 0.7 };

function band(d: Result, r: Rules): "act" | "confirm" | "review" | "escalate" {
  const c = d.confidence ?? 1;
  if (d.probability >= r.act && c >= r.minConfidence) return "act";
  if (d.probability >= r.confirm) return "confirm";
  if (d.probability >= r.review) return "review";
  return "escalate";
}

async function route(texts: string[], labels: Record<string, string>, rules: Rules) {
  const res = await fetch(`${URL}/classify`, {
    method: "POST",
    headers: { "Content-Type": "application/json", authorization: "Bearer test_sk-..." },
    body: JSON.stringify({ texts, labels }),
  });
  if (!res.ok) throw new Error(`classify failed: ${res.status}`);
  const { results } = (await res.json()) as { results: Result[] };
  // results follow the order of texts
  return results.map((d, i) => ({ text: texts[i], label: d.label, band: band(d, rules) }));
}
```

Every request needs an API key. See [Authentication](/authentication).

## Route a rate call on score

`rate` returns `level` as an argmax. It can flip on 0.001, as this real response shows.

**`rate — scale Calm to Threatening to leave`**

```json title="rate — scale Calm to Threatening to leave"
{"score":1.585,"level":2,"label":"Frustrated","confidence":0.371,"scores":[0,0.471,0.472,0.057]}
```

Route on `score` and gate with `confidence`. `score` moves smoothly from 0 to `len(scale) - 1`, so pick
the cut-off on that range. The call is the same `rate` request shown on [Rate](/capabilities/rate). Only
the router changes. Here `act` means the quiet path, with no escalation.

**`Python`**

```python title="Python"
def rate_band(d, escalate_above=2.0, min_confidence=0.6):
    if d["confidence"] < min_confidence:
        return "review"          # the levels sit too close to trust
    return "escalate" if d["score"] >= escalate_above else "act"
```

**`TypeScript`**

```typescript title="TypeScript"
type RateResult = { score: number; level: number; confidence: number; scores: number[] };

function rateBand(d: RateResult, escalateAbove = 2.0, minConfidence = 0.6) {
  if (d.confidence < minConfidence) return "review"; // the levels sit too close to trust
  return d.score >= escalateAbove ? "escalate" : "act";
}
```

## Keep the bands honest

Store `probability`, `confidence` and the chosen band with every decision. You cannot tune without that
record. Watch the escalate share in [Monitoring](/evaluate/monitoring): a rise means the inputs changed.

Move one number at a time. Raise `act` when a wrong auto-action costs more. Lower `review` when the
queue grows faster than people clear it.

> **Tip**
>
> Batch the call, not the routing. One request carries up to 32 texts in `texts[]`, and the results come
> back in input order. Apply the bands in your own loop.

## Next

#### [Tuning thresholds](/evaluate/tuning-thresholds)

Turn a golden set into precision and recall at each cut-off, then pick real numbers.

#### [Cascade to an LLM](/patterns/cascade)

Send only the escalate band to a large model and keep the cost of the rest flat.

#### [Extract, then verify](/patterns/extract-then-verify)

Give `extract` the probability it does not return, then route on it.