diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 3aad0be1913..e42cadbe3af 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -529,10 +529,16 @@ 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": + 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", ""), - 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) @@ -1321,6 +1327,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) 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 9e0bd5c60b8..70c77eefdf1 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 @@ -4712,6 +4712,106 @@ 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_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() + + 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",