# Reasoning control
Most models on the catalog are hybrid reasoners, they can think before answering. Every model vendor
invented a different switch for it. On Keln there is one field in and one field out, identical
on every model.
## One switch: `reasoning_effort`
```json
{
"model": "deepseek-ai/deepseek-v4.1-flash",
"messages": [{"role": "user", "content": "23 × 47?"}],
"reasoning_effort": "none"
}
```
- `"none"`, answer directly, no thinking. Lower latency, fewer billed tokens.
- `"low"` / `"medium"` / `"high"` / `"max"`, thinking on, at that depth where the serving route
grades it (`"xhigh"` is accepted as an alias of `"max"`).
- Omitted, **thinking on, at the model's deepest setting**. See below.
`reasoning_effort` is a depth control, not a token budget: bound the output with `max_tokens`,
and where you want to hint a thinking budget use the OpenRouter-style
`reasoning: {"max_tokens": N}`, honored where the serving route supports a thinking budget.
## The default is full depth
Send no reasoning field and a thinking-capable model thinks at its deepest setting. This
default prevents a request from silently getting less reasoning than you assumed; levels exist to
ask for less, and `"none"` turns thinking off.
In practice:
- Requests that send no reasoning field bill more output tokens (thinking bills as output) and
answer more slowly than a `"none"` request, on a hard prompt, several times more of both.
- It is the policy across the catalog, **including models whose upstream default is off**
(`google/gemma-4-*`), on Keln they think unless you say otherwise.
- Only an explicit `"none"` (or a vendor alias meaning off) disables thinking.
```json
{ "model": "google/gemma-4-31b-it",
"messages": [{"role": "user", "content": "Summarize this changelog."}],
"reasoning_effort": "none" }
```
Cost-sensitive, high-volume, or latency-critical traffic should send `"none"` explicitly rather
than relying on a default.
Keln accepts vendor spellings as aliases and maps them to the same canonical behavior, so your
existing code works unchanged: `enable_thinking`, `thinking: {"type": "disabled"}`,
`chat_template_kwargs.enable_thinking`, and the OpenRouter/Vercel object, `reasoning:
{"enabled": true | false}`, `reasoning: {"effort": …}`, `reasoning: {"max_tokens": …}` (a
thinking budget implies **on**, honored where the serving route supports a thinking budget).
`include_reasoning` (OpenRouter legacy) is accepted and inert, the trace comes back either way,
as is `reasoning: {"exclude": true}` ([migration ledger](../migrate.md#every-openrouter-field-accounted-for)).
## One trace field: `reasoning_content`
The thinking trace always comes back the same way, whatever the model, in the canonical
`reasoning_content`, mirrored in `reasoning` so OpenRouter/Vercel-style readers work unchanged:
=== "Non-streaming"
```json
"message": {
"role": "assistant",
"reasoning_content": "The user wants 23 × 47…",
"reasoning": "The user wants 23 × 47…",
"content": "23 × 47 = 1,081."
}
```
=== "Streaming"
```json
data: {"choices":[{"delta":{"reasoning_content":"The user wants","reasoning":"The user wants"}}]}
data: {"choices":[{"delta":{"content":"23 × 47 = "}}]}
```
No per-vendor tag formats, no `<think>` blocks to parse out of `content`. Both keys always carry
the identical trace; `reasoning_content` is the canonical name.
## `reasoning_tokens` in usage
Thinking bills as output, so the usage object itemizes it, the standard OpenAI shape, on every
route (streaming delivers it in the final usage frame with `stream_options.include_usage`):
```json
"usage": {
"prompt_tokens": 44,
"completion_tokens": 1090,
"completion_tokens_details": { "reasoning_tokens": 950 }
}
```
`completion_tokens` stays the billable total; `reasoning_tokens` itemizes how much of it was
thinking. Where the serving route reports the split it is passed through as reported; where it
doesn't, Keln derives it. `0` means no reasoning happened; a missing field means the split
could not be established. The tokens are part of `completion_tokens` either way, not billed
separately.
## Models that cannot disable reasoning
A few models reason **always-on by design**: `moonshotai/kimi-k2.7-code`, `openai/gpt-oss-120b`,
`qwen/qwen3.8-2.4t-a95b`, `zai-org/glm-5.3` and `zai-org/glm-5.3-flash` cannot be told to stop thinking. Ask one to disable it and you get an
explicit error:
```json
{"error": {"code": "reasoning_not_disableable",
"message": "Reasoning cannot be disabled for this model."}}
```
Every model on the catalog today is reasoning-capable, so the switch either takes effect or
returns this error.
## Where each model stands
Every model accepts all of the values above. On each model the levels that change depth are
listed once; a level that is not listed runs the same depth as a listed one (on `zai-org/glm-5.2`,
`low` and `medium` run like `high`). The same facts are machine-readable in the `reasoning` block
of [`GET /v1/models`](../api/models.md).
<!-- REASONING:BEGIN — generated by services/exchange/scripts/render_reasoning_table.py from
services/exchange/spillover.json (the same derivation as /v1/models `reasoning`). Do not
edit by hand: run the script. The control-plane test test_docs_reasoning_mirror.py fails
whenever this block is stale. -->
| Model | `none` (thinking off) | Levels that change depth | Omitted level runs |
|---|---|---|---|
| `deepseek-ai/deepseek-v4-pro-0813` | yes | `low`, `high`, `max` | `max` |
| `deepseek-ai/deepseek-v4.1-flash` | yes | `low`, `high`, `max` | `max` |
| `google/gemma-4-31b-it` | yes | on/off only | thinking on |
| `minimaxai/minimax-m3` | yes | on/off only | thinking on |
| `moonshotai/kimi-k2.7-code` | no, always on | no levels | thinking on |
| `moonshotai/kimi-k3` | yes | `low`, `high`, `max` | `max` |
| `nvidia/nemotron-3-ultra-550b-a55b` | yes | `medium`, `high` | `high` |
| `openai/gpt-oss-120b` | no, always on | `low`, `medium`, `high` | `high` |
| `qwen/qwen3.6-35b-a3b` | yes | on/off only | thinking on |
| `qwen/qwen3.8-2.4t-a95b` | no, always on | `low`, `medium`, `xhigh` | `xhigh` |
| `qwen/qwen3.8-27b` | yes | `low`, `medium`, `xhigh` | `xhigh` |
| `zai-org/glm-5.2` | yes | `high`, `max` | `max` |
| `zai-org/glm-5.3` | no, always on | `low`, `high`, `max` | `max` |
| `zai-org/glm-5.3-flash` | no, always on | `low`, `high`, `max` | `max` |
<!-- REASONING:END -->
Reasoning control
Most models on the catalog are hybrid reasoners, they can think before answering. Every model vendor
invented a different switch for it. On Keln there is one field in and one field out, identical
on every model.
"none", answer directly, no thinking. Lower latency, fewer billed tokens.
"low" / "medium" / "high" / "max", thinking on, at that depth where the serving route
grades it ("xhigh" is accepted as an alias of "max").
Omitted, thinking on, at the model's deepest setting. See below.
reasoning_effort is a depth control, not a token budget: bound the output with max_tokens,
and where you want to hint a thinking budget use the OpenRouter-style
reasoning: {"max_tokens": N}, honored where the serving route supports a thinking budget.
The default is full depth
Send no reasoning field and a thinking-capable model thinks at its deepest setting. This
default prevents a request from silently getting less reasoning than you assumed; levels exist to
ask for less, and "none" turns thinking off.
In practice:
Requests that send no reasoning field bill more output tokens (thinking bills as output) and
answer more slowly than a "none" request, on a hard prompt, several times more of both.
It is the policy across the catalog, including models whose upstream default is off
(google/gemma-4-*), on Keln they think unless you say otherwise.
Only an explicit "none" (or a vendor alias meaning off) disables thinking.
{"model":"google/gemma-4-31b-it","messages":[{"role":"user","content":"Summarize this changelog."}],"reasoning_effort":"none"}
Cost-sensitive, high-volume, or latency-critical traffic should send "none" explicitly rather
than relying on a default.
Keln accepts vendor spellings as aliases and maps them to the same canonical behavior, so your
existing code works unchanged: enable_thinking, thinking: {"type": "disabled"},
chat_template_kwargs.enable_thinking, and the OpenRouter/Vercel object, reasoning:
{"enabled": true | false}, reasoning: {"effort": …}, reasoning: {"max_tokens": …} (a
thinking budget implies on, honored where the serving route supports a thinking budget).
include_reasoning (OpenRouter legacy) is accepted and inert, the trace comes back either way,
as is reasoning: {"exclude": true} (migration ledger).
One trace field: reasoning_content
The thinking trace always comes back the same way, whatever the model, in the canonical
reasoning_content, mirrored in reasoning so OpenRouter/Vercel-style readers work unchanged:
"message":{"role":"assistant","reasoning_content":"The user wants 23 × 47…","reasoning":"The user wants 23 × 47…","content":"23 × 47 = 1,081."}
data:{"choices":[{"delta":{"reasoning_content":"The user wants","reasoning":"The user wants"}}]}data:{"choices":[{"delta":{"content":"23 × 47 = "}}]}
No per-vendor tag formats, no <think> blocks to parse out of content. Both keys always carry
the identical trace; reasoning_content is the canonical name.
reasoning_tokens in usage
Thinking bills as output, so the usage object itemizes it, the standard OpenAI shape, on every
route (streaming delivers it in the final usage frame with stream_options.include_usage):
completion_tokens stays the billable total; reasoning_tokens itemizes how much of it was
thinking. Where the serving route reports the split it is passed through as reported; where it
doesn't, Keln derives it. 0 means no reasoning happened; a missing field means the split
could not be established. The tokens are part of completion_tokens either way, not billed
separately.
Models that cannot disable reasoning
A few models reason always-on by design: moonshotai/kimi-k2.7-code, openai/gpt-oss-120b,
qwen/qwen3.8-2.4t-a95b, zai-org/glm-5.3 and zai-org/glm-5.3-flash cannot be told to stop thinking. Ask one to disable it and you get an
explicit error:
{"error":{"code":"reasoning_not_disableable","message":"Reasoning cannot be disabled for this model."}}
Every model on the catalog today is reasoning-capable, so the switch either takes effect or
returns this error.
Where each model stands
Every model accepts all of the values above. On each model the levels that change depth are
listed once; a level that is not listed runs the same depth as a listed one (on zai-org/glm-5.2,
low and medium run like high). The same facts are machine-readable in the reasoning block
of GET /v1/models.