From fe53ba509625a90fa83a49ba9030a0c123419f5b Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Sun, 23 Aug 2026 11:32:06 +0000 Subject: [PATCH] fix: accumulate all text blocks in Anthropic non-streaming response The non-streaming _query loop set llm_response.completion_text on every text content block instead of appending to it, so only the last text block in a multi-block Message.content survived; earlier blocks were silently dropped. The streaming path already accumulates correctly with final_text +=. A Message can carry more than one text block (the Citations API and extended-thinking flows both produce this shape). Adds a regression test that fails on the old overwrite behavior and passes with the fix. --- .../core/provider/sources/anthropic_source.py | 5 +-- tests/test_anthropic_kimi_code_provider.py | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 27cc459622..f4e425e3eb 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -540,11 +540,11 @@ async def _query( ) llm_response = LLMResponse(role="assistant") + completion_text_parts: list[str] = [] for content_block in completion.content: if content_block.type == "text": - completion_text = str(content_block.text).strip() - llm_response.completion_text = completion_text + completion_text_parts.append(str(content_block.text)) if content_block.type == "thinking": reasoning_content = str(content_block.thinking).strip() @@ -556,6 +556,7 @@ async def _query( llm_response.tools_call_name.append(content_block.name) llm_response.tools_call_ids.append(content_block.id) + llm_response.completion_text = "".join(completion_text_parts).strip() llm_response.id = completion.id llm_response.usage = self._extract_usage(completion.usage) diff --git a/tests/test_anthropic_kimi_code_provider.py b/tests/test_anthropic_kimi_code_provider.py index 0dc33f58ba..42e8855249 100644 --- a/tests/test_anthropic_kimi_code_provider.py +++ b/tests/test_anthropic_kimi_code_provider.py @@ -736,6 +736,40 @@ async def fake_create(**kwargs): assert llm_response.usage.output == 0 +@pytest.mark.asyncio +async def test_query_accumulates_multiple_text_blocks(monkeypatch): + """A Message with more than one text content block (e.g. Citations API + output) must concatenate every block, not keep only the last one.""" + provider = _setup_provider_with_mock_client(monkeypatch) + + class _FakeMessageBlock: + def __init__(self, text: str): + self.type = "text" + self.text = text + + class _FakeMessage: + def __init__(self): + self.id = "msg_multi_block" + self.content = [ + _FakeMessageBlock("First block: the important part."), + _FakeMessageBlock("Second block: a trailing citation note."), + ] + self.stop_reason = "end_turn" + self.usage = None + + async def fake_create(**kwargs): + return _FakeMessage() + + monkeypatch.setattr(anthropic_source, "Message", _FakeMessage) + provider.client.messages.create = fake_create + + llm_response = await provider.text_chat(prompt="test") + + assert llm_response.completion_text == ( + "First block: the important part.Second block: a trailing citation note." + ) + + @pytest.mark.asyncio async def test_tool_choice_auto_converts_to_dict(monkeypatch): """tool_choice='auto' 应转换为 {'type': 'auto'}"""