From 78139472a166b60d51b2b4298dc829ccde393603 Mon Sep 17 00:00:00 2001 From: BillionToken Date: Sat, 21 Mar 2026 02:39:17 +0800 Subject: [PATCH] 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 Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- litellm/llms/moonshot/chat/transformation.py | 8 ++- .../test_moonshot_chat_transformation.py | 70 ++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) 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"