Skip to content

Reasoning control

Most models on the catalog are hybrid reasoners, they can think before answering. Every vendor invented a different switch for it. On Keln there is one field in, one field out, identical on every model.

One switch: reasoning_effort

{
  "model": "deepseek-ai/deepseek-v4-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 has a budget knob.

The default is full depth

Send no reasoning field and a thinking-capable model thinks at its deepest setting. This default protects against a request quietly getting less reasoning than assumed; levels exist to ask for less, and "none" turns thinking off.

What that means 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. Nothing else does.
{ "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.

Already using a vendor spelling? 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 has a budget knob), all accepted as aliases and folded into the same canonical behavior. Your existing code works unchanged. 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):

"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, stepfun-ai/step-3.7-flash, openai/gpt-oss-120b, and openai/gpt-oss-20b cannot be told to stop thinking. Ask one to disable it and you get an explicit error:

{"error": {"code": "reasoning_not_disableable",
           "message": "This model cannot disable reasoning."}}

Every model on the catalog today is reasoning-capable, so the switch either takes effect or returns this error.

Where each model stands

The rate card marks the always-on models; everything else is switchable per request.