# Reasoning

> Control how much a model thinks, read its reasoning, and carry reasoning state through multi-turn and tool conversations.

Source: https://0.0.0.0:3000/docs/features/reasoning
Documentation index: https://0.0.0.0:3000/docs/llms.txt

Reasoning models can think before they answer. Numes offers one set of controls for every provider: request an effort level or a token budget, turn reasoning off, or hide it from the response. Controls are upstream hints by default. Set `provider.require_parameters: true` to require declared capabilities. Reasoning comes back in OpenRouter-compatible fields.

## Request controls

Send a `reasoning` object with a chat completion:

```request
{
  "method": "POST",
  "path": "/chat/completions",
  "body": {
    "model": "openai/gpt-6-luna",
    "messages": [
      { "role": "user", "content": "How many times does the letter r appear in \"strawberry\"?" }
    ],
    "reasoning": { "effort": "high" },
    "max_tokens": 4096
  }
}
```

:::fields reasoning
- `effort` `string` — Common values include `none`, `minimal`, `low`, `medium`, `high`, `xhigh` and `max`; providers may accept others. `none` turns reasoning off.
- `max_tokens` `integer` — Provider reasoning token budget. The upstream validates its range.
- `exclude` `boolean` default `false` — Keep reasoning out of the response. The model can still reason; you do not receive it.
- `enabled` `boolean` — `true` asks for reasoning without choosing a level; `false` turns it off.
:::

Two shorthand fields are also accepted:

:::fields Other fields
- `reasoning_effort` `string` — OpenAI-style shorthand for `reasoning.effort`, with the same values. `null` is treated as omitted.
- `include_reasoning` `boolean` — Deprecated. The inverse of `reasoning.exclude`.
:::

Shorthand aliases take precedence when both forms are present. Unfamiliar effort
values and reasoning attributes are forwarded; the upstream validates them.

## Model capabilities

`GET /models` and the model endpoints list include a `reasoning` object for offers that support the `reasoning` parameter and have published reasoning details. Every field is optional. Missing catalogue facts do not exclude an endpoint unless you opt into strict matching.

```request
{
  "method": "GET",
  "path": "/models/openai/gpt-6-luna/endpoints"
}
```

```json title="Endpoint excerpt"
{
  "tag": "example-provider/primary",
  "supported_parameters": ["max_tokens", "reasoning", "include_reasoning", "tools"],
  "reasoning": {
    "supported_efforts": ["low", "medium", "high"],
    "default_effort": "medium",
    "default_enabled": true,
    "mandatory": false,
    "supports_max_tokens": true
  }
}
```

:::fields reasoning capabilities
- `supported_efforts` `string[]` — Effort levels the endpoint accepts.
- `default_effort` `string | null` — Effort used when you send none.
- `default_enabled` `boolean` — Whether the model reasons when you send no reasoning control.
- `mandatory` `boolean` — `true` when reasoning cannot be turned off.
- `supports_max_tokens` `boolean` — Whether `reasoning.max_tokens` budgets are accepted.
:::

A model row in `GET /models` describes one representative offer. Other endpoints of the same model can differ, so check the endpoints list when you depend on a specific control.

## Routing requirements

Reasoning controls are forwarded by default, with one exception: an `effort` goes only to endpoints that list it in `supported_efforts`, or that don't publish `supported_efforts` at all. `effort: "none"` also reaches endpoints with `mandatory: false`. If no endpoint of the model accepts your effort, the request fails with `400 invalid_request` on `reasoning.effort` and is never sent to a provider.

With `provider.require_parameters: true`, the following declarations narrow routing:

| You send | The endpoint must declare |
| --- | --- |
| Any reasoning control that does not disable reasoning, including `exclude` or `include_reasoning` on their own | `reasoning` in `supported_parameters` |
| `effort` other than `none` | That level in `supported_efforts`, or no `reasoning` object at all |
| `effort: "none"` or `enabled: false` | `none` in `supported_efforts`, or `mandatory: false` |
| `max_tokens` | `supports_max_tokens: true` |

