Reasoning & extended thinking
One parameter — reasoning_effort — controls extended thinking across OpenAI, Anthropic, Google, Mistral and xAI models. Stav translates it per provider and per Provider Endpoint, and tells you what was sent.
Every provider shapes extended thinking differently: an effort enum, a token budget, an on/off mode. On /v1/chat/completions you send one parameter and Stav translates it.
resp = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Prove that √2 is irrational."}],
reasoning_effort="high",
)
print(resp.choices[0].message.content)
print(resp.usage.completion_tokens_details.reasoning_tokens)
The parameter
reasoning_effort is canonical. reasoning and thinking are aliases, as a string or an object with an effort field:
{ "reasoning_effort": "high" }
{ "reasoning": "high" }
{ "reasoning": { "effort": "high" } }
{ "thinking": { "effort": "high" } }
Levels, lowest to highest: none, minimal, low, medium, high, xhigh, max. Case and surrounding whitespace are ignored.
If more than one field is present, reasoning_effort wins, then reasoning, then thinking.
An object without a string effort is a 400. That includes Anthropic's native shape and on/off flags:
{ "thinking": { "type": "enabled", "budget_tokens": 4096 } }
{ "thinking": { "type": "disabled" } }
{ "reasoning": { "enabled": false } }
{
"error": {
"message": "'thinking' must be one of ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], or an object with a string 'effort' field, on /v1/chat/completions. Use 'reasoning_effort' for the simplest form.",
"type": "invalid_request_error",
"param": "reasoning_effort",
"code": "invalid_value"
}
}
Turning reasoning off
Send reasoning_effort: "none". Omitting the field leaves a model at its own default, and some models reason by default. none does not require a reasoning-capable model: on a model that cannot reason it is accepted and nothing is forwarded.
What each provider receives
| Provider | Translation |
|---|---|
| OpenAI, xAI and other OpenAI-compatible providers | reasoning_effort forwarded as is |
| Google (Gemini) | reasoning_effort; minimal → low, xhigh and max → high |
| Anthropic, newer models (Opus 4.7+, Opus 5, Sonnet 5) | thinking: {type: "adaptive"} plus output_config.effort; minimal → low |
| Anthropic, earlier models | thinking: {type: "enabled", budget_tokens: N} with max_tokens sized to fit |
| Mistral (Magistral) | prompt_mode: "reasoning". There is no level to vary. none sends prompt_mode: null |
On Anthropic, none sends no thinking block at all.
The Anthropic budget
For models that take a token budget:
| Level | budget_tokens |
|---|---|
minimal | 1,024 |
low | 2,048 |
medium | 8,192 |
high | 16,384 |
xhigh | 32,768 |
max | 65,536 |
Anthropic requires the budget to be below max_tokens. Stav:
- sets
max_tokensto budget + 512 when you did not set one - otherwise shrinks the budget to leave 512 tokens for the answer
- raises
max_tokensif even the 1,024 minimum does not fit
While reasoning is on, Anthropic rejects custom temperature and top_p, so Stav does not send them.
Levels are translated per Provider Endpoint
Not every endpoint accepts every level, and one model can accept different levels on different APIs. Stav records, per Provider Endpoint and surface, which levels are accepted.
If the serving endpoint does not accept your level, Stav sends the nearest accepted level. On a tie it picks the lower one, so a request never gets more expensive by rounding. If no list is recorded for the endpoint, your level passes through unchanged.
The level actually sent comes back in a header:
X-Stav-Reasoning-Effort: medium
It is the value after both steps — the endpoint step and the provider translation — or fixed for Mistral. The same value is on the request in Your workspace → Monitor → Request log. On a stream the header describes the first endpoint tried; if failover moves the request, the log has the final value.
What comes back on /v1/chat/completions
{
"choices": [{
"message": {
"role": "assistant",
"content": "…",
"reasoning_content": "…"
}
}],
"usage": {
"prompt_tokens": 42,
"completion_tokens": 1310,
"total_tokens": 1352,
"completion_tokens_details": { "reasoning_tokens": 1180 }
}
}
reasoning_tokensis part ofcompletion_tokens, not extra. Here 1,180 went to thinking and 130 to the answer.completion_tokens_detailsappears only when thinking tokens were spent.reasoning_contentcarries the thinking text when the provider returns it separately. Many do not (OpenAI does not), and then the field is absent. When streaming, it arrives asdelta.reasoning_content, never mixed intocontent.prompt_tokens_details.cached_tokensappears when part of the prompt was read from a cache.include_reasoningis refused with a400.
Streaming usage
Token counts arrive in a final chunk only if you ask:
stream = client.chat.completions.create(
model="auto",
messages=[...],
reasoning_effort="medium",
stream=True,
stream_options={"include_usage": True},
)
Without include_usage a stream reports no token counts.
/v1/messages
claude-* models are proxied to Anthropic. The native contract applies: thinking in the request, thinking blocks in the response, Anthropic's usage object as returned. Two Stav additions:
output_config.effortis translated per endpoint like any other level.- A top-level
reasoning_effortis turned into Anthropic's native shape before forwarding.
msg = anthropic_client.messages.create(
model="anthropic/claude-sonnet-4-5",
max_tokens=8192,
thinking={"type": "enabled", "budget_tokens": 4096},
messages=[{"role": "user", "content": "Plan a migration from Postgres 14 to 17."}],
)
for block in msg.content:
if block.type == "thinking":
print("[thinking]", block.thinking[:
Other models (including auto when it picks a non-Claude model) are translated to the chat path. Your thinking object becomes a level: output_config.effort is used as is; budget_tokens is bucketed to the highest level whose budget it reaches, up to high; type: "disabled" means no reasoning. On this path the response has text and tool_use blocks only — no thinking blocks — and usage has input_tokens and output_tokens.
/v1/responses
reasoning.effort is the control. OpenAI models are proxied to OpenAI's Responses API as is. Other models are translated to the chat path; there usage.output_tokens_details.reasoning_tokens reports thinking tokens, and reasoning items and encrypted reasoning are not returned.
Which models can reason
A per-model flag, confirmed by probing rather than guessed from names:
info = client.models.retrieve("anthropic/claude-sonnet-4-5")
info.model_extra["stav_supports_thinking"] # True
It is also extended_thinking in stav_capabilities.
Name a model that cannot reason, with a level other than none, and the level is left out: the request is answered, and X-Stav-Dropped-Params names the field you sent (reasoning_effort, reasoning or thinking). No X-Stav-Reasoning-Effort header comes back, because no level was sent. Agents that send a default level on every request keep working on every model.
Reasoning and the router
With auto or a Router, a reasoning level other than none:
- filters the pool to reasoning-capable models before scoring, so failover candidates can reason too
- scales the expected output used for cost and speed:
minimal×1.25,low×2,medium×4,high×6, more forxhighandmax, capped bymax_tokens
If nothing eligible can reason, you get an error, not a silent downgrade.
When you send no level, the router may choose one for the model it picks — from the levels that model's Provider Endpoint accepts, and never above the model's own default unless your Router allows it. It picks less thinking when the request is short or max_tokens leaves no room for it, so a reasoning model does not spend the whole output limit thinking and answer nothing. A level you send is never changed. The response says which level was sent (X-Stav-Reasoning-Effort), and the request log says who chose it: you, the router, or the provider's default.
Each Router has a Reasoning setting: Auto (the router chooses, at most the model's default), Never (no reasoning unless you ask for it), or At most a level; and a switch to let the router go above the model's default.
Cost
Reasoning tokens are output tokens and are billed as output. Where an endpoint has a separate reasoning rate, that rate is used.
Treat effort as a cost dial. high on a hard problem can be worth it; high on a summary multiplies the bill for nothing. A common pattern:
def complete(prompt, hard=False):
kwargs = {"reasoning_effort": "high"} if hard else {}
return client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": prompt}],
**kwargs,
)
The router then picks a cheap model for the easy path and a reasoning model for the hard one, from the same call site. Measure on your own tasks before turning reasoning on everywhere.