diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index a23a05463e7..266d60bdce8 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -643,6 +643,17 @@ def _format_messages( _lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls), ) + if role == "assistant" and _i == len(merged_messages) - 1: + if isinstance(content, str): + content = content.rstrip() + elif ( + isinstance(content, list) + and content + and isinstance(content[-1], dict) + and content[-1].get("type") == "text" + ): + content[-1]["text"] = content[-1]["text"].rstrip() + if not content and role == "assistant" and _i < len(merged_messages) - 1: # anthropic.BadRequestError: Error code: 400: all messages must have # non-empty content except for the optional final assistant message diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index 42bb97e2944..e669b0655c0 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -2319,3 +2319,23 @@ def test_extras_with_multiple_fields() -> None: assert tool_def.get("defer_loading") is True assert tool_def.get("cache_control") == {"type": "ephemeral"} assert "input_examples" in tool_def + + +def test__format_messages_trailing_whitespace() -> None: + """Test that trailing whitespace is trimmed from the final assistant message.""" + human = HumanMessage("foo") # type: ignore[misc] + + # Test string content + ai_string = AIMessage("thought ") # type: ignore[misc] + _, anthropic_messages = _format_messages([human, ai_string]) + assert anthropic_messages[-1]["content"] == "thought" + + # Test list content + ai_list = AIMessage([{"type": "text", "text": "thought "}]) # type: ignore[misc] + _, anthropic_messages = _format_messages([human, ai_list]) + assert anthropic_messages[-1]["content"][0]["text"] == "thought" # type: ignore[index] + + # Test that intermediate messages are NOT trimmed + ai_intermediate = AIMessage("thought ") # type: ignore[misc] + _, anthropic_messages = _format_messages([human, ai_intermediate, human]) + assert anthropic_messages[1]["content"] == "thought "