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

# Long text and chunking

A single call accepts up to 20,000 characters in `text`. Past 2,000 characters, the API splits
the text and runs the model once per piece. The split is internal. You always get one result
per input text.

Both models slow down past roughly 500 tokens. The classifier backs `yes-no`, `classify`,
`classify-tree` and `rate`. The extractor backs `answer`, `extract`, `entities` and `verify`. The
2,000-character chunk keeps every call in the linear regime, so **cost stays linear in the
character count**.

Every capability response reports the input size in two headers. `x-input-chars` is the number
of input characters. `x-input-tokens` is the input tokens billed for this call.

## How the split works

The splitter cuts on whitespace. It never cuts inside a word. A single over-long token becomes
its own chunk.

Each capability then merges the per-chunk results a different way.

| Capability                                    | Merge rule                                                                                                                                            |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `yes-no`, `classify`, `classify-tree`, `rate` | A label's score is its **max over chunks**.                                                                                                           |
| `extract`                                     | Per-chunk records merge. At most one record per chunk folds into one record, first non-empty value per field. Several records in a chunk concatenate. |
| `entities`, `verify`                          | Overlapping token windows (window 384 tokens, overlap 64), with offsets remapped to the original text.                                                |

The max rule protects recall. A claim that holds only in the last paragraph still scores high on
its own chunk, so the chunk boundary does not hide it.

> **Note**
>
> `extract` also collapses runs of two or more spaces to `" | "` before the split. Table cells then
> become distinct spans instead of one long run.

## Offsets stay valid

`answer`, `entities` and `verify` return `start` and `end` offsets into the text you sent. For
`entities` and `verify`, the API remaps the window offsets back to that text. You can slice the
original string directly.

This call sent 2,469 characters. The last entity sits well past the 2,000-character boundary:

**`Response (truncated)`**

```json title="Response (truncated)"
{"entities":[
  {"type":"place","text":"northern corridor","probability":0.984,"start":112,"end":129},
  {"type":"person","text":"Amelia Okafor","probability":0.995,"start":2432,"end":2445},
  {"type":"place","text":"Rotterdam depot","probability":0.967,"start":2453,"end":2468}
]}
```

`text.slice(2432, 2445)` on the original input returns `Amelia Okafor`.

## Long text dilutes a classification

Chunking keeps a late signal readable. It does not isolate it. A short decisive sentence inside a
long off-topic document shares its chunk with everything around it.

These three calls used the same three labels. The only difference is how much text went in.

| Input                                                   | `label`                       | `probability` | `confidence`  |
| ------------------------------------------------------- | ----------------------------- | ------------- | ------------- |
| 2,889 characters, billing sentence at the end           | `logistics`                   | 0.803         | 0.548         |
| The same text, split into 2 texts of \~1,450 characters | `logistics`, then `logistics` | 1, then 0.868 | 1, then 0.645 |
| The billing sentence alone                              | `billing`                     | 1             | 1             |

The billing request scored `1` on its own. Inside 2,889 characters of logistics prose it never
won a chunk. **Split the text yourself when you need per-part answers.**

> **Warning**
>
> One `yes-no`, `classify`, `classify-tree` or `rate` call returns one verdict per text. If your document holds
> several verdicts, one call cannot report them all. Split first.

## Pre-split and batch

Send the parts as `texts`. The API returns `{"results":[...]}` in the same order. The batch limit
is 32 texts per call. Each text is a separate inference call, so a batch of 8 runs 8 calls.

#### curl

```bash
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": [
      "Dock scheduling stayed inside the agreed window. No carrier missed a slot.",
      "The customer asks us to cancel the annual subscription and refund the last invoice."
    ],
    "labels": {
      "billing": "the message is about invoices, payments, refunds or subscription charges",
      "logistics": "the message is about warehouses, shipping, carriers or packaging"
    }
  }'
```

#### Python

```python
import requests

URL = "https://api.milliseconds.ai/v1/decision-machine-1/classify"
LABELS = {
    "billing": "the message is about invoices, payments, refunds or subscription charges",
    "logistics": "the message is about warehouses, shipping, carriers or packaging",
}

def split_paragraphs(text, limit=2000):
    parts, current = [], ""
    for para in text.split("\n\n"):
        if len(current) + len(para) > limit and current:
            parts.append(current.strip())
            current = ""
        current += para + "\n\n"
    if current.strip():
        parts.append(current.strip())
    return parts

document = open("report.txt").read_text()
parts = split_paragraphs(document)
for batch_start in range(0, len(parts), 32):
    batch = parts[batch_start:batch_start + 32]
    r = requests.post(URL, headers={"authorization": "Bearer test_sk-..."}, json={"texts": batch, "labels": LABELS})
    for part, result in zip(batch, r.json()["results"]):
        print(result["label"], result["probability"], part[:40])
```

#### TypeScript

```ts
import { readFileSync } from "node:fs";

const URL = "https://api.milliseconds.ai/v1/decision-machine-1/classify";
const labels = {
  billing: "the message is about invoices, payments, refunds or subscription charges",
  logistics: "the message is about warehouses, shipping, carriers or packaging",
};

const doc = readFileSync("report.txt", "utf8");
const parts = doc.split(/\n\s*\n/).filter((p) => p.trim().length > 0);

for (let i = 0; i < parts.length; i += 32) {
  const texts = parts.slice(i, i + 32);
  const res = await fetch(URL, {
    method: "POST",
    headers: { "content-type": "application/json", authorization: "Bearer test_sk-..." },
    body: JSON.stringify({ texts, labels }),
  });
  const { results } = await res.json();
  results.forEach((r, j) => console.log(r.label, r.probability, texts[j].slice(0, 40)));
}
```

## Practical guidance

* **Split on the structure you already have.** Paragraphs, email messages, ticket replies, table
  rows and retrieved passages are natural units. Keep each unit under 2,000 characters.
* **Keep a field's context together.** The merge rule takes the first non-empty value per field,
  so keep a label and its value in one chunk. Never cut between `Total due:` and the amount.
* **Send the whole document for `entities`.** Offsets stay correct, and you want every mention.
* **Send the whole document for a document-level verdict.** The max rule finds a late claim.
* **Split when you need per-part verdicts.** One call decides once per text.
* **Cut boilerplate first.** Signatures, legal footers and quoted history add characters and cost
  without adding signal.
* **Watch the limits.** `text` accepts 20,000 characters and `texts` accepts 32 items. Over either
  limit the API returns `invalid_request` with status 400.

## Next

#### [Input](/concepts/input)

What you send, and batch semantics.

#### [Limits](/reference/limits)

Every field limit in one table.

#### [How it works](/concepts/how-it-works)

The models and the measured latencies.