With `provider.require_parameters: true`, an endpoint that lists `reasoning` in `supported_parameters` but has no `reasoning` object hasn't published its effort levels. It still receives a request with an `effort`, passed on as a hint: the provider may use that level, use its own default, or reject it. Endpoints that publish a `reasoning` object get only the efforts they list.

With strict matching, if no eligible endpoint qualifies, the request fails with `503 provider_unavailable`, unless the `effort` alone ruled out every endpoint, which returns `400 invalid_request`. To use a model's defaults without narrowing routing, omit the reasoning fields entirely.

## Response fields

Visible reasoning is returned next to the answer:

```json title="Response excerpt"
{
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "The letter r appears three times in \"strawberry\".",
        "reasoning": "Spell it out: s, t, r, a, w, b, e, r, r, y. That is r at positions 3, 8 and 9.",
        "reasoning_details": [
          {
            "type": "reasoning.text",
            "text": "Spell it out: s, t, r, a, w, b, e, r, r, y. That is r at positions 3, 8 and 9.",
            "signature": "EqQBCkgIBxABGAIqQ...",
            "format": "anthropic-claude-v1"
          }
        ]
      }
    }
  ]
}
```

- `reasoning` is the plain visible reasoning text, for display.
- `reasoning_details` holds structured reasoning blocks as the provider returned them. It is present when the provider returns structured reasoning, such as signed blocks, summaries or encrypted state.

With `exclude: true`, both fields are omitted.

| Detail `type` | Carries | Meaning |
| --- | --- | --- |
| `reasoning.text` | `text`, optional `signature` | Raw reasoning text. A `signature` lets the provider verify the block when you send it back. |
| `reasoning.summary` | `summary` (or native `text`) | A summary of the reasoning, for providers that summarize instead of exposing raw text. |
| `reasoning.encrypted` | `data` | Opaque reasoning state you cannot read, only return. |

:::fields Reasoning detail
- `type` `string` — `reasoning.text`, `reasoning.summary` or `reasoning.encrypted`.
- `text` `string` — Reasoning or summary text. Up to 4 MiB.
- `signature` `string` — Provider signature for the block. Up to 1 MiB.
- `data` `string` — Encrypted state. Up to 8 MiB.
- `id` `string` — Provider block identifier.
- `format` `string` — Provider-specific encoding of the block. Treat it as opaque. `unknown` marks plain reasoning without provider-specific state.
- `index` `integer` — Provider block index.
:::

## Streaming

In a stream, each visible reasoning fragment arrives in one chunk as both `delta.reasoning` and a `reasoning.text` entry in `delta.reasoning_details` with the same text. Read **one** of the two, never both:

