This commit is contained in:
Kalai 2026-08-26 21:05:19 -04:00 committed by GitHub
commit f32a0cc924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 77 additions and 1 deletions

View file

@ -7797,9 +7797,17 @@ def cleanup_none_field_in_message(message: AllMessageValues):
Cleans up the message by removing the none field.
remove None fields in the message - e.g. {"function": None} - some providers raise validation errors
`content` is exempt on an assistant tool-call turn. The OpenAI spec
prescribes `content: null` for an assistant message that only calls tools,
and providers that require the key to be present reject the request when it
is dropped entirely.
"""
new_message: Final = message.copy()
return {k: v for k, v in new_message.items() if v is not None}
keeps_null_content: Final = message.get("role") == "assistant" and bool(
message.get("tool_calls") or message.get("function_call")
)
return {k: v for k, v in new_message.items() if v is not None or (k == "content" and keeps_null_content)}
def validate_chat_completion_user_messages(messages: list[AllMessageValues]):

View file

@ -0,0 +1,68 @@
"""`content: null` is the OpenAI-prescribed shape for an assistant tool-call turn."""
from litellm.utils import cleanup_none_field_in_message, validate_and_fix_openai_messages
TOOL_CALLS = [{"id": "call_1", "type": "function", "function": {"name": "f", "arguments": "{}"}}]
class TestCleanupNoneFieldInMessage:
def test_keeps_null_content_on_a_tool_call_turn(self):
cleaned = cleanup_none_field_in_message(
message={"role": "assistant", "content": None, "tool_calls": TOOL_CALLS}
)
assert "content" in cleaned
assert cleaned["content"] is None
def test_keeps_null_content_on_a_legacy_function_call_turn(self):
cleaned = cleanup_none_field_in_message(
message={
"role": "assistant",
"content": None,
"function_call": {"name": "f", "arguments": "{}"},
}
)
assert "content" in cleaned
def test_still_strips_other_none_fields_on_that_turn(self):
# The original purpose of the helper: providers reject e.g. {"function": None}.
cleaned = cleanup_none_field_in_message(
message={
"role": "assistant",
"content": None,
"tool_calls": TOOL_CALLS,
"function": None,
"name": None,
}
)
assert "function" not in cleaned
assert "name" not in cleaned
assert "content" in cleaned
def test_still_strips_none_content_without_tool_calls(self):
cleaned = cleanup_none_field_in_message(message={"role": "assistant", "content": None})
assert "content" not in cleaned
def test_user_message_is_unaffected(self):
cleaned = cleanup_none_field_in_message(message={"role": "user", "content": None, "name": None})
assert cleaned == {"role": "user"}
class TestValidateAndFixOpenAIMessages:
def test_tool_call_turn_survives_the_full_validation_pass(self):
"""End to end through the caller that strips the field."""
messages = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": None, "tool_calls": TOOL_CALLS},
{"role": "tool", "tool_call_id": "call_1", "content": "ok"},
]
fixed = validate_and_fix_openai_messages(messages=messages)
assert "content" in fixed[1]
assert fixed[1]["content"] is None
assert fixed[1]["tool_calls"] == TOOL_CALLS