mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-17 04:26:20 +00:00
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:
co-authored by
BillionClaw
Aarish Alam
greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
parent
00dd984415
commit
78139472a1
@@ -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>"
|
||||
|
||||
Reference in New Issue
Block a user