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

# When to use the native API

The OpenAI-compatible surface exists for one reason: you already have an OpenAI client. It maps `/v1/chat/completions` onto `extract` for structured output, and onto `classify` plus `extract` for function calling. The other six capabilities have no chat equivalent.

Move to the native endpoints when you need a probability, a character offset, a batch, or a capability the facade cannot reach.

## What the OpenAI surface cannot return

| You need                              | Native field                                                       | Available over chat/completions |
| ------------------------------------- | ------------------------------------------------------------------ | ------------------------------- |
| Truth probability for a statement     | `probability` on [yes-no](/capabilities/yes-no)                    | No                              |
| Label distribution over your taxonomy | `scores`, `confidence` on [classify](/capabilities/classify)       | No                              |
| A path through a nested taxonomy      | `path`, `levels[]` on [classify-tree](/capabilities/classify-tree) | No                              |
| A position on an ordered scale        | `score`, `level` on [rate](/capabilities/rate)                     | No                              |
| Character offsets of an answer span   | `start`, `end` on [answer](/capabilities/answer)                   | No                              |
| Every span per type, with offsets     | `entities[]` on [entities](/capabilities/entities)                 | No                              |
| A value check against the source text | `matches`, `found[]` on [verify](/capabilities/verify)             | No                              |
| Up to 32 texts in one request         | `texts` on every capability                                        | No                              |
| A filled JSON Schema                  | `data` on [extract](/capabilities/extract)                         | Yes, as a JSON **string**       |

The chat facade returns one assistant message. Structured extraction arrives as a JSON string you must parse. Function calling arrives as one tool call with a stringified `arguments` object. Neither carries a confidence number, so you cannot build the [confidence routing](/patterns/confidence-routing) that the native fields make trivial.

> **Note**
>
> **The facade never chats.** Plain chat, `response_format: {"type":"text"}` and `response_format: {"type":"json_object"}` all return `400 unsupported_request`. That is deliberate. The [streaming and differences](/openai/streaming-and-differences) page lists every rejected request.

## Migration table

| Chat/completions usage                           | Native call                            | What you gain                                                          |
| ------------------------------------------------ | -------------------------------------- | ---------------------------------------------------------------------- |
| `response_format.json_schema`                    | `POST /v1/decision-machine-1/extract`  | A parsed `data` object, `texts` batching, no JSON string to decode     |
| `tools` with two or more entries                 | `POST /v1/decision-machine-1/classify` | `scores` per tool, `confidence`, and one inference call instead of two |
| `tools` with one entry, or a named `tool_choice` | `POST /v1/decision-machine-1/extract`  | The same arguments, without the tool-call envelope                     |
| A prompt asking "is this X, yes or no?"          | `POST /v1/decision-machine-1/yes-no`   | `probability`, and up to 32 statements per call                        |
| A prompt asking for a 1-to-5 rating              | `POST /v1/decision-machine-1/rate`     | `score`, `level`, `confidence`, `scores[]`                             |
| A prompt asking "where does it say X?"           | `POST /v1/decision-machine-1/answer`   | An extractive span plus `start` and `end`                              |
| A prompt listing names, dates or amounts         | `POST /v1/decision-machine-1/entities` | Every span, typed, with offsets and probabilities                      |
| A prompt checking a field against a document     | `POST /v1/decision-machine-1/verify`   | `matches`, `probability` and `found[]` for review queues               |

## Fewer calls per request

Tool selection over two or more tools runs `classify` first to pick the tool, then `extract` to fill its arguments. That is two inference calls. Calling `classify` or `extract` directly runs one.

Measured on 2026-09-18 against production, median of 7 runs:

| Call                                       | Observed latency |
| ------------------------------------------ | ---------------- |
| chat `tools`, 2 tools (classify + extract) | 0.72 s           |
| chat forced `tool_choice` (extract only)   | 0.40 s           |
| chat `json_schema`                         | 0.39 s           |
| native `extract`, 9 properties             | 0.41 s           |
| native `classify`, 3 labels                | 0.43 s           |

Batching removes round trips. The chat facade takes one message set per request. Every native endpoint takes `texts` with up to 32 items. `yes-no` and `answer` also take up to 32 `statements` or `questions`.

Statements and questions cost almost nothing extra, because they go into one inference call. Each item in `texts` costs its own inference call. [Batching](/capabilities/batching) covers the ordering rules.

