From 561951e31700d3fade16b660b45c51db24ce7c21 Mon Sep 17 00:00:00 2001 From: er-s-an Date: Fri, 9 Oct 2026 09:35:37 +0800 Subject: [PATCH 1/2] fix(anthropic): preserve assistant text order around tool_use in Responses adapter Fixes #45470. translate_messages_to_responses_input previously deferred all assistant text into one trailing message after function_call items, replaying text that appeared before the tool. Emit text blocks in group order and stop collecting them at the end. --- .../responses_adapters/transformation.py | 23 +++++----- .../test_responses_adapters_transformation.py | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/litellm/llms/anthropic/messages/responses_adapters/transformation.py b/litellm/llms/anthropic/messages/responses_adapters/transformation.py index 0630bef0311..0b48f9aaae2 100644 --- a/litellm/llms/anthropic/messages/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/messages/responses_adapters/transformation.py @@ -287,6 +287,16 @@ class LiteLLMAnthropicToResponsesAPIAdapter: "arguments": json.dumps(first.get("input", {})), }, ) + if btype == "text": + # Emit text in block order so text before/after tool_use is not + # replayed as one trailing assistant message (#45470). + return ( + { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": first.get("text", "")}], + }, + ) return () def translate_messages_to_responses_input( @@ -440,19 +450,6 @@ class LiteLLMAnthropicToResponsesAPIAdapter: for _, group in groupby(enumerate(blocks), key=self._assistant_block_group_key) for item in self._assistant_group_to_input_items(tuple(block for _, block in group)) ) - asst_parts: list[dict[str, object]] = [ # mutable-ok: API message payload - {"type": "output_text", "text": block.get("text", "")} - for block in blocks - if block.get("type") == "text" - ] - if asst_parts: - input_items.append( - { - "type": "message", - "role": "assistant", - "content": asst_parts, - } - ) return input_items diff --git a/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py b/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py index ece0cf5d423..e72e8555409 100644 --- a/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py @@ -671,6 +671,51 @@ class TestTranslateMessagesToResponsesInput: assert result[0]["summary"] == [{"type": "summary_text", "text": "Before the call."}] assert result[2]["summary"] == [{"type": "summary_text", "text": "After the call."}] + def test_assistant_text_before_and_after_tool_use_preserves_order(self): + """Text around tool_use stays in block order, not replayed after (#45470).""" + messages = [ + {"role": "user", "content": "weather in Paris?"}, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "Let me check."}, + { + "type": "tool_use", + "id": "toolu_1", + "name": "get_weather", + "input": {"city": "Paris"}, + }, + {"type": "text", "text": "(after)"}, + ], + }, + { + "role": "user", + "content": [ + {"type": "tool_result", "tool_use_id": "toolu_1", "content": "sunny"} + ], + }, + ] + result = _translate_messages(messages) + assert [item["type"] for item in result] == [ + "message", + "message", + "function_call", + "message", + "function_call_output", + ] + assert result[1] == { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "Let me check."}], + } + assert result[2]["type"] == "function_call" + assert result[2]["call_id"] == "toolu_1" + assert result[3] == { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "(after)"}], + } + def test_thinking_and_text_stay_separate(self): """The visible answer stays the only thing in the assistant message.""" messages = [ From 4c1cc99d3e64f5aea5ef6c3037bd0044efefcd93 Mon Sep 17 00:00:00 2001 From: er-s-an <92594860+er-s-an@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:05:18 +0800 Subject: [PATCH 2/2] style: drop redundant comment and add Final on #45470 regression test Address greptile review on PR #45478: remove explanatory comments and annotate new test locals with Final per AGENTS.md. --- .../anthropic/messages/responses_adapters/transformation.py | 2 -- .../test_responses_adapters_transformation.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/litellm/llms/anthropic/messages/responses_adapters/transformation.py b/litellm/llms/anthropic/messages/responses_adapters/transformation.py index 0b48f9aaae2..547a48e5fbc 100644 --- a/litellm/llms/anthropic/messages/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/messages/responses_adapters/transformation.py @@ -288,8 +288,6 @@ class LiteLLMAnthropicToResponsesAPIAdapter: }, ) if btype == "text": - # Emit text in block order so text before/after tool_use is not - # replayed as one trailing assistant message (#45470). return ( { "type": "message", diff --git a/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py b/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py index e72e8555409..efe0c23cbcf 100644 --- a/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py @@ -673,7 +673,7 @@ class TestTranslateMessagesToResponsesInput: def test_assistant_text_before_and_after_tool_use_preserves_order(self): """Text around tool_use stays in block order, not replayed after (#45470).""" - messages = [ + messages: Final = [ {"role": "user", "content": "weather in Paris?"}, { "role": "assistant", @@ -695,7 +695,7 @@ class TestTranslateMessagesToResponsesInput: ], }, ] - result = _translate_messages(messages) + result: Final = _translate_messages(messages) assert [item["type"] for item in result] == [ "message", "message",