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
Response

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:

Response
{
  "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 sendThinking budget
Nothing about thinking8,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_reached notice. A request that finishes thinking inside it is not affected.
  • A reply with no max_tokens stops at 32,768 tokens, thinking and answer together. You get finish_reason: "length" and a notice with code: "truncated_at_default_max_tokens". Set max_tokens to 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_tokens or 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_tokens is 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 small max_tokens no 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_tokens can come back slightly above the number you set.
  • You're told when it happens. The reply carries a pendra.notice with code: "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.
  • 0 turns 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.

The thinking budget is applied by the worker that serves the request, so it needs an up-to-date worker, and that goes for the default budget too. On an older worker the default budget is not applied, and the oldest ignore a budget you set as well, so the model can think for as long as it likes. See Install a worker for how updates reach it.

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.

Thinking is a feature of the chat endpoint. For every field and the full response shape, see the Chat completions API reference.