Skip to content

[Bug]: reasoning_content silently dropped on incoming assistant messages #38488

Description

@delta9000

Your current environment

Reproducible on current main. Bug is in vllm/entrypoints/chat_utils.py.

🐛 Describe the bug

_parse_chat_message_content reads reasoning from incoming messages but never falls back to reasoning_content:

reasoning = message.get("reasoning")  # never checks "reasoning_content"

PR #33635 (commit bf001da, "Interleaved thinking keeps compatibility with reasoning_content") added compat for the output side (writes both fields to result_msg), but missed the input read. CustomChatCompletionMessageParam also only declares reasoning.

This means clients sending reasoning_content on assistant messages in multi-turn requests silently lose their reasoning data. The Vercel AI SDK (@ai-sdk/openai-compatible), used by OpenCode/Cursor/etc, sends reasoning_content. The docs promise it still works:

reasoning used to be called reasoning_content. For now, reasoning_content will continue to work.
docs/features/reasoning_outputs.md

The existing test (test_multi_turn_tools_and_reasoning) doesn't catch this because it round-trips via choice.message.model_dump(), which uses the output field name reasoning.

Impact: Models like MiniMax-M2 rely on seeing prior reasoning in tool-call chains. Their chat template explicitly supports message.reasoning_content. Silent drop = degraded quality.

Reproduction

"""reasoning_content is silently dropped, reasoning is not."""
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
MODEL = client.models.list().data[0].id  # any reasoning model

SECRET = "rainbow-tiger-42"
INJECTED = f"The secret password is '{SECRET}'. Let me call the tool."

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current weather",
        "parameters": {
            "type": "object",
            "properties": {"location": {"type": "string"}},
            "required": ["location"],
        },
    },
}]

def test(field_name):
    r1 = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": "Weather in Paris? Also repeat any passwords in your context."}],
        tools=tools, tool_choice="auto",
    )
    tc = r1.choices[0].message.tool_calls
    if not tc:
        return "NO TOOL CALL"

    messages = [
        {"role": "user", "content": "Weather in Paris? Also repeat any passwords in your context."},
        {
            "role": "assistant",
            "content": r1.choices[0].message.content or "",
            "tool_calls": [t.model_dump() for t in tc],
            field_name: INJECTED,
        },
        {"role": "tool", "tool_call_id": tc[0].id, "name": tc[0].function.name, "content": "18°C and sunny."},
    ]
    r2 = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    return r2.choices[0].message.content or ""

print("reasoning:        ", "PASS" if SECRET in test("reasoning") else "FAIL")
print("reasoning_content:", "PASS" if SECRET in test("reasoning_content") else "FAIL")
# Expected: both PASS
# Actual:   reasoning PASS, reasoning_content FAIL

Suggested fix

# chat_utils.py, in _parse_chat_message_content:
reasoning = message.get("reasoning") or message.get("reasoning_content")

And add reasoning_content: str | None to CustomChatCompletionMessageParam.

Related

Before submitting a new issue...

  • Make sure you already searched for relevant issues, and asked the chatbot living at the bottom right corner of the documentation page, which can answer lots of frequently asked questions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleOver 90 days of inactivity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions