# Reasoning

Reasoning models think before they answer. You set how much in the field your API uses, and llm.buzz sends it to each provider in that provider's terms, by OpenRouter's rules.

## Which models reason

`GET https://api.llm.buzz/api/v1/models` lists a model's efforts as `reasoning_efforts`, in order from `none` to `max`, when the model's lead provider's listing names them. The key is left out otherwise, including where llm.buzz knows a model's efforts only from its own rules for the model's family. A model without the key can still reason. Do not filter on `supported_parameters` for reasoning: no model in the public list names `reasoning` there today.

The efforts differ from model to model. `openai/gpt-5.6-luna` takes `none`, `low`, `medium`, `high`, `xhigh` and `max`, with no `minimal`. `anthropic/claude-sonnet-5.5` takes `low` to `max` and has no `none`, because it cannot turn thinking off. A request may name `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` or `max`; Chat Completions and Responses answer 400 `invalid_parameter`, naming the field, for any other value.

## The controls in each API

| | Chat Completions | Responses | Messages |
|---|---|---|---|
| Effort | `reasoning_effort`, or `reasoning.effort` | `reasoning.effort` | `output_config.effort`, or `reasoning.effort` |
| Token budget | `reasoning.max_tokens`, `thinking.budget_tokens` or `thinking_budget` | None | `thinking.budget_tokens` or `reasoning.max_tokens` |
| On or off | `reasoning.enabled`, `thinking`, `enable_thinking` or `chat_template_kwargs.enable_thinking` | An effort of `none` is off | `thinking`, or `reasoning.enabled` |
| Hide the reasoning text | `reasoning.exclude: true` or `include_reasoning: false` | None | `reasoning.exclude: true` |

