From 22f996e3f2590a3a7cedd909b5a04276c6ec6f2f Mon Sep 17 00:00:00 2001 From: lsmlhi_25 Date: Sun, 13 Sep 2026 10:15:00 +0000 Subject: [PATCH] fix(anthropic): preserve sibling keys beside function_response result When a tool response dict has result plus other keys, serialize the full dict for Claude tool_result content (parity with Gemini). Keep single-key {result: x} unwrapping. Fixes #7073. Rebased onto latest main. --- src/google/adk/models/anthropic_llm.py | 18 ++++++++++-------- tests/unittests/models/test_anthropic_llm.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/google/adk/models/anthropic_llm.py b/src/google/adk/models/anthropic_llm.py index 5cf8f5dcbd..9392bdc473 100644 --- a/src/google/adk/models/anthropic_llm.py +++ b/src/google/adk/models/anthropic_llm.py @@ -514,15 +514,17 @@ def _part_to_message_block( # SDK ref: anthropic.types.tool_result_block_param # https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/types/tool_result_block_param.py # Exactly {"result": value} is ADK's wrapper for a non-dict tool return. - elif ( - response_data.keys() == {"result"} - and response_data["result"] is not None - ): - result = response_data["result"] - if isinstance(result, (dict, list)): - content = json.dumps(result) + # If sibling keys exist beside result, serialize the whole dict so Claude + # matches Gemini (issue #7073). + elif "result" in response_data and response_data["result"] is not None: + if response_data.keys() == {"result"}: + result = response_data["result"] + if isinstance(result, (dict, list)): + content = json.dumps(result) + else: + content = str(result) else: - content = str(result) + content = json.dumps(response_data, default=str) elif response_data: # Fallback: serialize the entire response dict as JSON so that tools # returning arbitrary key structures (e.g. load_skill returning diff --git a/tests/unittests/models/test_anthropic_llm.py b/tests/unittests/models/test_anthropic_llm.py index 6b309e200e..ba9015507a 100644 --- a/tests/unittests/models/test_anthropic_llm.py +++ b/tests/unittests/models/test_anthropic_llm.py @@ -928,6 +928,25 @@ def test_part_to_message_block_with_traditional_result(): assert "This is the result from the tool" in result["content"] +def test_part_to_message_block_preserves_sibling_keys_with_result(): + """Issue #7073: sibling keys next to result must reach Claude.""" + response_part = types.Part.from_function_response( + name="run_python_analysis", + response={ + "result": {"matched": 33, "total": 49}, + "files": [{"filename": "matching.xlsx", "bytes": 8684}], + "stdout": "Saved to /tmp/outputs/matching.xlsx\n", + }, + ) + response_part.function_response.id = "toolu_01" + + result = part_to_message_block(response_part) + parsed = json.loads(result["content"]) + assert parsed["result"] == {"matched": 33, "total": 49} + assert parsed["files"] == [{"filename": "matching.xlsx", "bytes": 8684}] + assert parsed["stdout"] == "Saved to /tmp/outputs/matching.xlsx\n" + + def test_part_to_message_block_with_multiple_content_items(): """Test content with multiple items.""" from google.adk.models.anthropic_llm import part_to_message_block