From 622ca30b9a3559eba5ad3bc9d0d06ce5ee93f382 Mon Sep 17 00:00:00 2001 From: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:07:22 -0700 Subject: [PATCH 1/5] fix(utils): avoid pydantic serializer warnings in validate_and_fix_openai_messages Copy pydantic Messages (via model_dump) and dicts before rewriting tool_calls, so the later model_dump() no longer warns and the caller's messages are left untouched. Fixes #25880 Signed-off-by: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com> --- litellm/utils.py | 7 ++ .../test_validate_and_fix_openai_messages.py | 82 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 tests/unit/test_validate_and_fix_openai_messages.py diff --git a/litellm/utils.py b/litellm/utils.py index fc3acd3065d..bf1c20e6286 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8427,6 +8427,13 @@ def validate_and_fix_openai_messages(messages: list): """ new_messages: Final = [] for message in messages: + # Copy before mutating: assigning dicts into a pydantic Message's + # tool_calls triggers PydanticSerializationUnexpectedValue warnings on + # the later model_dump(), and it rewrites the caller's objects. + 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"): diff --git a/tests/unit/test_validate_and_fix_openai_messages.py b/tests/unit/test_validate_and_fix_openai_messages.py new file mode 100644 index 00000000000..cd2d41bd8f2 --- /dev/null +++ b/tests/unit/test_validate_and_fix_openai_messages.py @@ -0,0 +1,82 @@ +"""Tests for validate_and_fix_openai_messages (issue #25880).""" + +import warnings + +from litellm.types.utils import ChatCompletionMessageToolCall, Function, Message +from litellm.utils import validate_and_fix_openai_messages + + +def _make_tool_call_message() -> Message: + 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(): + msg = _make_tool_call_message() + + with warnings.catch_warnings(): + warnings.filterwarnings( + "error", + message=".*serialized value may not be as expected.*", + category=UserWarning, + ) + result = validate_and_fix_openai_messages( + [ + {"role": "user", "content": "hi"}, + msg, + {"role": "tool", "tool_call_id": "toolu_1", "content": "3"}, + ] + ) + + assert len(result) == 3 + assert all(isinstance(m, dict) for m in result) + assistant_msg = result[1] + assert assistant_msg["role"] == "assistant" + assert isinstance(assistant_msg["tool_calls"][0], dict) + assert assistant_msg["tool_calls"][0]["function"]["name"] == "add" + + +def test_original_message_not_mutated(): + msg = _make_tool_call_message() + original_tc = msg.tool_calls[0] + + validate_and_fix_openai_messages([{"role": "user", "content": "hi"}, msg]) + + assert isinstance(msg.tool_calls[0], ChatCompletionMessageToolCall) + assert msg.tool_calls[0] is original_tc + + +def test_dict_messages_with_tool_calls_still_work(): + 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_defaults_to_assistant_without_mutating_input(): + original = {"content": "some content"} + + result = validate_and_fix_openai_messages([original]) + + assert result[0]["role"] == "assistant" + assert original == {"content": "some content"} From 71eb804d76db0f973cb6ba35b1979f8045d34e4b Mon Sep 17 00:00:00 2001 From: Christian Sidak <61099993+Christian-Sidak@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:06:03 -0700 Subject: [PATCH 2/5] fix(utils): annotate messages as list[Any] to fix reportUnknownVariableType --- litellm/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/utils.py b/litellm/utils.py index bf1c20e6286..fdefb5102bc 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8419,7 +8419,7 @@ def convert_list_message_to_dict( return [dict(cleanup_none_field_in_message(message=_as_message_value(message))) for message in messages] -def validate_and_fix_openai_messages(messages: list): +def validate_and_fix_openai_messages(messages: list[Any]): """ Ensures all messages are valid OpenAI chat completion messages. From c91106b258ed6725b0359ca4d74578b511643bcd Mon Sep 17 00:00:00 2001 From: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:07:17 -0700 Subject: [PATCH 3/5] fix(utils): explicitly type new_messages list to clear reportUnknownArgumentType The Final=[] annotation let basedpyright infer the list type from appended values, which introduced one new reportUnknownArgumentType error past the project ceiling. Using an explicit list[AllMessageValues] annotation and casting the cleanup return value makes the type explicit and removes the ambiguity. --- litellm/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index fdefb5102bc..a70b58c2f52 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8425,7 +8425,7 @@ def validate_and_fix_openai_messages(messages: list[Any]): Handles missing role for assistant messages. """ - new_messages: Final = [] + new_messages: list[AllMessageValues] = [] for message in messages: # Copy before mutating: assigning dicts into a pydantic Message's # tool_calls triggers PydanticSerializationUnexpectedValue warnings on @@ -8446,7 +8446,7 @@ def validate_and_fix_openai_messages(messages: list[Any]): ), ) cleaned_message = cleanup_none_field_in_message(message=convert_msg_to_dict) - new_messages.append(cleaned_message) + new_messages.append(cast(AllMessageValues, cleaned_message)) return validate_chat_completion_user_messages(messages=new_messages) From feb2ccc76347d8f7e6fc6ba1314f6a2cfbd8507e Mon Sep 17 00:00:00 2001 From: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:05:56 -0700 Subject: [PATCH 4/5] fix(utils): add type discipline annotations for validate_and_fix_openai_messages Adds mutable-ok suppressions to the list parameter annotation and the new_messages accumulator, Final to the accumulator, and a cast-ok on the append call to satisfy the LIT001/LIT006/LIT010 discipline gate. --- litellm/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index a70b58c2f52..0ce46bce7c9 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8419,13 +8419,15 @@ def convert_list_message_to_dict( return [dict(cleanup_none_field_in_message(message=_as_message_value(message))) for message in messages] -def validate_and_fix_openai_messages(messages: list[Any]): +def validate_and_fix_openai_messages( + messages: list[Any], # mutable-ok: caller passes arbitrary message dicts +): """ Ensures all messages are valid OpenAI chat completion messages. Handles missing role for assistant messages. """ - new_messages: list[AllMessageValues] = [] + new_messages: Final[list[AllMessageValues]] = [] # mutable-ok: list is populated by appending, name never rebound for message in messages: # Copy before mutating: assigning dicts into a pydantic Message's # tool_calls triggers PydanticSerializationUnexpectedValue warnings on @@ -8446,7 +8448,7 @@ def validate_and_fix_openai_messages(messages: list[Any]): ), ) cleaned_message = cleanup_none_field_in_message(message=convert_msg_to_dict) - new_messages.append(cast(AllMessageValues, cleaned_message)) + new_messages.append(cast(AllMessageValues, cleaned_message)) # cast-ok: cleanup_none_field_in_message return satisfies AllMessageValues return validate_chat_completion_user_messages(messages=new_messages) From f58deb32865a1233b41a916a611fcdcf9e827d88 Mon Sep 17 00:00:00 2001 From: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:31:52 -0700 Subject: [PATCH 5/5] style: ruff format utils.py --- litellm/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/utils.py b/litellm/utils.py index 0ce46bce7c9..a80c79c61f6 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8448,7 +8448,9 @@ def validate_and_fix_openai_messages( ), ) cleaned_message = cleanup_none_field_in_message(message=convert_msg_to_dict) - new_messages.append(cast(AllMessageValues, cleaned_message)) # cast-ok: cleanup_none_field_in_message return satisfies AllMessageValues + new_messages.append( + cast(AllMessageValues, cleaned_message) + ) # cast-ok: cleanup_none_field_in_message return satisfies AllMessageValues return validate_chat_completion_user_messages(messages=new_messages)