Thinking chunks: reading the model's reasoning
On this page
Lesson rewritten on November 2, 2026 based on the Handling thinking chunks section of the Mistral documentation. The previous version read reasoning from a
textfield in the thinking chunk: that field does not exist; reasoning is a list of text chunks.
Two response shapes
With reasoning_effort="high", the message.content field of a response is no longer a string but a list of chunks. With reasoning_effort="none", it’s again a plain string, with no trace of reasoning. Robust code always handles both shapes: that’s the golden rule of this lesson.
Anatomy of a thinking chunk
Two chunk types appear in the list:
ThinkChunk(type: "thinking") carries the reasoning trace. Itsthinkingfield is itself a list ofTextChunkfragments;TextChunk(type: "text") carries the final answer, in itstextfield.
Here’s the shape of a Mistral Small 4 response, taken from the official example (texts abbreviated):
{
"model": "mistral-small-2603",
"usage": {"prompt_tokens": 77, "completion_tokens": 1114, "total_tokens": 1191},
"choices": [{
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": [
{"type": "thinking", "thinking": [
{"type": "text", "text": "Okay, let's try to figure out John's age..."}
]},
{"type": "text", "text": "Let's denote the ages of the sisters as follows..."}
]
}
}]
}
The Python SDK object model follows this structure exactly: ThinkChunk.thinking holds the reasoning chunks, and each TextChunk exposes its content via .text.
Parsing the response in Python
The official code distinguishes chunks with isinstance, after ruling out the plain-string case:
from mistralai.client.models import TextChunk, ThinkChunk
content = chat_response.choices[0].message.content
if isinstance(content, str): # reasoning_effort="none"
print(content)
else:
for chunk in content:
if isinstance(chunk, ThinkChunk):
print("--- reasoning ---")
for inner in chunk.thinking:
if isinstance(inner, TextChunk):
print(inner.text)
print("--- end of reasoning ---")
elif isinstance(chunk, TextChunk):
print(chunk.text)
In an application, you’ll want a function that returns reasoning and answer separately. We’ll call it separate and reuse it later in the course:
from mistralai.client.models import TextChunk, ThinkChunk
def separate(content):
"""Return (reasoning, answer) for content that is either a string or a list of chunks."""
if isinstance(content, str):
return "", content
reasoning, answer = [], []
for chunk in content or []:
if isinstance(chunk, ThinkChunk):
reasoning.extend(i.text for i in chunk.thinking if isinstance(i, TextChunk))
elif isinstance(chunk, TextChunk):
answer.append(chunk.text)
return "".join(reasoning), "".join(answer)
reasoning, answer = separate(chat_response.choices[0].message.content)
Displaying reasoning in an interface
Separating the two parts lets you decide what the user sees. Three common approaches:
- Hide the reasoning and show only the answer, for a consumer-facing assistant;
- Collapse the reasoning into a block the user can open if they wish, for an expert tool;
- Show the full reasoning, for an educational tutor where the process matters as much as the result.
def render_html(content, show_reasoning=False):
reasoning, answer = separate(content)
html = f"<div class='answer'>{answer}</div>"
if show_reasoning and reasoning:
html = (
"<details><summary>View model reasoning</summary>"
f"<pre>{reasoning}</pre></details>" + html
)
return html
In a real project, escape the text before inserting it into HTML (for example with html.escape): both reasoning and answer are generated text, not trusted markup.
Streaming in three phases
In streaming, delta.content changes shape as the response unfolds:
- Reasoning phase:
delta.contentis a list containing aThinkChunk; - Transition: a single list containing both the closing
ThinkChunkand the firstTextChunk; - Answer phase:
delta.contentis a plain string.
The following code, adapted from the official example, displays both phases by tracking a state flag:
import os
from mistralai.client import Mistral
from mistralai.client.models import TextChunk, ThinkChunk
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
in_reasoning = False
for event in client.chat.stream(
model="mistral-large-4-0",
messages=[{"role": "user", "content": "What is 17 × 23?"}],
reasoning_effort="high",
):
delta = event.data.choices[0].delta.content
if not delta:
continue
if isinstance(delta, str): # answer phase
if in_reasoning:
print("\n--- end of reasoning ---")
in_reasoning = False
print(delta, end="", flush=True)
continue
for chunk in delta: # reasoning phase and transition
if isinstance(chunk, ThinkChunk):
if not in_reasoning:
print("--- reasoning ---")
in_reasoning = True
for inner in chunk.thinking:
if isinstance(inner, TextChunk):
print(inner.text, end="", flush=True)
elif isinstance(chunk, TextChunk):
if in_reasoning:
print("\n--- end of reasoning ---")
in_reasoning = False
print(chunk.text, end="", flush=True)
Streaming changes the experience: instead of waiting for a long reasoning process to finish in front of a frozen screen, the user sees the model at work, or at least an activity indicator if you choose to hide the reasoning.
Key takeaways
contentis a string withnone, a list of chunks withhigh: always handle bothThinkChunk.thinkingis a list ofTextChunk; the final answer lives in a top-levelTextChunk- A
separatefunction that returns (reasoning, answer) simplifies the rest of your code - In streaming: reasoning list, mixed transition, then answer strings
- Escape both reasoning and answer before displaying them