Every capability response reports usage in the `x-input-chars` and `x-input-tokens` headers. `x-input-chars` is the number of input characters. `x-input-tokens` is the input tokens billed for this call. `x-inference-ms` reports the model processing time for this request, and it comes back on `/v1/chat/completions` too. Cost scales with everything you send, text plus labels, questions, or schema, at \$0.04 per million input tokens and \$0 per output token. See [Pricing](/reference/pricing).

## The same job, both ways

A chat client extracting a reservation:

**`OpenAI SDK`**

```python title="OpenAI SDK"
from openai import OpenAI

client = OpenAI(base_url="https://api.milliseconds.ai/v1", api_key="test_sk-...")

completion = client.chat.completions.create(
    model="decision-machine-1",
    messages=[{"role": "user", "content": "Book Nobu for 4 on Friday."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "reservation",
            "schema": {
                "type": "object",
                "properties": {
                    "restaurant": {"type": "string"},
                    "party_size": {"type": "integer"},
                    "day": {"type": "string"},
                },
            },
        },
    },
)
print(completion.choices[0].message.content)
```

That returns the JSON as a string:

```json
{"restaurant":"Nobu","party_size":4,"day":"Friday"}
```

The native call returns the same result as an object, and accepts a batch:

**`curl`**

```bash title="curl"
curl -s https://api.milliseconds.ai/v1/decision-machine-1/extract \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer test_sk-...' \
  -d '{
    "texts": ["Book Nobu for 4 on Friday.", "Table for 2 at Kura on Sunday."],
    "schema": {
      "type": "object",
      "properties": {
        "restaurant": {"type": "string"},
        "party_size": {"type": "integer"},
        "day": {"type": "string"}
      }
    }
  }'
```

**`Python`**

```python title="Python"
import requests

payload = {
    "texts": ["Book Nobu for 4 on Friday.", "Table for 2 at Kura on Sunday."],
    "schema": {
        "type": "object",
        "properties": {
            "restaurant": {"type": "string"},
            "party_size": {"type": "integer"},
            "day": {"type": "string"},
        },
    },
}

r = requests.post(
    "https://api.milliseconds.ai/v1/decision-machine-1/extract",
    headers={"authorization": "Bearer test_sk-..."},
    json=payload,
    timeout=30,
)
for result in r.json()["results"]:
    print(result["data"])
```

**`TypeScript`**

```typescript title="TypeScript"
const res = await fetch(
  "https://api.milliseconds.ai/v1/decision-machine-1/extract",
  {
    method: "POST",
    headers: { "content-type": "application/json", authorization: "Bearer test_sk-..." },
    body: JSON.stringify({
      texts: ["Book Nobu for 4 on Friday.", "Table for 2 at Kura on Sunday."],
      schema: {
        type: "object",
        properties: {
          restaurant: { type: "string" },
          party_size: { type: "integer" },
          day: { type: "string" },
        },
      },
    }),
  },
);

const { results } = await res.json();
console.log(results.map((r) => r.data));
```

```json
{
  "results": [
    {"data": {"restaurant": "Nobu", "party_size": 4, "day": "Friday"}},
    {"data": {"restaurant": "Kura", "party_size": 2, "day": "Sunday"}}
  ]
}
```

Two texts, one request, one `results` array in input order. The chat surface needs two round trips for the same work.

## Keep the OpenAI surface when

* An agent framework or SDK speaks OpenAI and you cannot change it.
* You route tools by description and one tool call per turn is enough.
* You want a drop-in swap behind an existing `base_url`, with no client changes.

The facade and the native endpoints run the same models. The facade adds a translation layer, not a different decision.

> **Tip**
>
> You can mix the two. Keep the OpenAI client for tool calling inside your agent loop, and call [verify](/capabilities/verify) or [yes-no](/capabilities/yes-no) natively for the guardrails around it.

## Migration checklist

#### Name the decision

Write the question your prompt asks in one sentence. Match it to a capability in [Choosing a capability](/concepts/choosing-a-capability).

#### Replace the endpoint

Drop the OpenAI client. Send JSON to `https://api.milliseconds.ai/v1/decision-machine-1/<capability>` with your key in the `Authorization` header. See [Authentication](/authentication).

#### Route on the numbers

Read `probability` and `confidence`, not just the label. Set a threshold per action with [Thresholds and confidence routing](/concepts/thresholds).

#### Batch what you can

Collapse loops into `texts`, `statements` or `questions`, up to 32 items each.

## Next

* [Choosing a capability](/concepts/choosing-a-capability) — the decision table, including the near-miss pairs.
* [Decisions and probabilities](/concepts/decisions-and-probabilities) — what each returned number means.
* [Batching](/capabilities/batching) — how `texts` spreads across inference slots.