Logic behind mapping <think> tags to reasoning_content #6816

Closed
opened 2026-02-16 18:05:21 -05:00 by yindo · 9 comments
Owner

Originally created by @zikajk on GitHub (Jan 19, 2026).

Originally assigned to: @rekram1-node on GitHub.

Question

I noticed that if a model sends <think> tags, you convert them into reasoning_content.
And if a model supports interleaved reasoning, you theoretically send the content of those <think> tags back as reasoning_content (otherwise they are filtered out).

I'm not sure if there is a model that supports reasoning_content while simultaneously sending <think> tags, but could you please tell me the logic behind this?

Originally created by @zikajk on GitHub (Jan 19, 2026). Originally assigned to: @rekram1-node on GitHub. ### Question I noticed that if a model sends` <think>` tags, you convert them into `reasoning_content`. And if a model supports interleaved reasoning, you theoretically send the content of those `<think>` tags back as `reasoning_content` (otherwise they are filtered out). I'm not sure if there is a model that supports `reasoning_content` while simultaneously sending `<think>` tags, but could you please tell me the logic behind this?
yindo closed this issue 2026-02-16 18:05:21 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Jan 19, 2026):

This issue might be a duplicate of existing issues. Please check:

  • #3555: MiniMax-M2 reasoning content not properly handled in OpenAI-compatible mode - discusses how models send <think> tags in delta.content instead of delta.reasoning_content
  • #7779: GLM 4.7 thinking process not properly formatted - discusses <think> tag formatting issues
  • #6337: Feature request for extracting reasoning XML tags
  • #6040: reasoning_content not sent when using @ai-sdk/openai-compatible with deepseek-reasoner - discusses reasoning_content not being sent in subsequent requests
  • #5577: DeepSeek V3.2 reasoning_content not cleared from message history - comprehensive discussion of reasoning_content handling

Feel free to ignore if none of these address your specific case.

@github-actions[bot] commented on GitHub (Jan 19, 2026): This issue might be a duplicate of existing issues. Please check: - #3555: MiniMax-M2 reasoning content not properly handled in OpenAI-compatible mode - discusses how models send `<think>` tags in `delta.content` instead of `delta.reasoning_content` - #7779: GLM 4.7 thinking process not properly formatted - discusses `<think>` tag formatting issues - #6337: Feature request for extracting reasoning XML tags - #6040: reasoning_content not sent when using @ai-sdk/openai-compatible with deepseek-reasoner - discusses `reasoning_content` not being sent in subsequent requests - #5577: DeepSeek V3.2 reasoning_content not cleared from message history - comprehensive discussion of reasoning_content handling Feel free to ignore if none of these address your specific case.
Author
Owner

@rekram1-node commented on GitHub (Jan 19, 2026):

And if a model supports interleaved reasoning

This should only happen if the model is configured to both:
A) be interleaved reasoning model
B) expect reasoning to be sent back as reasoning_content | reasoning_details

@rekram1-node commented on GitHub (Jan 19, 2026): > And if a model supports interleaved reasoning This should only happen if the model is configured to both: A) be interleaved reasoning model B) expect reasoning to be sent back as reasoning_content | reasoning_details
Author
Owner

@zikajk commented on GitHub (Jan 20, 2026):

@rekram1-node
It seems OpenCode always extracts <think>...</think> tags from model output into “reasoning” parts here.
Then, only when model.capabilities.interleaved.field === "reasoning_content", those reasoning parts are mapped into providerOptions.openaiCompatible.reasoning_content and removed from content.

This means a model that expects <think> tags to remain in history won’t receive them back. For example, MiniMax 2.1 docs state tags should stay in history.

Questions:

  • Do you know of any model/provider that both outputs <think>...</think> and expects preserved/interleaved reasoning via reasoning_content / reasoning_details?
  • If not, what’s the rationale for always stripping <think> tags by default — is it purely to save context/cost, or something else?
  • Should this behavior be driven by models.dev capabilities or be configurable?

Thank you

