diff --git a/litellm/llms/moonshot/chat/transformation.py b/litellm/llms/moonshot/chat/transformation.py
index 24f852c28b..c97bd6c4e1 100644
--- a/litellm/llms/moonshot/chat/transformation.py
+++ b/litellm/llms/moonshot/chat/transformation.py
@@ -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 {}
diff --git a/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py b/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py
index c557fb395f..f7e07ce8d9 100644
--- a/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py
+++ b/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py
@@ -550,4 +550,72 @@ class TestMoonshotConfig:
# reasoning_content must not have been injected
for msg in result["messages"]:
- assert "reasoning_content" not in msg
\ No newline at end of file
+ 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="User wants weather",
+ 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") == "User wants weather"
+
+ 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": "Planning to call weather tool",
+ "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") == "Planning to call weather tool"