Thinking
Some models — Qwen3, Phi-4-reasoning, gpt-oss, and the like — reason through a problem step by step before giving an answer. They're worth reaching for on maths, logic, multi-step planning, and tricky code, where the extra deliberation noticeably improves accuracy.
What the model page shows
On a model's page, the header shows the model's reasoning style under Reasoning:
- Always on — the model thinks before it answers, as described below.
- Hybrid (toggle thinking) — same, but you can switch the thinking off per request when you want a fast, direct reply (see Turn thinking off below).
How it works
There's no special flag — send a normal chat completion to a
reasoning-capable model and Pendra does the rest. It keeps the model's
chain-of-thought separate from its final answer: the thinking comes back
on a reasoning_content field (streamed as
delta.reasoning_content) — and, for wider client
compatibility, mirrored on reasoning — while the answer stays
in the usual content. In the Playground the thinking shows up
in a collapsible Thinking block above the reply.
from pendra import Pendra
client = Pendra()
response = client.chat.completions.create(
model="qwen3.6:27b",
messages=[{"role": "user", "content": "If a train leaves at 3pm going 60mph..."}],
)
msg = response.choices[0].message
print(msg.reasoning) # the model's working
print(msg.content) # the final answer
Thinking — The train leaves at 3pm at 60mph. To find arrival time I need the distance… 60mph means 1 mile a minute, so 90 miles takes 90 minutes…
It arrives at 4:30pm.
Control the effort
Set reasoning_effort to "low",
"medium", or "high" to trade speed against depth
on models that support it. Higher effort spends more tokens thinking and
takes longer; lower effort answers faster. Some models accept only some of
these levels. If you send one a model doesn't accept, the request still
succeeds, but the model uses its own default level instead, and for some
models that default is to answer without thinking. The tokens spent reasoning are
reported under
usage.completion_tokens_details.reasoning_tokens so you can
see what each request cost.
Limit how long it thinks
On a hard or open-ended prompt a reasoning model can keep thinking for a
very long time before it starts to answer. Give it a thinking budget, in
tokens, with reasoning.max_tokens. When the budget is used up,
Pendra ends the thinking and the model goes straight on to its answer,
using what it has worked out so far.
{
"model": "qwen3.6:27b",
"messages": [{ "role": "user", "content": "List the data-quality problems in this CSV: ..." }],
"max_tokens": 1500,
"reasoning": { "max_tokens": 4096 }
}
The reply is an ordinary completion. When the budget was reached, it also says so:
{
"choices": [{ "index": 0, "message": { ... }, "finish_reason": "stop" }],
"usage": { "completion_tokens": 4870, "completion_tokens_details": { "reasoning_tokens": 4102 } },
"pendra": {
"notice": {
"code": "reasoning_budget_reached",
"message": "Thinking was capped at 4096 tokens and the model moved on to its answer. ..."
}
}
}
The budget you get without asking
You don't have to set anything to be protected. A request that names no
thinking budget gets a default one, and a request that sets no
max_tokens gets a default limit on the whole reply:
| You send | Thinking budget |
|---|---|
| Nothing about thinking | 8,192 tokens |
"reasoning_effort": "low" | 2,048 tokens |
"reasoning_effort": "medium" | 8,192 tokens |
"reasoning_effort": "high" | 24,576 tokens |
"reasoning": { "max_tokens": N } | The number you set |
-
The default works like a budget you set yourself. When
it is used up the thinking ends, the model goes on to its answer, and
the reply carries the
reasoning_budget_reachednotice. A request that finishes thinking inside it is not affected. -
A reply with no
max_tokensstops at 32,768 tokens, thinking and answer together. You getfinish_reason: "length"and a notice withcode: "truncated_at_default_max_tokens". Setmax_tokensto allow a longer reply. -
The default never costs you the answer. On the few
models that can't be moved on to their answer part-way through thinking
(gpt-oss and Gemma 4 among them), the default thinking budget is not
applied. They think as they did before, limited by your
max_tokensor the 32,768-token limit.
A budget you set yourself follows the same rules:
-
The budget is separate from
max_tokens. With a thinking budget set,max_tokensis kept for the answer and the thinking comes on top. The request above can use up to 4,096 tokens thinking and then up to 1,500 answering, so a smallmax_tokensno longer comes back as an empty answer. -
The thinking ends at a natural point. Pendra lets the
model finish its current line, for about 32 tokens past the budget, so
reasoning_tokenscan come back slightly above the number you set. -
You're told when it happens. The reply carries a
pendra.noticewithcode: "reasoning_budget_reached". The reply is complete; the notice only says the thinking was cut short, in case you want to raise the budget and try again. -
0turns thinking off, the same as"reasoning_effort": "none". There is no value for unlimited; set a larger number if you want more. - A very large budget is trimmed to fit. Thinking can use at most half of the context window left after your prompt, so there is always room for an answer.
On the Anthropic Messages API the same budget is
thinking.budget_tokens, and on the Responses API it is
reasoning.max_tokens:
{
"model": "qwen3.6:27b",
"max_tokens": 1500,
"thinking": { "type": "enabled", "budget_tokens": 4096 },
"messages": [{ "role": "user", "content": "List the data-quality problems in this CSV: ..." }]
}
A few models, gpt-oss and Gemma 4 among them, can't yet be moved on to
their answer part-way through thinking. On those, a budget that runs out
stops the reply instead: you get finish_reason: "length", the
thinking so far, no answer, and a notice with
code: "truncated_at_reasoning_budget". Raise the budget, or
turn thinking off for that request.
That stop applies only to a budget you set with
reasoning.max_tokens. The default budget is not applied on
those models, as described above. On the Anthropic Messages API,
thinking.budget_tokens is never allowed to cost you the answer
either: on those same models it is not enforced as a limit and only sets
how much effort the model puts into thinking, so the reply still ends with
an answer. On every other reasoning model it is enforced as described
above.
Turn thinking off
Sometimes you want a reasoning model to answer directly — for a quick
classification, a short completion, or when you don't want to wait for the
thinking or pay for its tokens. Send
"enable_thinking": false to skip the chain-of-thought and get
straight to the answer:
{
"model": "qwen3.6:27b",
"messages": [{ "role": "user", "content": "Classify: 'ship it'. positive or negative?" }],
"max_tokens": 5,
"enable_thinking": false
}
Through an SDK, it's the same field:
response = client.chat.completions.create(
model="qwen3.6:27b",
messages=[{"role": "user", "content": "Classify: 'ship it'. positive or negative?"}],
max_tokens=5,
enable_thinking=False,
)
"reasoning_effort": "none" (or the alias "minimal") does the same thing.
What happens when you don't say
Most reasoning models think by default, so you only opt out when you mean
to. A few small hybrid models answer directly by default instead, because
at that size their chain-of-thought tends to run on without ever reaching
an answer. To make one of them think, ask for it explicitly with
"enable_thinking": true or a reasoning_effort of
"low", "medium" or "high".
To see which way a model leans, check default_thinking on it in
the models list. false means it
answers directly unless you ask it to think:
{
"id": "qwen3.5:0.8b",
"type": "chat",
"capabilities": ["completion", "vision", "tools", "thinking"],
"reasoning": "hybrid",
"default_thinking": false
}
The thinking budget stops most replies from coming back empty. Where no
budget is in force (a model the default isn't applied to, or an older
worker), thinking can still use up the whole of a small
max_tokens. When that happens Pendra attaches a short advisory to the
response at pendra.notice — with
code: "truncated_during_reasoning" and a message explaining
what happened — so you can detect the case in code and retry with a larger
max_tokens, with a
thinking budget, or with thinking turned off. The
partial thinking is still returned on reasoning_content.
Both SDKs give you the advisory directly, so the empty answer is explainable without inspecting the raw JSON:
response = client.chat.completions.create(
model="qwen3.6:27b",
messages=[{"role": "user", "content": "Which is heavier, 1kg of steel or 1kg of feathers?"}],
max_tokens=200,
)
if response.notice and response.notice.code == "truncated_during_reasoning":
print(response.notice.message)
print(response.usage.reasoning_tokens) # the budget it spent thinking
# Retry with a bigger max_tokens, or with enable_thinking=False.
When streaming, the advisory arrives on its own chunk shortly before the end
— chunk.notice in Python, chunk.pendra?.notice in
JavaScript — so check it on every chunk rather than only the last.