mantismantis
selfhost

Thinking blocks

Models that reason out loud — DeepSeek-R1, QwQ, Marco-o1, R1-distills, and the OpenAI reasoning family — emit "thinking" content separately from the final answer. mantis-agent-sdk normalises both forms into a single ThinkingBlock you can render or hide.

Two formats, one block

Source Wire format Surface
DeepSeek-R1 / QwQ / Marco-o1 Inline <think>...</think> in the text stream. ThinkingBlock
DeepSeek API (out-of-band) thinking field on the message, separate from content. ThinkingBlock
OpenAI o1 / o3 / o4 API doesn't expose tokens — only counts. Not surfaced.

The runtime parses <think> tags out of the text stream so by the time you see content, thinking is its own block.

Streaming

async for event in agent.run_iter("..."):
    if event.type == "thinking_delta":
        print(f"\033[2m{event.text}\033[0m", end="", flush=True)
    elif event.type == "text_delta":
        print(event.text, end="", flush=True)

thinking_delta events stream as the thinking text arrives; text_delta events are the final answer.

Flat-shape messages

In query() output, ThinkingBlock appears in message.content:

from mantis_agent import MantisAgentOptions, query
 
async for msg in query(
    prompt="...", options=MantisAgentOptions(model="deepseek-r1:1.5b")
):
    if msg.type == "assistant":
        for block in msg.content:
            # Blocks are objects, not dicts — check the attribute you want.
            if getattr(block, "thinking", None):
                ...      # hide from end users; show in a "details" element
            elif getattr(block, "text", None):
                print(block.text)

Enabling / disabling

Some models gate thinking behind a request flag. Setting include_thinking=False strips it from the request body where supported, and removes any inline <think> blocks the model emits anyway:

There is no include_thinking option. What exists:

from mantis_agent import MantisAgentOptions
 
# Anthropic-style thinking block, passed through to providers that take one.
options = MantisAgentOptions(
    model="claude-opus-5",
    backend="anthropic",
    thinking={"type": "enabled", "budget_tokens": 4096},
)
 
# Cap the reasoning budget without naming a provider shape.
capped = MantisAgentOptions(model="deepseek-r1:1.5b", max_thinking_tokens=2048)
 
# Or steer effort: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "none"
effortful = MantisAgentOptions(model="gpt-5.6", effort="high")
 
# reasoning_mode picks the provider's reasoning family where it has one:
# "standard" or "pro".
pro = MantisAgentOptions(model="gpt-5.6", reasoning_mode="pro")

These are control keys: they never reach a provider's wire verbatim. Each adapter translates the ones it supports (Anthropic's thinking block, OpenAI's reasoning_effort, Ollama's think) and drops the rest, so setting one can't 400 a provider that has never heard of it.

Inline <think> blocks a model emits anyway are parsed out of the text stream regardless — see the capability check below.

Capability check

from mantis_agent import lookup_model
 
cap = lookup_model("deepseek-r1:1.5b")
print(cap.emits_thinking_blocks)    # True — reasoning arrives as its own blocks
print(cap.emits_inline_thinking)    # True — and/or inline in the text stream
print(cap.inline_thinking_tags)     # the tag pairs stripped from that stream

(supports_thinking and thinking_format never existed on ModelCapability; these three are the real fields.)

When you want to use thinking

Thinking blocks are useful for:

  • Debugging. When a tool call goes wrong, the thinking trail shows why.
  • Self-consistency. Re-running with a different seed and comparing thinking can surface confidence.
  • UX. Render thinking in a collapsed disclosure so users can audit reasoning without it dominating the chat.

They are not useful as final output. Always render the regular text blocks as the user-facing answer.