- For display, concatenate `delta.reasoning`.
- To send reasoning back later, collect the `delta.reasoning_details` entries in order and rebuild them as shown in [From a stream](#from-a-stream).

Streamed details follow two further rules:

- **Signature-only fragments.** When the provider signs a block after its text has streamed, a `reasoning.text` detail arrives with the `signature` (and `id`, `format` or `index`) but **without** the text, which you already have.
- **Finish.** The finish chunk has an empty delta; no synthetic reasoning detail is added.

```sse
data: {"id":"6f1c2b8e-3d4a-4f5b-9c7d-2e8a1b0c9d3f","object":"chat.completion.chunk","created":1790000000,"model":"openai/gpt-6-luna","choices":[{"index":0,"delta":{"role":"assistant","reasoning":"Spell it out: ","reasoning_details":[{"type":"reasoning.text","text":"Spell it out: "}]},"finish_reason":null}]}

data: {"id":"6f1c2b8e-3d4a-4f5b-9c7d-2e8a1b0c9d3f","object":"chat.completion.chunk","created":1790000000,"model":"openai/gpt-6-luna","choices":[{"index":0,"delta":{"reasoning":"s, t, r, a, w, b, e, r, r, y.","reasoning_details":[{"type":"reasoning.text","text":"s, t, r, a, w, b, e, r, r, y."}]},"finish_reason":null}]}

data: {"id":"6f1c2b8e-3d4a-4f5b-9c7d-2e8a1b0c9d3f","object":"chat.completion.chunk","created":1790000000,"model":"openai/gpt-6-luna","choices":[{"index":0,"delta":{"content":"Three."},"finish_reason":null}]}

data: {"id":"6f1c2b8e-3d4a-4f5b-9c7d-2e8a1b0c9d3f","object":"chat.completion.chunk","created":1790000000,"model":"openai/gpt-6-luna","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
```

With `exclude: true`, no reasoning appears in the stream at all. See [Streaming](/docs/features/streaming) for the complete event format.

## Preserve reasoning across turns

Some providers need their earlier reasoning to continue a conversation correctly, most importantly after tool calls. Signed and encrypted details carry that state. Send it back on the assistant message it belongs to:

```json title="Next request, messages excerpt"
[
  { "role": "user", "content": "What is the weather in Lisbon?" },
  {
    "role": "assistant",
    "content": null,
    "reasoning_details": [
      {
        "type": "reasoning.text",
        "text": "I need current data, so I will call the weather tool.",
        "signature": "EqQBCkgIBxABGAIqQ...",
        "format": "anthropic-claude-v1"
      }
    ],
    "tool_calls": [
      {
        "id": "call_7f3a9c",
        "type": "function",
        "function": { "name": "get_weather", "arguments": "{\"city\":\"Lisbon\"}" }
      }
    ]
  },
  { "role": "tool", "tool_call_id": "call_7f3a9c", "content": "{\"temperature\":21}" }
]
```

Rules for reasoning in request messages:

- Return all reasoning details unchanged, including unfamiliar fields, formats, types and empty fragments.
- The upstream validates continuation semantics. Aggregate request and JSON complexity limits still apply.
- A native dialect adapter can return `unsupported_capability` when it cannot translate opaque state without losing data.

For requests without end-to-end encryption, sources using the OpenAI-compatible
chat format handle these reasoning fields as follows:

| You send | Result |
| --- | --- |
| Nonempty `reasoning_content`, with `reasoning` absent or empty | OpenRouter and PPQ send the text as `reasoning`; other compatible chat sources retain `reasoning_content`. |
| Both fields contain the same string | The source sends one representation. |
| `reasoning_content: ""` | The explicit empty string is retained. |
| `reasoning: ""`, with no `reasoning_content` | The empty field is omitted. |
| Other distinct string values | Both values stay independent. |

### From a buffered response

Copy `message.reasoning_details` as returned onto the assistant message you append. If the response only has a plain `reasoning` string, you can send that back as `reasoning`.

### From a stream

Rebuild complete blocks from the streamed fragments before sending them back. Empty metadata fragments are accepted; no synthetic terminator is required.

1. A `reasoning.text` fragment with `text` and no `signature` extends the previous detail if that is an unsigned `reasoning.text`; otherwise it starts a new one.
2. A `reasoning.text` fragment with a `signature` but no `text` completes the most recent unsigned `reasoning.text` detail. If there is none, keep it as its own detail.
3. A fragment with neither `text` nor `signature` only fills in missing fields, such as `format`, on the most recent `reasoning.text` detail.
4. Every other fragment is its own detail.

:::code-group
```python title="Python"
def rebuild_reasoning_details(fragments: list[dict]) -> list[dict]:
    """Merge streamed reasoning_details fragments into complete details."""
    merged: list[dict] = []

    def latest_text(unsigned_only: bool) -> dict | None:
        for detail in reversed(merged):
            if detail["type"] == "reasoning.text" and not (unsigned_only and "signature" in detail):
                return detail
        return None

    for fragment in fragments:
        has_text = bool(fragment.get("text"))
        signed = "signature" in fragment
        if fragment["type"] != "reasoning.text" or (has_text and signed):
            merged.append(dict(fragment))
        elif has_text:
            last = merged[-1] if merged else None
            if last and last["type"] == "reasoning.text" and "signature" not in last:
                last["text"] = last.get("text", "") + fragment["text"]
            else:
                merged.append(dict(fragment))
        else:
            target = latest_text(unsigned_only=signed)
            if target is None:
                if signed:
                    merged.append(dict(fragment))
                continue
            for key in ("signature", "id", "format", "index"):
                if key in fragment and key not in target:
                    target[key] = fragment[key]
    return merged
```

```typescript title="TypeScript"
type ReasoningDetail = {
  type: "reasoning.text" | "reasoning.summary" | "reasoning.encrypted";
  text?: string;
  signature?: string;
  data?: string;
  id?: string;
  format?: string;
  index?: number;
};

export function rebuildReasoningDetails(fragments: ReasoningDetail[]): ReasoningDetail[] {
  const merged: ReasoningDetail[] = [];
  const latestText = (unsignedOnly: boolean) =>
    [...merged].reverse().find(
      (detail) => detail.type === "reasoning.text" && !(unsignedOnly && detail.signature !== undefined),
    );

  for (const fragment of fragments) {
    const hasText = Boolean(fragment.text);
    const signed = fragment.signature !== undefined;
    if (fragment.type !== "reasoning.text" || (hasText && signed)) {
      merged.push({ ...fragment });
    } else if (hasText) {
      const last = merged.at(-1);
      if (last?.type === "reasoning.text" && last.signature === undefined) {
        last.text = (last.text ?? "") + fragment.text;
      } else {
        merged.push({ ...fragment });
      }
    } else {
      const target = latestText(signed);
      if (!target) {
        if (signed) merged.push({ ...fragment });
        continue;
      }
      for (const key of ["signature", "id", "format", "index"] as const) {
        if (fragment[key] !== undefined && target[key] === undefined) {
          (target as Record<string, unknown>)[key] = fragment[key];
        }
      }
    }
  }
  return merged;
}
```
:::

SDKs built for OpenRouter reasoning details, such as the OpenRouter AI SDK provider, perform this accumulation for you.

Keep a conversation on one endpoint where possible: signed state is produced by one provider and is meant for that provider. A [session ID](/docs/features/sessions) keeps follow-up turns on the same endpoint while it remains eligible.

## Output limits and billing

`max_tokens` (or `max_completion_tokens`) is passed to the provider, which applies it to the whole output. On most models it covers reasoning as well as the answer. `reasoning.max_tokens` is a separate budget for thinking; keep it below `max_tokens` so the answer has room.

When a native adapter converts an effort hint or default into a numeric thinking
budget, it caps that generated budget below the total output limit. An explicit
`reasoning.max_tokens` value is preserved for upstream validation.

Reasoning tokens are part of `completion_tokens` and are billed, including reasoning you exclude from the response. Where an endpoint prices reasoning separately, reasoning tokens use that rate instead of the completion rate, so each token is charged once, and a `provider.max_price.completion` ceiling also applies to it.

```json title="Usage excerpt"
{
  "prompt_tokens": 24,
  "completion_tokens": 212,
  "total_tokens": 236,
  "completion_tokens_details": { "reasoning_tokens": 168 },
  "token_count_basis": "provider_native",
  "cost": 0.000765,
  "cost_details": { "upstream_inference_cost": 0.000765 }
}
```

See [Usage accounting](/docs/account/usage-accounting) for every usage field.

## Best practices

- **Check `supported_efforts` for guidance**, or omit reasoning controls to use the model default. Strict matching is opt-in.
- **Budget output for thinking.** Raise `max_tokens` for reasoning models and watch for `finish_reason: "length"`.
- **Use `exclude: true` when you only need the answer**, and expect a longer wait before the first chunk.
- **Always return reasoning details during tool loops** for providers that sign or encrypt reasoning.
- **Read one streaming representation.** Concatenating `reasoning` and the `reasoning_details` text doubles the output.

## Next steps

:::cards
- {wrench} [Tool calling](/docs/features/tool-calling) Return reasoning details with tool results.
- {waves} [Streaming](/docs/features/streaming) The complete SSE event format.
- {tree} [Sticky sessions](/docs/features/sessions) Keep a conversation on the same endpoint.
- {receipt} [Usage accounting](/docs/account/usage-accounting) How reasoning tokens and cost are reported.
- {boxes} [Models](/docs/models) Read reasoning capabilities from the catalogue.
:::
