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.
reasoningis OpenRouter's object:effortormax_tokens, not both (a request with both is 400invalid_parameter), plusenabledandexclude. Its fields must agree: an effort tier or a budget besideenabled: false, oreffort: "none"besideenabled: true, is 400invalid_parameternamingreasoning.enabled.include_reasoning: trueisreasoning: {}andfalseisreasoning: {"exclude": true}.thinkingtakesenabled,adaptiveordisabled: a bareenabledoradaptiveswitches reasoning on at llm.buzz's default effort for the model (below), anenabledwithbudget_tokensis a budget (Token budgets), anddisabledis effortnone. The body'sx-sgr-ignored-parametersnames the move to an effort asthinking->translated(reasoning_effort). It says what llm.buzz did with the field, not that it dropped it. - Responses.
reasoningtakeseffort,summary,generate_summaryandcontext;summaryandgenerate_summarysent together must match.reasoning.modeis 400invalid_parameter, and so is any other key. Without asummary, llm.buzz asks OpenAI for an automatic one unless the effort isnone. - Messages.
thinkingtakes Anthropic's forms, described under Thinking. On the Claude models llm.buzz lists today, a bare{"type": "enabled"}runs adaptive thinking, namedthinking.type->adaptive, andbudget_tokensis refused (Token budgets).reasoningis OpenRouter's object, as on Chat Completions, and its fields must agree: an effort tier or a budget besideenabled: false, oreffort: "none"besideenabled: true, is a 400invalid_request_errorwhose message ends(param: reasoning.enabled).reasoning.effortwithreasoning.max_tokens, or areasoning.effort,reasoning.max_tokensoroutput_config.effortbeside athinking.budget_tokens, is a 400invalid_request_error. - A control that belongs to another API is dropped and named in
x-sgr-ignored-parameters, as Chat'sreasoning_effortis on Messages. Send one depth control per request: an effort and a budget in one Chat Completions request, or two budgets, are 400invalid_parameter, andmetadata.paramnames one of the controls. Two efforts in one Chat Completions request,reasoning_effortwithreasoning.effort, are not refused:reasoning_effortis used, in either order and in a stream, andx-sgr-ignored-parametersnames the other asreasoning->ignored(explicit_reasoning_effort). If one of them isnoneand the other is not, the request is 400invalid_parameternamingreasoning_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:
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)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);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:
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:
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-parametersnames the move asreasoning_effort-><level>on every API.minimalonopenai/gpt-5.6-lunais served aslow, namedreasoning_effort->low, andxhighonopenai/gpt-5-mini, whose highest effort ishigh, is served ashigh. nonenever becomes an active effort, and an active effort never becomesnone. A model with no off level refusesnonewith a 400 before any provider is called, namingreasoning_efforton Chat Completions, whichever spelling you sent, andreasoning.efforton 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: trueorenable_thinking: true, takes llm.buzz's default effort for the model, which is medium unless the model has another. On Messages, a bareadaptivesent 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:andmetadata.paramnaming 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_tokensunder 1,024 with no setting of your own, on a model whose providers all reason by default and offernone, is sent at effortnone, namedreasoning_effort->none(max_tokens_headroom), so the reply is text (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.
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_parameteron Chat Completions and 400invalid_request_erroron Messages, naming the budget field you sent. Choose an effort for these models. - On Chat Completions,
thinking.budget_tokensis an integer of at least 1,024 and below the output limit; any other value is 400invalid_parameternaming it. On Messages,thinking.budget_tokensandreasoning.max_tokensmust be belowmax_tokens, so the answer has room. - On a Claude model that takes a budget, which none on llm.buzz does today, a
reasoning.max_tokensunder 1,024 is raised to 1,024 and namedreasoning.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_tokensormax_output_tokens, so a limit that is too low can end the answer inside the reasoning, withfinish_reason: lengthon Chat Completions,status: incompleteon Responses orstop_reason: max_tokenson 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
reasoningamong its prices (Public models). Pricing 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 ofcompletion_tokens, where the provider reports a count. A stream's usage chunk always has it,0when the provider reported none, withusage.unreported_token_detailslistingreasoning_tokens(Usage and cost). Claude reports no separate count: its thinking is insidecompletion_tokens(output_tokenson Messages).
Keep reasoning in the conversation
Send the conversation back unchanged, reasoning included.
- Chat Completions. An assistant message may carry
reasoning,reasoning_contentand OpenRouter'sreasoning_details(itsreasoning.textblocks). A provider that takes earlier reasoning back gets it. For another, llm.buzz drops it and names it, asmessages.reasoning_content->dropped(unsupported_by_provider); the request still serves. - Responses. Send
reasoningitems back as they came, and ask forinclude: ["reasoning.encrypted_content"]when you sendstore: false: Input items. - Messages. Send every
thinkingandredacted_thinkingblock back with its assistant turn. Claude's signed blocks go back to Claude; another provider receives the turn without them, andx-sgr-ignored-parametersnames itmessages.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).
Related
- Chat Completions, Responses and Messages list every field of their API.
- Tools and structured output and Streaming cover the rest of what the three text APIs share.
- Usage and limits collects the limits of every API.