Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/google/adk/models/anthropic_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,16 @@ def _is_pdf_part(part: types.Part) -> bool:
)


def _is_empty_text_part(part: types.Part) -> bool:
"""Returns True for parts that carry nothing but an empty text string.

ADK itself can produce such parts (e.g. code execution with no output writes
`Part(text='')` into the content), and Anthropic rejects empty text blocks.
"""
payload = part.model_dump(exclude_none=True)
return payload.get("text") == "" and set(payload) <= {"text", "thought"}


def _normalize_image_media_type(mime_type: str) -> _ImageMediaType:
normalized = mime_type.split(";", 1)[0].strip().lower()
if normalized not in _ANTHROPIC_IMAGE_MEDIA_TYPES:
Expand Down Expand Up @@ -575,6 +585,12 @@ def _content_to_message_param(
logger.warning("PDF data is not supported in Claude for assistant turns.")
continue

# Anthropic rejects empty text blocks; skip them rather than failing the
# whole request with NotImplementedError.
if _is_empty_text_part(part):
logger.debug("Skipping empty text part for Claude request.")
continue

message_block.append(_part_to_message_block(part, sanitizer))

return {
Expand Down
32 changes: 32 additions & 0 deletions tests/unittests/models/test_anthropic_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,38 @@ def test_content_to_message_param(
# --- Tests for Bug #2: json.dumps for dict/list function results ---


def test_content_to_message_param_skips_empty_text_part():
"""An empty text part must be skipped instead of raising NotImplementedError.

ADK can emit `Part(text='')` itself, e.g. when code execution produces no
output, and Anthropic rejects empty text blocks.
"""
content = types.Content(
role="user",
parts=[
types.Part(text="run it"),
types.Part(text=""),
],
)

result = content_to_message_param(content)

assert result["role"] == "user"
assert result["content"] == [{"type": "text", "text": "run it"}]


def test_content_to_message_param_keeps_non_text_payload_with_empty_text():
"""A part that has other payload alongside empty text is not dropped."""
part = types.Part(text="")
part.function_call = types.FunctionCall(id="call_1", name="tool", args={})
content = types.Content(role="model", parts=[part])

result = content_to_message_param(content)

assert len(result["content"]) == 1
assert result["content"][0]["type"] == "tool_use"


def test_part_to_message_block_dict_result_serialized_as_json():
"""Dict results should be serialized with json.dumps, not str()."""
response_part = types.Part.from_function_response(
Expand Down