Skip to content

Logic behind mapping <think> tags to reasoning_content #9433

Closed

Description

@zikajk

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?

Activity

  1. github-actions commented on Jan 19, 2026

    @github-actions
    Contributor

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

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

  2. rekram1-node commented on Jan 19, 2026

    @rekram1-node
    Contributor

    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

  3. zikajk commented on Jan 20, 2026

    @zikajk
    Author

    @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

  4. rekram1-node commented on Jan 20, 2026

    @rekram1-node
    Contributor

    I see what u mean hmm

  5. rekram1-node commented on Jan 20, 2026

    @rekram1-node
    Contributor

    Yeah ig we should remove that wrapping or something hm

  6. zikajk commented on Jan 20, 2026

    @zikajk
    Author

    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.

  7. rekram1-node commented on Jan 20, 2026

    @rekram1-node
    Contributor

    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

  8. daryltucker commented on Jan 26, 2026

    @daryltucker

    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?

  9. rekram1-node commented on Jan 31, 2026

    @rekram1-node
    Contributor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions