From 6ff5a9f76c3e6839a463dde2f760ddf7fc2c0279 Mon Sep 17 00:00:00 2001 From: Mrunmayee Daware <165297479+Mrun25@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:46:22 +0530 Subject: [PATCH 1/4] fix: keep is_error from Anthropic tool_result in /v1/messages translation --- .../pass_through/adapters/transformation.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 8e0dc83e563..8bf0d43b235 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -525,10 +525,13 @@ class LiteLLMAnthropicMessagesAdapter: self._add_cache_control_if_applicable(content, doc_obj, model) new_user_content_list.append(doc_obj) elif content.get("type") == "tool_result": + tool_result_content = self._tool_result_content(content.get("content")) + if content.get("is_error"): + tool_result_content = self._mark_tool_result_as_error(tool_result_content) tool_result = ChatCompletionToolMessage( role="tool", tool_call_id=content.get("tool_use_id", ""), - content=self._tool_result_content(content.get("content")), + content=tool_result_content, ) self._add_cache_control_if_applicable(content, tool_result, model) tool_message_list.append(tool_result) @@ -1317,6 +1320,20 @@ class LiteLLMAnthropicMessagesAdapter: case _: return list(parts) + @staticmethod + def _mark_tool_result_as_error(content: ToolResultContent) -> ToolResultContent: + """Keep Anthropic's `is_error` signal visible. + + Chat Completions tool messages have no error field, so encode the + flag into the content itself; models and downstream provider + mappings can then still tell the tool call failed. + """ + if isinstance(content, str): + return "[tool error] " + content if content else "[tool error]" + if isinstance(content, list): + return [ChatCompletionTextObject(type="text", text="[tool error] "), *content] + return content + def _tool_result_part(self, item: object) -> ToolMessageContentPart | None: if isinstance(item, str): return ChatCompletionTextObject(type="text", text=item) From 65c92ee6bfd6b1e49d701b3b5ebd22a1a34e91ee Mon Sep 17 00:00:00 2001 From: Mrunmayee Daware <165297479+Mrun25@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:50:39 +0530 Subject: [PATCH 2/4] test: cover is_error encoding in anthropic messages adapter --- ...al_pass_through_adapters_transformation.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 06aaa4e61fb..36cc9baba95 100644 --- a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -4644,6 +4644,83 @@ def test_tool_result_without_translatable_content_still_answers_its_tool_use(too assert result[0]["role"] == "assistant" +def test_tool_result_is_error_is_encoded_in_string_content(): + adapter = LiteLLMAnthropicMessagesAdapter() + + result = adapter.translate_anthropic_messages_to_openai( + messages=[ + _anthropic_tool_use_turn("toolu_01"), + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_01", + "content": "deploy failed", + "is_error": True, + } + ], + }, + ] + ) + + assert result[1] == { + "role": "tool", + "tool_call_id": "toolu_01", + "content": "[tool error] deploy failed", + } + + +def test_tool_result_is_error_puts_the_marker_first_in_part_lists(): + adapter = LiteLLMAnthropicMessagesAdapter() + + result = adapter.translate_anthropic_messages_to_openai( + messages=[ + _anthropic_tool_use_turn("toolu_01"), + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_01", + "content": [{"type": "text", "text": "boom"}, _tool_reference_block()], + "is_error": True, + } + ], + }, + ] + ) + + assert result[1]["content"] == [ + {"type": "text", "text": "[tool error] "}, + {"type": "text", "text": "boom"}, + {"type": "tool_reference", "tool_name": "WebFetch"}, + ] + + +def test_tool_result_without_is_error_keeps_content_unchanged(): + adapter = LiteLLMAnthropicMessagesAdapter() + + result = adapter.translate_anthropic_messages_to_openai( + messages=[ + _anthropic_tool_use_turn("toolu_01"), + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_01", + "content": "deploy succeeded", + "is_error": False, + } + ], + }, + ] + ) + + assert result[1]["content"] == "deploy succeeded" + + def _openai_response_with_usage(usage: Usage) -> ModelResponse: return ModelResponse( id="resp_web_search", From 67426ac2633e76dcaf5fc2c7990d955ae645ecc9 Mon Sep 17 00:00:00 2001 From: Mrunmayee Daware <165297479+Mrun25@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:25:14 +0530 Subject: [PATCH 3/4] refactor: compute tool_result content once, annotate as Final --- .../anthropic/pass_through/adapters/transformation.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 8bf0d43b235..f2734e77aec 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -525,9 +525,12 @@ class LiteLLMAnthropicMessagesAdapter: self._add_cache_control_if_applicable(content, doc_obj, model) new_user_content_list.append(doc_obj) elif content.get("type") == "tool_result": - tool_result_content = self._tool_result_content(content.get("content")) - if content.get("is_error"): - tool_result_content = self._mark_tool_result_as_error(tool_result_content) + raw_tool_result_content: Final = self._tool_result_content(content.get("content")) + tool_result_content: Final = ( + self._mark_tool_result_as_error(raw_tool_result_content) + if content.get("is_error") + else raw_tool_result_content + ) tool_result = ChatCompletionToolMessage( role="tool", tool_call_id=content.get("tool_use_id", ""), From ec7ffe3b36d5ea78b6d6458d8fc3e5297adf2f36 Mon Sep 17 00:00:00 2001 From: Mrunmayee Daware <165297479+Mrun25@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:26:20 +0530 Subject: [PATCH 4/4] test: cover is_error with empty tool_result content --- ...al_pass_through_adapters_transformation.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 36cc9baba95..0527ba0d487 100644 --- a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -4698,6 +4698,29 @@ def test_tool_result_is_error_puts_the_marker_first_in_part_lists(): ] +def test_tool_result_is_error_with_empty_content_keeps_the_marker(): + adapter = LiteLLMAnthropicMessagesAdapter() + + result = adapter.translate_anthropic_messages_to_openai( + messages=[ + _anthropic_tool_use_turn("toolu_01"), + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_01", + "content": "", + "is_error": True, + } + ], + }, + ] + ) + + assert result[1]["content"] == "[tool error]" + + def test_tool_result_without_is_error_keeps_content_unchanged(): adapter = LiteLLMAnthropicMessagesAdapter()