Reasoning Models
Access model chain-of-thought reasoning traces alongside the final answer
Some models produce a chain-of-thought trace before the final answer. The trace is exposed alongside content so you can log it, show it to users, or strip it out.
Supported models
Reasoning capability varies by model family. See the Model Catalog for the current set of reasoning-capable models. The catalog is the source of truth and is kept up to date as models are added or retired.
How reasoning is returned
The reasoning trace is not always in the same field. It depends on the model family:
reasoning_contenton the message: used by most reasoning models, includingzai-org/GLM-5.3,openai/gpt-oss-120b, the Nemotron family, and Qwen3 thinking variants.reasoningon the message: used by some earlier GLM-5.x models and MiniMax-M2.5.- Inline
</thinking>blocks insidecontent: a small number of models emit the trace inline rather than in a separate field. Strip the block out ofcontentif you only want the final answer.
On the Chat Completions API you need to handle whichever of these the model you call returns. The Responses API normalizes the trace into a structured output item with type: "reasoning", regardless of model family, which is easier to handle uniformly.
Reading the trace (Chat Completions)
def extract_reasoning(message) -> tuple[str, str]:
"""Return (reasoning, final_answer) for any reasoning model."""
reasoning = ""
content = message.content or ""
# Most models: GLM-5.3, OpenAI-style, Nemotron, Qwen3 thinking
reasoning = getattr(message, "reasoning_content", None) or ""
# Earlier GLM-5.x / MiniMax
if not reasoning:
reasoning = getattr(message, "reasoning", None) or ""
# Inline thinking blocks in content (e.g. some Gemma variants)
import re
think = re.findall(r"<thinking>(.*?)</thinking>", content, re.DOTALL)
if think and not reasoning:
reasoning = "\n".join(t.strip() for t in think)
content = re.sub(r"<thinking>.*?</thinking>", "", content, flags=re.DOTALL).strip()
return reasoning, contentNon-streaming response shape
{
"choices": [
{
"message": {
"role": "assistant",
"reasoning_content": "Let me work through this step by step...",
"content": "The answer is 4."
},
"finish_reason": "stop"
}
]
}A few families (some earlier GLM-5.x models, MiniMax-M2.5) put the trace in reasoning instead; the structure is otherwise the same.
Reasoning effort
Use reasoning_effort to control how much reasoning the model does before answering. Lower effort is faster and cheaper; higher effort is more thorough for hard problems.
| Value | When to use |
|---|---|
low | Simple tasks where speed matters |
medium | Default for most workloads |
high | Multi-step math, logic, planning |
max | Hardest problems; longest latency |
reasoning_effort is accepted on both endpoints:
- Chat Completions (
/v1/chat/completions): top-levelreasoning_effortfield. - Responses (
/v1/responses): top-levelreasoning_effort, or nested asreasoning: { effort: "..." }.
Not every model honors every value identically. The mapping from effort to actual token budget is model-specific; treat the table above as guidance, not a guarantee. Models that are not reasoning-capable will silently ignore reasoning_effort. To see which effort values a specific model accepts, check its reasoning_efforts list on GET /v1/model/info.
from openai import OpenAI
import os
client = OpenAI(
api_key=os.environ["NEBUL_API_KEY"],
base_url="https://api.inference.nebul.io/v1"
)
# GLM-5.3 returns the trace in reasoning_content
response = client.chat.completions.create(
model="zai-org/GLM-5.3",
messages=[{"role": "user", "content": "What is 15% of 80?"}],
reasoning_effort="high",
)
msg = response.choices[0].message
reasoning = getattr(msg, "reasoning_content", None) or getattr(msg, "reasoning", None) or ""
print("Reasoning:", reasoning)
print("Answer:", msg.content)Streaming
When streaming, reasoning tokens arrive first, followed by the final answer. Which delta field carries the reasoning depends on the model family, same as the non-streaming case: delta.reasoning_content for most models (including GLM-5.3), delta.reasoning for some earlier GLM-5.x models and MiniMax.
from openai import OpenAI
import os
client = OpenAI(
api_key=os.environ["NEBUL_API_KEY"],
base_url="https://api.inference.nebul.io/v1"
)
stream = client.chat.completions.create(
model="zai-org/GLM-5.3",
messages=[{"role": "user", "content": "What is 15% of 80?"}],
reasoning_effort="high",
stream=True,
)
answer_started = False
print("Thinking:")
for chunk in stream:
delta = chunk.choices[0].delta
reasoning = getattr(delta, "reasoning_content", None) or getattr(delta, "reasoning", None)
if reasoning:
print(reasoning, end="", flush=True)
if delta.content:
if not answer_started:
print("\n\n--- Answer ---\n")
answer_started = True
print(delta.content, end="", flush=True)Notes
- Latency: reasoning models take longer to produce the first token because they reason first. Budget for this in your timeouts.
- Logging the trace: even if you only surface the final answer to users, keep the reasoning trace in your logs. It is the fastest way to debug a wrong answer.
- Prompting: you do not need to tell a reasoning model to "think step by step". It does so by default. Reserve effort control for
reasoning_effort.