Skip to content

Commit 8aa964b

Browse files
mike-luabaseclaude
andauthored
Prevent Anthropic API errors from empty message content (#1934)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent b7a2870 commit 8aa964b

File tree

2 files changed

+59
-5
lines changed

2 files changed

+59
-5
lines changed

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[B
276276
tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
277277
return tools
278278

279-
async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[BetaMessageParam]]:
279+
async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[BetaMessageParam]]: # noqa: C901
280280
"""Just maps a `pydantic_ai.Message` to a `anthropic.types.MessageParam`."""
281281
system_prompt_parts: list[str] = []
282282
anthropic_messages: list[BetaMessageParam] = []
@@ -315,7 +315,8 @@ async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[Be
315315
assistant_content_params: list[BetaTextBlockParam | BetaToolUseBlockParam] = []
316316
for response_part in m.parts:
317317
if isinstance(response_part, TextPart):
318-
assistant_content_params.append(BetaTextBlockParam(text=response_part.content, type='text'))
318+
if response_part.content: # Only add non-empty text
319+
assistant_content_params.append(BetaTextBlockParam(text=response_part.content, type='text'))
319320
else:
320321
tool_use_block_param = BetaToolUseBlockParam(
321322
id=_guard_tool_call_id(t=response_part),
@@ -324,7 +325,8 @@ async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[Be
324325
input=response_part.args_as_dict(),
325326
)
326327
assistant_content_params.append(tool_use_block_param)
327-
anthropic_messages.append(BetaMessageParam(role='assistant', content=assistant_content_params))
328+
if len(assistant_content_params) > 0:
329+
anthropic_messages.append(BetaMessageParam(role='assistant', content=assistant_content_params))
328330
else:
329331
assert_never(m)
330332
system_prompt = '\n\n'.join(system_prompt_parts)
@@ -337,11 +339,13 @@ async def _map_user_prompt(
337339
part: UserPromptPart,
338340
) -> AsyncGenerator[BetaContentBlockParam]:
339341
if isinstance(part.content, str):
340-
yield BetaTextBlockParam(text=part.content, type='text')
342+
if part.content: # Only yield non-empty text
343+
yield BetaTextBlockParam(text=part.content, type='text')
341344
else:
342345
for item in part.content:
343346
if isinstance(item, str):
344-
yield BetaTextBlockParam(text=item, type='text')
347+
if item: # Only yield non-empty text
348+
yield BetaTextBlockParam(text=item, type='text')
345349
elif isinstance(item, BinaryContent):
346350
if item.is_image:
347351
yield BetaImageBlockParam(

tests/models/test_anthropic.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,3 +1063,53 @@ async def test_anthropic_model_empty_message_on_history(allow_model_requests: No
10631063
10641064
What specifically would you like to know about potatoes?\
10651065
""")
1066+
1067+
1068+
async def test_anthropic_empty_content_filtering(env: TestEnv):
1069+
"""Test the empty content filtering logic directly."""
1070+
1071+
from pydantic_ai.messages import (
1072+
ModelMessage,
1073+
ModelRequest,
1074+
ModelResponse,
1075+
SystemPromptPart,
1076+
TextPart,
1077+
UserPromptPart,
1078+
)
1079+
1080+
# Initialize model for all tests
1081+
env.set('ANTHROPIC_API_KEY', 'test-key')
1082+
model = AnthropicModel('claude-3-5-sonnet-latest', provider='anthropic')
1083+
1084+
# Test _map_message with empty string in user prompt
1085+
messages_empty_string: list[ModelMessage] = [
1086+
ModelRequest(parts=[UserPromptPart(content='')], kind='request'),
1087+
]
1088+
_, anthropic_messages = await model._map_message(messages_empty_string) # type: ignore[attr-defined]
1089+
assert anthropic_messages == snapshot([]) # Empty content should be filtered out
1090+
1091+
# Test _map_message with list containing empty strings in user prompt
1092+
messages_mixed_content: list[ModelMessage] = [
1093+
ModelRequest(parts=[UserPromptPart(content=['', 'Hello', '', 'World'])], kind='request'),
1094+
]
1095+
_, anthropic_messages = await model._map_message(messages_mixed_content) # type: ignore[attr-defined]
1096+
assert anthropic_messages == snapshot(
1097+
[{'role': 'user', 'content': [{'text': 'Hello', 'type': 'text'}, {'text': 'World', 'type': 'text'}]}]
1098+
)
1099+
1100+
# Test _map_message with empty assistant response
1101+
messages: list[ModelMessage] = [
1102+
ModelRequest(parts=[SystemPromptPart(content='You are helpful')], kind='request'),
1103+
ModelResponse(parts=[TextPart(content='')], kind='response'), # Empty response
1104+
ModelRequest(parts=[UserPromptPart(content='Hello')], kind='request'),
1105+
]
1106+
_, anthropic_messages = await model._map_message(messages) # type: ignore[attr-defined]
1107+
# The empty assistant message should be filtered out
1108+
assert anthropic_messages == snapshot([{'role': 'user', 'content': [{'text': 'Hello', 'type': 'text'}]}])
1109+
1110+
# Test with only empty assistant parts
1111+
messages_resp: list[ModelMessage] = [
1112+
ModelResponse(parts=[TextPart(content=''), TextPart(content='')], kind='response'),
1113+
]
1114+
_, anthropic_messages = await model._map_message(messages_resp) # type: ignore[attr-defined]
1115+
assert len(anthropic_messages) == 0 # No messages should be added

0 commit comments

Comments
 (0)