@zikajk commented on GitHub (Jan 20, 2026): @rekram1-node It seems OpenCode always extracts `<think>...</think>` tags from model output into “reasoning” parts [here](https://github.com/anomalyco/opencode/blob/86b2002deb35932d858137eb0e3439fb87e4cacb/packages/opencode/src/session/llm.ts#L251). Then, only when `model.capabilities.interleaved.field === "reasoning_content"`, those reasoning parts are mapped into `providerOptions.openaiCompatible.reasoning_content` and removed from content. This means a model that expects` <think>` tags to remain in history won’t receive them back. For example, MiniMax 2.1 [docs](https://huggingface.co/MiniMaxAI/MiniMax-M2#inference-parameters) state <think> tags should stay in history. Questions: - Do you know of any model/provider that both outputs `<think>...</think>` and expects preserved/interleaved reasoning via `reasoning_content / reasoning_details`? - If not, what’s the rationale for always stripping `<think>` tags by default — is it purely to save context/cost, or something else? - Should this behavior be driven by _models.dev_ capabilities or be configurable? Thank you
Author
Owner

@rekram1-node commented on GitHub (Jan 20, 2026):

I see what u mean hmm

@rekram1-node commented on GitHub (Jan 20, 2026): I see what u mean hmm
Author
Owner

@rekram1-node commented on GitHub (Jan 20, 2026):

Yeah ig we should remove that wrapping or something hm

@rekram1-node commented on GitHub (Jan 20, 2026): Yeah ig we should remove that wrapping or something hm
Author
Owner

@zikajk commented on GitHub (Jan 20, 2026):

And could you tell me if removing the <think>...</think> from history was intentional or a mistake? I've heard conflicting opinions on whether the coding agent should restore it.

@zikajk commented on GitHub (Jan 20, 2026): And could you tell me if removing the `<think>...</think>` from history was intentional or a mistake? I've heard conflicting opinions on whether the coding agent should restore it.
Author
Owner

@rekram1-node commented on GitHub (Jan 20, 2026):

There is some nuance but tldr:

  • we should prolly keep rendering the tags as actual reasoning blocks
  • we shouldn't transform the way they are sent back, if they are sent as part of the assistant text, then they should stay that way

I feel like this is best of both worlds the only reason we have that parser is for that

@rekram1-node commented on GitHub (Jan 20, 2026): There is some nuance but tldr: - we should prolly keep rendering the <think> tags as actual reasoning blocks - we shouldn't transform the way they are sent back, if they are sent as part of the assistant text, then they should stay that way I feel like this is best of both worlds the only reason we have that parser is for that
Author
Owner

@daryltucker commented on GitHub (Jan 26, 2026):

response.reasoning_text.delta is this why Thinking isn't shown in opencode now?

#4947

./build/bin/llama-server   -m /mnt/ollama/ggufs/Qwen3-30B-A3B-Thinking-2507-UD-Q8_K_XL.gguf   --port 8079   --host 0.0.0
.0   -c 65536   -np 1   -ctk q8_0 -ctv q8_0   -cb   --no-warmup   --jinja   --chat-template-file UnForced-Thinking-TEMPLATE-Responses.j2   --split-mode layer  
 -ngl 99 --reasoning-format deepseek --reasoning-budget -1
{# System Prompt Logic #}
{%- set system_message = "You are a helpful assistant. IMPORTANT: You MUST always begin your response with a <think> tag to start your reasoning process." -%}
{%- if messages[0]['role'] == 'system' -%}
    {%- set system_message = messages[0]['content'] -%}
{%- endif -%}

<|im_start|>system
{{ system_message }}<|im_end|>

{# Loop through remaining Messages #}
{%- for message in messages -%}
    {# Skip System Prompt if it was handled above #}
    {%- if not (loop.first and message['role'] == 'system') -%}
        <|im_start|>{{ message['role'] }}

        {# Allow Deep Reasoning #}
        {%- set reasoning = message['reasoning_content'] or message['thinking'] -%}
        {%- if reasoning -%}
            <think>
            {{ reasoning }}
            </think>
        {%- endif -%}

        {# Completion vs Response #}
        {%- if message['content'] is string -%}
            {{ message['content'] }}
        {%- elif message['content'] is iterable -%}
            {%- for item in message['content'] -%}
                {%- if item['type'] in ['text', 'input_text'] -%}
                    {{ item['text'] }}
                {%- endif -%}
            {%- endfor -%}
        {%- endif -%}

        <|im_end|>
    {%- endif -%}
{%- endfor -%}

{# Provide <think> Tag for models used to Native Thinking flow #}
{%- if add_generation_prompt -%}
<|im_start|>assistant
{%- endif -%}
{# System Prompt Logic #}
{%- set system_message = "You are a helpful assistant." -%}
{%- if messages[0]['role'] == 'system' -%}
    {%- set system_message = messages[0]['content'] -%}
{%- endif -%}

<|im_start|>system
{{ system_message }}<|im_end|>

{# Loop through remaining Messages #}
{%- for message in messages -%}
    {# Skip System Prompt if it was handled above #}
    {%- if not (loop.first and message['role'] == 'system') -%}
        <|im_start|>{{ message['role'] }}

        {# Allow Deep Reasoning #}
        {%- set reasoning = message['reasoning_content'] or message['thinking'] -%}
        {%- if reasoning -%}
            <think>
            {{ reasoning }}
            </think>
        {%- endif -%}

        {# Completion vs Response #}
        {%- if message['content'] is string -%}
            {{ message['content'] }}
        {%- elif message['content'] is iterable -%}
            {%- for item in message['content'] -%}
                {%- if item['type'] in ['text', 'input_text'] -%}
                    {{ item['text'] }}
                {%- endif -%}
            {%- endfor -%}
        {%- endif -%}

        <|im_end|>
    {%- endif -%}
{%- endfor -%}

{# Provide <think> Tag for models used to Native Thinking flow #}
{%- if add_generation_prompt -%}
<|im_start|>assistant
<think>
{%- endif -%}

I can't get it to work anymore, specifically v1/responses. Any hints or confirmation at least it's just transition pains?

@daryltucker commented on GitHub (Jan 26, 2026): `response.reasoning_text.delta` is this why Thinking isn't shown in `opencode` now? #4947 ``` ./build/bin/llama-server -m /mnt/ollama/ggufs/Qwen3-30B-A3B-Thinking-2507-UD-Q8_K_XL.gguf --port 8079 --host 0.0.0 .0 -c 65536 -np 1 -ctk q8_0 -ctv q8_0 -cb --no-warmup --jinja --chat-template-file UnForced-Thinking-TEMPLATE-Responses.j2 --split-mode layer -ngl 99 --reasoning-format deepseek --reasoning-budget -1 ``` ``` {# System Prompt Logic #} {%- set system_message = "You are a helpful assistant. IMPORTANT: You MUST always begin your response with a <think> tag to start your reasoning process." -%} {%- if messages[0]['role'] == 'system' -%} {%- set system_message = messages[0]['content'] -%} {%- endif -%} <|im_start|>system {{ system_message }}<|im_end|> {# Loop through remaining Messages #} {%- for message in messages -%} {# Skip System Prompt if it was handled above #} {%- if not (loop.first and message['role'] == 'system') -%} <|im_start|>{{ message['role'] }} {# Allow Deep Reasoning #} {%- set reasoning = message['reasoning_content'] or message['thinking'] -%} {%- if reasoning -%} <think> {{ reasoning }} </think> {%- endif -%} {# Completion vs Response #} {%- if message['content'] is string -%} {{ message['content'] }} {%- elif message['content'] is iterable -%} {%- for item in message['content'] -%} {%- if item['type'] in ['text', 'input_text'] -%} {{ item['text'] }} {%- endif -%} {%- endfor -%} {%- endif -%} <|im_end|> {%- endif -%} {%- endfor -%} {# Provide <think> Tag for models used to Native Thinking flow #} {%- if add_generation_prompt -%} <|im_start|>assistant {%- endif -%} ``` ``` {# System Prompt Logic #} {%- set system_message = "You are a helpful assistant." -%} {%- if messages[0]['role'] == 'system' -%} {%- set system_message = messages[0]['content'] -%} {%- endif -%} <|im_start|>system {{ system_message }}<|im_end|> {# Loop through remaining Messages #} {%- for message in messages -%} {# Skip System Prompt if it was handled above #} {%- if not (loop.first and message['role'] == 'system') -%} <|im_start|>{{ message['role'] }} {# Allow Deep Reasoning #} {%- set reasoning = message['reasoning_content'] or message['thinking'] -%} {%- if reasoning -%} <think> {{ reasoning }} </think> {%- endif -%} {# Completion vs Response #} {%- if message['content'] is string -%} {{ message['content'] }} {%- elif message['content'] is iterable -%} {%- for item in message['content'] -%} {%- if item['type'] in ['text', 'input_text'] -%} {{ item['text'] }} {%- endif -%} {%- endfor -%} {%- endif -%} <|im_end|> {%- endif -%} {%- endfor -%} {# Provide <think> Tag for models used to Native Thinking flow #} {%- if add_generation_prompt -%} <|im_start|>assistant <think> {%- endif -%} ``` I can't get it to work anymore, specifically `v1/responses`. Any hints or confirmation at least it's just transition pains?
Author
Owner

@rekram1-node commented on GitHub (Jan 30, 2026):

I removed the thing, need to make it prettier later

@daryltucker im not sure im following urs? Im closing cause I addressed issue OP raised

@rekram1-node commented on GitHub (Jan 30, 2026): I removed the <think> thing, need to make it prettier later @daryltucker im not sure im following urs? Im closing cause I addressed issue OP raised
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#6816