mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-02 13:48:09 +00:00
fix: strip trailing whitespace from final assistant message for Anthropic
Fixes #4413. Anthropic API rejects requests where the final assistant message ends with trailing whitespace (400 invalid_request_error). This strips trailing whitespace from the last assistant message content in _format_messages_for_anthropic, handling both string and list content. Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -631,6 +631,22 @@ class AnthropicCompletion(BaseLLM):
|
||||
# If first message is not from user, insert a user message at the beginning
|
||||
formatted_messages.insert(0, {"role": "user", "content": "Hello"})
|
||||
|
||||
# Strip trailing whitespace from the final assistant message content.
|
||||
# Anthropic rejects requests where the final assistant message ends
|
||||
# with trailing whitespace (400 invalid_request_error).
|
||||
if formatted_messages and formatted_messages[-1].get("role") == "assistant":
|
||||
last_content = formatted_messages[-1].get("content")
|
||||
if isinstance(last_content, str):
|
||||
formatted_messages[-1]["content"] = last_content.rstrip()
|
||||
elif isinstance(last_content, list):
|
||||
for i in range(len(last_content) - 1, -1, -1):
|
||||
block = last_content[i]
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text_val = block.get("text", "")
|
||||
if isinstance(text_val, str):
|
||||
block["text"] = text_val.rstrip()
|
||||
break
|
||||
|
||||
return formatted_messages, system_message
|
||||
|
||||
def _handle_completion(
|
||||
|
||||
@@ -990,3 +990,117 @@ 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_strips_trailing_whitespace_from_final_assistant_message():
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello. Say world"},
|
||||
{"role": "assistant", "content": "Say: "},
|
||||
]
|
||||
|
||||
formatted_messages, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
last_msg = formatted_messages[-1]
|
||||
assert last_msg["role"] == "assistant"
|
||||
assert last_msg["content"] == "Say:"
|
||||
assert not last_msg["content"].endswith(" ")
|
||||
|
||||
|
||||
def test_anthropic_strips_trailing_whitespace_tabs_and_newlines():
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Response \t\n "},
|
||||
]
|
||||
|
||||
formatted_messages, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
last_msg = formatted_messages[-1]
|
||||
assert last_msg["role"] == "assistant"
|
||||
assert last_msg["content"] == "Response"
|
||||
|
||||
|
||||
def test_anthropic_does_not_strip_whitespace_from_non_final_assistant_message():
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi "},
|
||||
{"role": "user", "content": "How are you?"},
|
||||
]
|
||||
|
||||
formatted_messages, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
assert formatted_messages[-1]["role"] == "user"
|
||||
assistant_msg = formatted_messages[1]
|
||||
assert assistant_msg["role"] == "assistant"
|
||||
assert assistant_msg["content"] == "Hi "
|
||||
|
||||
|
||||
def test_anthropic_strips_trailing_whitespace_from_list_content_text_block():
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": [
|
||||
{"type": "text", "text": "Some response "},
|
||||
]},
|
||||
]
|
||||
|
||||
formatted_messages, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
last_msg = formatted_messages[-1]
|
||||
assert last_msg["role"] == "assistant"
|
||||
last_block = last_msg["content"][-1]
|
||||
assert last_block["text"] == "Some response"
|
||||
|
||||
|
||||
def test_anthropic_strips_trailing_whitespace_only_from_last_text_block():
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": [
|
||||
{"type": "text", "text": "First block "},
|
||||
{"type": "text", "text": "Last block "},
|
||||
]},
|
||||
]
|
||||
|
||||
formatted_messages, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
last_msg = formatted_messages[-1]
|
||||
blocks = last_msg["content"]
|
||||
assert blocks[0]["text"] == "First block "
|
||||
assert blocks[1]["text"] == "Last block"
|
||||
|
||||
|
||||
def test_anthropic_no_strip_when_final_message_is_user():
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello "},
|
||||
]
|
||||
|
||||
formatted_messages, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
last_msg = formatted_messages[-1]
|
||||
assert last_msg["role"] == "user"
|
||||
assert last_msg["content"] == "Hello "
|
||||
|
||||
|
||||
def test_anthropic_empty_assistant_content_not_affected():
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": ""},
|
||||
]
|
||||
|
||||
formatted_messages, _ = llm._format_messages_for_anthropic(messages)
|
||||
|
||||
last_msg = formatted_messages[-1]
|
||||
assert last_msg["role"] == "assistant"
|
||||
assert last_msg["content"] == ""
|
||||
|
||||
Reference in New Issue
Block a user