mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-11 05:39:39 +00:00
fix: sanitize empty user message content in AnthropicCompletion
Fixes #4427. Anthropic API rejects messages with empty content for non-assistant roles. This adds sanitization in _format_messages_for_anthropic to replace empty or whitespace-only content in user messages with a single space placeholder. Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -615,6 +615,8 @@ class AnthropicCompletion(BaseLLM):
|
||||
formatted_messages.append({"role": role_str, "content": content})
|
||||
else:
|
||||
content_str = content if content is not None else ""
|
||||
if not content_str or not content_str.strip():
|
||||
content_str = " "
|
||||
formatted_messages.append(
|
||||
LLMMessage(role=role_str, content=content_str)
|
||||
)
|
||||
|
||||
@@ -990,3 +990,104 @@ def test_anthropic_agent_kickoff_structured_output_with_tools():
|
||||
assert result.pydantic.result == 42, f"Expected result 42 but got {result.pydantic.result}"
|
||||
assert result.pydantic.operation, "Operation should not be empty"
|
||||
assert result.pydantic.explanation, "Explanation should not be empty"
|
||||
|
||||
|
||||
def test_anthropic_empty_user_message_content_is_sanitized():
|
||||
"""Test that empty string user message content is replaced with a space placeholder."""
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [{"role": "user", "content": ""}]
|
||||
formatted, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
assert len(formatted) == 1
|
||||
assert formatted[0]["role"] == "user"
|
||||
assert formatted[0]["content"] == " "
|
||||
|
||||
|
||||
def test_anthropic_whitespace_only_user_message_is_sanitized():
|
||||
"""Test that whitespace-only user message content is replaced with a space placeholder."""
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [{"role": "user", "content": " "}]
|
||||
formatted, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
assert len(formatted) == 1
|
||||
assert formatted[0]["role"] == "user"
|
||||
assert formatted[0]["content"] == " "
|
||||
|
||||
|
||||
def test_anthropic_none_user_message_content_is_sanitized():
|
||||
"""Test that None user message content is replaced with a space placeholder."""
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [{"role": "user", "content": None}]
|
||||
formatted, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
assert len(formatted) == 1
|
||||
assert formatted[0]["role"] == "user"
|
||||
assert formatted[0]["content"] == " "
|
||||
|
||||
|
||||
def test_anthropic_non_empty_user_message_is_unchanged():
|
||||
"""Test that non-empty user message content is not modified."""
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
formatted, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
assert len(formatted) == 1
|
||||
assert formatted[0]["role"] == "user"
|
||||
assert formatted[0]["content"] == "Hello"
|
||||
|
||||
|
||||
def test_anthropic_empty_content_in_multi_message_conversation():
|
||||
"""Test that empty content is sanitized in a multi-message conversation."""
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi!"},
|
||||
{"role": "user", "content": ""},
|
||||
]
|
||||
formatted, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
assert formatted[0]["role"] == "user"
|
||||
assert formatted[0]["content"] == "Hello"
|
||||
assert formatted[1]["role"] == "assistant"
|
||||
assert formatted[1]["content"] == "Hi!"
|
||||
assert formatted[2]["role"] == "user"
|
||||
assert formatted[2]["content"] == " "
|
||||
|
||||
|
||||
def test_anthropic_empty_assistant_message_is_allowed():
|
||||
"""Test that empty assistant message content is NOT sanitized (Anthropic allows it)."""
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": ""},
|
||||
{"role": "user", "content": "Continue"},
|
||||
]
|
||||
formatted, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
assistant_msgs = [m for m in formatted if m["role"] == "assistant"]
|
||||
assert len(assistant_msgs) == 1
|
||||
assert assistant_msgs[0]["content"] == ""
|
||||
|
||||
|
||||
def test_anthropic_empty_content_does_not_crash_call():
|
||||
"""Test that calling with empty user message content does not raise before API call."""
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
with patch.object(llm, '_handle_completion', return_value="response") as mock_handle:
|
||||
with patch.object(llm, '_emit_call_started_event'):
|
||||
with patch.object(llm, '_invoke_before_llm_call_hooks', return_value=True):
|
||||
result = llm.call([{"role": "user", "content": ""}])
|
||||
|
||||
assert result == "response"
|
||||
call_args = mock_handle.call_args
|
||||
params = call_args[0][0]
|
||||
messages = params["messages"]
|
||||
assert all(
|
||||
m["content"] != "" for m in messages if m["role"] != "assistant"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user