From a04553013d81610f73b7fe3bdb98a0df53451942 Mon Sep 17 00:00:00 2001 From: Afonso Coutinho Date: Tue, 2 Jun 2026 16:57:20 +0100 Subject: [PATCH] fix: Anthropic responses with multiple text blocks lose all but the first (#1255) * fix: concatenate all Anthropic text blocks, not just the first * test: Anthropic response parsing concatenates text blocks --- src/llm_core.py | 17 +++++++++++----- tests/test_anthropic_response_parse.py | 27 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 tests/test_anthropic_response_parse.py diff --git a/src/llm_core.py b/src/llm_core.py index 77afbd1..1e3d12a 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -562,11 +562,18 @@ def _build_anthropic_headers(headers): return h def _parse_anthropic_response(data: dict) -> str: - """Extract text from Anthropic response.""" - for block in data.get("content", []): - if block.get("type") == "text": - return block.get("text", "") - return "" + """Extract text from an Anthropic response. + + The Messages API `content` is an array that can hold more than one text + block (e.g. text split around a tool_use block, or citation-segmented + text). Concatenate them all instead of returning only the first, which + silently dropped the rest of the reply. + """ + return "".join( + block.get("text", "") + for block in data.get("content", []) + if isinstance(block, dict) and block.get("type") == "text" + ) def _sanitize_llm_messages(messages: List[Dict]) -> List[Dict]: diff --git a/tests/test_anthropic_response_parse.py b/tests/test_anthropic_response_parse.py new file mode 100644 index 0000000..e41c9bb --- /dev/null +++ b/tests/test_anthropic_response_parse.py @@ -0,0 +1,27 @@ +"""Tests for _parse_anthropic_response (src/llm_core.py).""" + +from src.llm_core import _parse_anthropic_response + + +def test_concatenates_multiple_text_blocks(): + # Regression: only the first text block was returned, dropping the rest. + data = {"content": [ + {"type": "text", "text": "Part A "}, + {"type": "tool_use", "id": "t1", "name": "x", "input": {}}, + {"type": "text", "text": "Part B"}, + ]} + assert _parse_anthropic_response(data) == "Part A Part B" + + +def test_skips_non_text_blocks(): + data = {"content": [ + {"type": "thinking", "thinking": "..."}, + {"type": "text", "text": "answer"}, + ]} + assert _parse_anthropic_response(data) == "answer" + + +def test_single_block_and_empty(): + assert _parse_anthropic_response({"content": [{"type": "text", "text": "hi"}]}) == "hi" + assert _parse_anthropic_response({"content": []}) == "" + assert _parse_anthropic_response({}) == ""