- **Chat Completions.** `reasoning` is OpenRouter's object: `effort` or `max_tokens`, not both (a request with both is 400 `invalid_parameter`), plus `enabled` and `exclude`. Its fields must agree: an effort tier or a budget beside `enabled: false`, or `effort: "none"` beside `enabled: true`, is 400 `invalid_parameter` naming `reasoning.enabled`. `include_reasoning: true` is `reasoning: {}` and `false` is `reasoning: {"exclude": true}`. `thinking` takes `enabled`, `adaptive` or `disabled`: a bare `enabled` or `adaptive` switches reasoning on at llm.buzz's default effort for the model ([below](#how-an-effort-is-mapped)), an `enabled` with `budget_tokens` is a budget ([Token budgets](#token-budgets)), and `disabled` is effort `none`. The body's `x-sgr-ignored-parameters` names the move to an effort as `thinking->translated(reasoning_effort)`. It says what llm.buzz did with the field, not that it dropped it.
- **Responses.** `reasoning` takes `effort`, `summary`, `generate_summary` and `context`; `summary` and `generate_summary` sent together must match. `reasoning.mode` is 400 `invalid_parameter`, and so is any other key. Without a `summary`, llm.buzz asks OpenAI for an automatic one unless the effort is `none`.
- **Messages.** `thinking` takes Anthropic's forms, described under [Thinking](messages.md#thinking). On the Claude models llm.buzz lists today, a bare `{"type": "enabled"}` runs adaptive thinking, named `thinking.type->adaptive`, and `budget_tokens` is refused ([Token budgets](#token-budgets)). `reasoning` is OpenRouter's object, as on Chat Completions, and its fields must agree: an effort tier or a budget beside `enabled: false`, or `effort: "none"` beside `enabled: true`, is a 400 `invalid_request_error` whose message ends `(param: reasoning.enabled)`. `reasoning.effort` with `reasoning.max_tokens`, or a `reasoning.effort`, `reasoning.max_tokens` or `output_config.effort` beside a `thinking.budget_tokens`, is a 400 `invalid_request_error`.
- A control that belongs to another API is dropped and named in `x-sgr-ignored-parameters`, as Chat's `reasoning_effort` is on Messages. Send one depth control per request: an effort and a budget in one Chat Completions request, or two budgets, are 400 `invalid_parameter`, and `metadata.param` names one of the controls. Two efforts in one Chat Completions request, `reasoning_effort` with `reasoning.effort`, are not refused: `reasoning_effort` is used, in either order and in a stream, and `x-sgr-ignored-parameters` names the other as `reasoning->ignored(explicit_reasoning_effort)`. If one of them is `none` and the other is not, the request is 400 `invalid_parameter` naming `reasoning_effort`.

## Set the effort

This request asks `openai/gpt-5.6-luna` for a low effort and reads the model's reasoning beside its answer:

```python tab="Python"
from openai import OpenAI
import os

client = OpenAI(
    base_url="https://api.llm.buzz/api/v1",
    api_key=os.environ["LLM_BUZZ_KEY"],
)
reply = client.chat.completions.create(
    model="openai/gpt-5.6-luna",
    messages=[{"role": "user", "content": "A train leaves at 9:40 and the trip takes 2 h 35 min. When does it arrive?"}],
    extra_body={"reasoning": {"effort": "low"}},
)
message = reply.choices[0].message
print((message.model_extra or {}).get("reasoning"))
print(message.content)
```

```ts tab="TypeScript"
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.llm.buzz/api/v1",
  apiKey: process.env.LLM_BUZZ_KEY,
});
const reply = await client.chat.completions.create({
  model: "openai/gpt-5.6-luna",
  messages: [{ role: "user", content: "A train leaves at 9:40 and the trip takes 2 h 35 min. When does it arrive?" }],
  reasoning: { effort: "low" },
} as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming);
const message = reply.choices[0].message as { content: string | null; reasoning?: string };
console.log(message.reasoning);
console.log(message.content);
```

```bash tab="curl"
curl https://api.llm.buzz/api/v1/chat/completions \
  -H "Authorization: Bearer $LLM_BUZZ_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.6-luna",
    "messages": [{"role": "user", "content": "A train leaves at 9:40 and the trip takes 2 h 35 min. When does it arrive?"}],
    "reasoning": {"effort": "low"}
  }'
```

`reasoning` is not a field of the SDKs' Chat Completions method, so the Python call sends it through `extra_body` and the TypeScript call casts its parameters; `reasoning_effort` is one, and takes the effort alone. On Responses `reasoning` is an SDK field, and it can ask for a summary of the reasoning:

```python
response = client.responses.create(
    model="openai/gpt-5.6-luna",
    input="A train leaves at 9:40 and the trip takes 2 h 35 min. When does it arrive?",
    reasoning={"effort": "low", "summary": "auto"},
)
for item in response.output:
    if item.type == "reasoning":
        for part in item.summary:
            print(part.text)
print(response.output_text)
```

On Messages, `anthropic/claude-sonnet-5.5` thinks adaptively and takes its depth from `output_config.effort`. The Anthropic SDK builds `client` as in [Messages](messages.md#send-a-request):

```python
message = client.messages.create(
    model="anthropic/claude-sonnet-5.5",
    max_tokens=4096,
    thinking={"type": "adaptive"},
    output_config={"effort": "low"},
    messages=[{"role": "user", "content": "A train leaves at 9:40 and the trip takes 2 h 35 min. When does it arrive?"}],
)
for block in message.content:
    if block.type == "thinking":
        print(block.thinking)
    elif block.type == "text":
        print(block.text)
```

## How an effort is mapped

A model takes some efforts and not others, and llm.buzz moves your effort to one the model takes instead of failing the request.

- Where llm.buzz knows a model's efforts, an effort the model does not take becomes the nearest one it does, and a tie takes the lower. `x-sgr-ignored-parameters` names the move as `reasoning_effort-><level>` on every API. `minimal` on `openai/gpt-5.6-luna` is served as `low`, named `reasoning_effort->low`, and `xhigh` on `openai/gpt-5-mini`, whose highest effort is `high`, is served as `high`.
- `none` never becomes an active effort, and an active effort never becomes `none`. A model with no off level refuses `none` with a 400 before any provider is called, naming `reasoning_effort` on Chat Completions, whichever spelling you sent, and `reasoning.effort` on Responses. Today that includes Claude Opus 5.5, Sonnet 5.5, Fable 5 and Fable 5.1, and OpenAI's gpt-5-mini, gpt-5-nano, gpt-6-astra and gpt-6.1-sol. On Messages, `thinking: {"type": "disabled"}` is refused for the Claude models among them; Sonnet 5.5 offers `{"type": "between_tools"}` in its place. The refusal holds on every route of these models, llm.buzz's own provider included: a Claude model takes the efforts its family takes, so an effort the family refuses is refused before any provider is called, whichever API the provider speaks.
- A switch with no level, such as `reasoning.enabled: true` or `enable_thinking: true`, takes llm.buzz's default effort for the model, which is medium unless the model has another. On Messages, a bare `adaptive` sent to Claude carries no effort, so Claude's own default applies.
- Where llm.buzz does not know a model's efforts, your value goes to the provider as you sent it. A provider that refuses it answers 400, with its own sentence after `provider rejected the request:` and `metadata.param` naming the field.
- If you send no setting, llm.buzz sends no effort, and the model runs at its own default. One exception: on Messages, a `max_tokens` under 1,024 with no setting of your own, on a model whose providers all reason by default and offer `none`, is sent at effort `none`, named `reasoning_effort->none(max_tokens_headroom)`, so the reply is text ([Thinking](messages.md#thinking)).
- OpenAI's reasoning models and the Claude models that think adaptively take sampling settings only where the provider does: [Sampling settings a model does not take](api-reference.md#sampling-settings-a-model-does-not-take).

## Token budgets

Instead of an effort you can give a number of thinking tokens. Not every provider has a field for one, and llm.buzz never turns a budget into an effort: where none of a model's providers can take it, the request is refused before any provider is called, naming the field.

- OpenAI's models take no budget. Neither do the Claude models llm.buzz lists today, all of which are in the families that think adaptively (Opus 4.7 onward, Sonnet 5, Haiku 5 and Fable 5). A budget sent to one answers 400 `unsupported_parameter` on Chat Completions and 400 `invalid_request_error` on Messages, naming the budget field you sent. Choose an effort for these models.
- On Chat Completions, `thinking.budget_tokens` is an integer of at least 1,024 and below the output limit; any other value is 400 `invalid_parameter` naming it. On Messages, `thinking.budget_tokens` and `reasoning.max_tokens` must be below `max_tokens`, so the answer has room.
- On a Claude model that takes a budget, which none on llm.buzz does today, a `reasoning.max_tokens` under 1,024 is raised to 1,024 and named `reasoning.max_tokens->1024(anthropic_minimum)`.

## Read the reasoning

| | Chat Completions | Responses | Messages |
|---|---|---|---|
| Where | `message.reasoning`, and `delta.reasoning` in a stream | A `reasoning` item in `output`, with `summary_text` parts | `thinking` blocks in `content` |

Readable reasoning is what the provider returns: reasoning text, thinking blocks or a summary. Where a provider withholds it by default, llm.buzz asks for it: Claude's thinking as a summary, and OpenAI's reasoning as an automatic summary unless you chose a `summary` or the effort is `none`. On Messages, Anthropic's own thinking blocks are signed. Reasoning from a provider other than Anthropic arrives as one unsigned `thinking` block.

`reasoning: {"exclude": true}` and `include_reasoning: false` keep the text out of the answer, in a stream too. The model still reasons, and you are billed for it. On Messages, Anthropic's own signed thinking blocks stay in the answer, because the next request needs them to continue. Responses has no field to hide a summary.

## What reasoning costs

- Reasoning tokens are output tokens. They count toward `max_tokens`, `max_completion_tokens` or `max_output_tokens`, so a limit that is too low can end the answer inside the reasoning, with `finish_reason: length` on Chat Completions, `status: incomplete` on Responses or `stop_reason: max_tokens` on Messages.
- They are billed at the reasoning price of the provider that answered, and at its output price when the provider sets none. A model's public document lists `reasoning` among its prices ([Public models](/docs/api/public-models)). [Pricing](pricing.md) explains how a request is priced. Hiding the reasoning text saves nothing.
- Chat Completions reports them in `usage.completion_tokens_details.reasoning_tokens`, a part of `completion_tokens`, where the provider reports a count. A stream's usage chunk always has it, `0` when the provider reported none, with `usage.unreported_token_details` listing `reasoning_tokens` ([Usage and cost](chat-completions.md#usage-and-cost)). Claude reports no separate count: its thinking is inside `completion_tokens` (`output_tokens` on Messages).

## Keep reasoning in the conversation

Send the conversation back unchanged, reasoning included.

- **Chat Completions.** An assistant message may carry `reasoning`, `reasoning_content` and OpenRouter's `reasoning_details` (its `reasoning.text` blocks). A provider that takes earlier reasoning back gets it. For another, llm.buzz drops it and names it, as `messages.reasoning_content->dropped(unsupported_by_provider)`; the request still serves.
- **Responses.** Send `reasoning` items back as they came, and ask for `include: ["reasoning.encrypted_content"]` when you send `store: false`: [Input items](responses.md#input-items).
- **Messages.** Send every `thinking` and `redacted_thinking` block back with its assistant turn. Claude's signed blocks go back to Claude; another provider receives the turn without them, and `x-sgr-ignored-parameters` names it `messages.thinking->dropped(unsupported_by_provider)`.

A tool turn whose reasoning a provider keeps for the next request stays with that provider. While it can serve, `provider.order`, `only`, `ignore` and the routing preset do not move the request off it, and `allow_fallbacks: false` keeps it there alone. If it is down, the model's other providers serve the turn without that reasoning, and the visible conversation is unchanged ([Provider preferences](models-and-routing.md#provider-preferences)).

## Related

- [Chat Completions](chat-completions.md), [Responses](responses.md) and [Messages](messages.md) list every field of their API.
- [Tools and structured output](tools-and-structured-output.md) and [Streaming](streaming.md) cover the rest of what the three text APIs share.
- [Usage and limits](usage-and-limits.md) collects the limits of every API.
