fix(utils): avoid pydantic serializer warnings in validate_and_fix_openai_messages

Convert pydantic Message objects to dicts before mutating tool_calls,
preventing PydanticSerializationUnexpectedValue warnings when
model_dump() is called on messages whose tool_calls field has been
replaced with plain dicts by jsonify_tools.

Also copies dict messages before mutation to avoid side effects on
the caller original data.

Fixes #25880
This commit is contained in:
Christian Sidak 2026-04-16 21:42:18 -07:00
parent 850fe595ac
commit 64d8764492
2 changed files with 121 additions and 0 deletions

View file

@ -7853,6 +7853,13 @@ def validate_and_fix_openai_messages(messages: List):
"""
new_messages = []
for message in messages:
# Convert pydantic models to dicts first to avoid mutating model
# fields in-place, which triggers PydanticSerializationUnexpectedValue
# warnings when model_dump() is called later.
if isinstance(message, BaseModel):
message = message.model_dump(exclude_none=True)
elif isinstance(message, dict):
message = message.copy()
if not message.get("role"):
message["role"] = "assistant"
if message.get("tool_calls"):

View file

@ -0,0 +1,114 @@
"""
Tests for validate_and_fix_openai_messages in litellm/utils.py
Covers the fix for https://github.com/BerriAI/litellm/issues/25880:
Pydantic Message objects with tool_calls should not trigger
PydanticSerializationUnexpectedValue warnings.
"""
import warnings
import pytest
from litellm.types.utils import (
ChatCompletionMessageToolCall,
Function,
Message,
)
from litellm.utils import validate_and_fix_openai_messages
def _make_tool_call_message():
"""Helper: build an assistant Message with a single tool_call."""
tc = ChatCompletionMessageToolCall(
id="toolu_1",
type="function",
function=Function(name="add", arguments='{"a":1,"b":2}'),
)
return Message(content="", role="assistant", tool_calls=[tc])
def test_no_pydantic_serialization_warning_with_tool_calls():
"""Regression: pydantic Messages with tool_calls must not emit warnings."""
msg = _make_tool_call_message()
with warnings.catch_warnings():
warnings.filterwarnings("error", message="Pydantic serializer warnings")
# Should not raise
result = validate_and_fix_openai_messages(
[
{"role": "user", "content": "hi"},
msg,
{"role": "tool", "tool_call_id": "toolu_1", "content": "3"},
]
)
# Basic sanity: all messages come back as dicts
assert isinstance(result, list)
assert len(result) == 3
for m in result:
assert isinstance(m, dict)
# The assistant message should have tool_calls serialized as dicts
assistant_msg = result[1]
assert assistant_msg["role"] == "assistant"
assert isinstance(assistant_msg["tool_calls"], list)
assert isinstance(assistant_msg["tool_calls"][0], dict)
assert assistant_msg["tool_calls"][0]["function"]["name"] == "add"
def test_original_message_not_mutated():
"""The original pydantic Message should not be mutated by validation."""
msg = _make_tool_call_message()
original_tc = msg.tool_calls[0]
validate_and_fix_openai_messages(
[
{"role": "user", "content": "hi"},
msg,
{"role": "tool", "tool_call_id": "toolu_1", "content": "3"},
]
)
# tool_calls on the original message should still be the pydantic model
assert isinstance(msg.tool_calls[0], ChatCompletionMessageToolCall)
assert msg.tool_calls[0] is original_tc
def test_dict_messages_still_work():
"""Plain dict messages should still be processed correctly."""
messages = [
{"role": "user", "content": "hello"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "greet", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": "hi"},
]
result = validate_and_fix_openai_messages(messages)
assert len(result) == 3
assert result[1]["tool_calls"][0]["function"]["name"] == "greet"
def test_missing_role_gets_default():
"""Messages without a role should default to 'assistant'."""
result = validate_and_fix_openai_messages(
[{"content": "some content"}]
)
assert result[0]["role"] == "assistant"
def test_dict_messages_not_mutated_in_place():
"""Original dict messages should not be mutated."""
original = {"role": "user", "content": "test"}
validate_and_fix_openai_messages([original])
# Should still be the same dict content
assert original == {"role": "user", "content": "test"}