fix(moonshot): preserve reasoning_content on Pydantic Message objects in multi-turn tool calls (#23828)

* fix(moonshot): preserve reasoning_content on Pydantic Message objects in multi-turn tool calls

The condition 'reasoning_content not in msg' doesn't work correctly for
Pydantic Message objects because they don't support the 'in' operator
like dicts do. This caused reasoning_content to be stripped from
assistant messages in multi-turn conversation history.

Changed the condition to use msg.get('reasoning_content') instead,
which works correctly for both dicts and Pydantic models.

Fixes #23765

* added newline eof

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Simplify assertions in test_moonshot_chat_transformation

Removed redundant assertions for non-assistant messages.

---------

Co-authored-by: BillionClaw <267901332+BillionClaw@users.noreply.github.com>
Co-authored-by: Aarish Alam <arishalam121@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
BillionToken
2026-03-21 00:09:17 +05:30
committed by GitHub
co-authored by BillionClaw Aarish Alam greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
parent 00dd984415
commit 78139472a1
2 changed files with 74 additions and 4 deletions
+5 -3
View File
@@ -155,9 +155,11 @@ class MoonshotChatConfig(OpenAIGPTConfig):
message that contains tool_calls (multi-turn tool-calling flows).
For each such message that is missing the field:
1. Promote provider_specific_fields["reasoning_content"] if present and non-empty
1. Check if reasoning_content exists at the top level (for Pydantic models
that have the attribute but don't support 'in' operator)
2. Promote provider_specific_fields["reasoning_content"] if present and non-empty
(this is where LiteLLM stores it from a previous response)
2. Otherwise inject a single space — the minimum value the API accepts
3. Otherwise inject a single space — the minimum value the API accepts
Messages that already carry the field, or are not assistant/tool-call messages,
are appended as-is (no copy made).
"""
@@ -166,7 +168,7 @@ class MoonshotChatConfig(OpenAIGPTConfig):
if (
msg.get("role") == "assistant"
and msg.get("tool_calls")
and "reasoning_content" not in msg
and not msg.get("reasoning_content") # Check using .get() which works for both dicts and Pydantic models
):
patched = dict(cast(dict, msg))
provider_fields = patched.get("provider_specific_fields") or {}
@@ -550,4 +550,72 @@ class TestMoonshotConfig:
# reasoning_content must not have been injected
for msg in result["messages"]:
assert "reasoning_content" not in msg
assert "reasoning_content" not in msg
def test_reasoning_content_preserved_on_pydantic_message_object(self):
"""reasoning_content on Pydantic Message objects is preserved (not overwritten with placeholder).
Regression test for: https://github.com/BerriAI/litellm/issues/23765
The issue was that 'reasoning_content' in msg doesn't work for Pydantic models
because they don't support the 'in' operator the same way as dicts.
"""
from litellm.types.utils import Message
config = MoonshotChatConfig()
# Create a Pydantic Message object with reasoning_content (as would come from API response)
message_with_reasoning = Message(
role="assistant",
content=None,
reasoning_content="<thinking>User wants weather</thinking>",
tool_calls=[
{"id": "call_1", "type": "function", "function": {"name": "fn", "arguments": "{}"}}
],
)
messages = [message_with_reasoning]
result = config.fill_reasoning_content(messages)
# reasoning_content should be preserved, not replaced with placeholder
assert result[0].get("reasoning_content") == "<thinking>User wants weather</thinking>"
def test_reasoning_content_preserved_in_multi_turn_flow(self):
"""reasoning_content is preserved through multi-turn conversation flow.
This tests the complete flow: API response -> Message object -> dict -> fill_reasoning_content
"""
from litellm.types.utils import Message
from litellm.utils import convert_to_dict
config = MoonshotChatConfig()
# Simulate API response with reasoning_content
api_response = {
"role": "assistant",
"content": None,
"reasoning_content": "<thinking>Planning to call weather tool</thinking>",
"tool_calls": [
{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": '{}'}}
],
}
# Convert to Message object (as LiteLLM does)
message_obj = Message(**api_response)
# Convert back to dict (when building next request)
message_dict = convert_to_dict(message_obj)
# Build multi-turn conversation
messages = [
{"role": "user", "content": "What's the weather?"},
message_dict,
{"role": "tool", "tool_call_id": "call_1", "content": '{"temp": 72}'},
{"role": "user", "content": "Thanks!"},
]
# Apply fill_reasoning_content
result = config.fill_reasoning_content(messages)
# reasoning_content should be preserved in the assistant message
assert result[1].get("reasoning_content") == "<thinking>Planning to call weather tool</thinking>"