From a30596c3dd6af008170491fc647c1f88bc25348b Mon Sep 17 00:00:00 2001 From: aayushbaluni <73417844+aayushbaluni@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:14:39 +0530 Subject: [PATCH] fix: detect Anthropic-format tool_use content blocks in orphan detection - Update _is_orphaned_tool_result to check for tool_use content blocks - Add tool_result to ValidUserMessageContentTypes - Add tests for Anthropic-format tool content handling Fixes #25669 Made-with: Cursor --- .../prompt_templates/factory.py | 11 ++ litellm/types/llms/openai.py | 7 +- .../anthropic/test_anthropic_tool_content.py | 110 ++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 tests/test_litellm/llms/anthropic/test_anthropic_tool_content.py diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index a037360c87d..61b859f7651 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -2218,6 +2218,7 @@ def _is_orphaned_tool_result( for j in range(len(sanitized_messages) - 1, -1, -1): prev_msg = sanitized_messages[j] if prev_msg.get("role") == "assistant": + # Check OpenAI format: tool_calls array tool_calls = prev_msg.get("tool_calls") if tool_calls: for tool_call in cast(list, tool_calls): @@ -2231,6 +2232,16 @@ def _is_orphaned_tool_result( found_matching_tool_call = True break + # Check Anthropic format: content blocks with type=tool_use + if not found_matching_tool_call: + content = prev_msg.get("content") + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "tool_use": + if block.get("id") == tool_call_id: + found_matching_tool_call = True + break + break if not found_matching_tool_call: diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 80b6190db8f..e53d9c36676 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -797,7 +797,8 @@ ValidUserMessageContentTypes = [ "guarded_text", "video_url", "file", -] # used for validating user messages. Prevent users from accidentally sending anthropic messages. + "tool_result", +] # used for validating user messages. Includes tool_result for Anthropic-format content blocks. ValidUserMessageContentTypesLiteral = Literal[ "text", @@ -808,6 +809,7 @@ ValidUserMessageContentTypesLiteral = Literal[ "guarded_text", "video_url", "file", + "tool_result", ] ValidUserMessageContentTypes = [ @@ -819,7 +821,8 @@ ValidUserMessageContentTypes = [ "guarded_text", "video_url", "file", -] # used for validating user messages. Prevent users from accidentally sending anthropic messages. + "tool_result", +] # used for validating user messages. Includes tool_result for Anthropic-format content blocks. # Assistant message content types (text, thinking, redacted_thinking, image_url) ValidAssistantMessageContentTypesLiteral = Literal[ diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_tool_content.py b/tests/test_litellm/llms/anthropic/test_anthropic_tool_content.py new file mode 100644 index 00000000000..17eea53b49b --- /dev/null +++ b/tests/test_litellm/llms/anthropic/test_anthropic_tool_content.py @@ -0,0 +1,110 @@ +""" +Tests for Anthropic-format tool_use handling in sanitization and user-message validation. + +Covers: +- _is_orphaned_tool_result matching prior assistant tool_use content blocks +- validate_chat_completion_user_messages accepting tool_result content blocks in user messages +""" + +import os +import sys + +import pytest + +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")) +) + +from litellm.litellm_core_utils.prompt_templates.factory import _is_orphaned_tool_result +from litellm.types.llms.openai import ValidUserMessageContentTypes +from litellm.utils import validate_chat_completion_user_messages + + +class TestIsOrphanedToolResultAnthropicFormat: + """_is_orphaned_tool_result with Anthropic-format tool_use content blocks.""" + + def test_not_orphaned_when_anthropic_tool_use_present(self): + """Tool message is not orphaned when the latest assistant has a matching tool_use block.""" + sanitized_messages = [ + { + "role": "assistant", + "content": [ + {"type": "text", "text": "Let me check that."}, + { + "type": "tool_use", + "id": "call_123", + "name": "get_weather", + "input": {"city": "SF"}, + }, + ], + } + ] + current_message = { + "role": "tool", + "tool_call_id": "call_123", + "content": "72F sunny", + } + assert _is_orphaned_tool_result(current_message, sanitized_messages) is False + + def test_orphaned_when_no_tool_use_in_assistant(self): + """Tool message is orphaned when the latest assistant has no tool_calls or tool_use.""" + sanitized_messages = [ + { + "role": "assistant", + "content": "Just a plain text response", + } + ] + current_message = { + "role": "tool", + "tool_call_id": "call_456", + "content": "result data", + } + assert _is_orphaned_tool_result(current_message, sanitized_messages) is True + + def test_not_orphaned_with_openai_format_tool_calls(self): + """Tool message is not orphaned when the assistant uses OpenAI-format tool_calls.""" + sanitized_messages = [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_789", + "type": "function", + "function": {"name": "search", "arguments": "{}"}, + } + ], + } + ] + current_message = { + "role": "tool", + "tool_call_id": "call_789", + "content": "search results", + } + assert _is_orphaned_tool_result(current_message, sanitized_messages) is False + + +class TestToolResultUserMessageValidation: + """validate_chat_completion_user_messages with Anthropic-style tool_result blocks.""" + + def test_tool_result_in_valid_types(self): + """tool_result is an allowed user content type for mixed Anthropic-style payloads.""" + assert "tool_result" in ValidUserMessageContentTypes + + def test_validate_accepts_tool_result_in_user_message(self): + """User messages may include type=tool_result content blocks (e.g. Cursor / Anthropic).""" + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Here is the tool output."}, + { + "type": "tool_result", + "tool_use_id": "call_abc", + "content": "ok", + }, + ], + } + ] + out = validate_chat_completion_user_messages(messages) + assert out == messages