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
This commit is contained in:
aayushbaluni 2026-04-15 18:14:39 +05:30
parent 72a461ba4a
commit a30596c3dd
3 changed files with 126 additions and 2 deletions

View file

@ -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:

View file

@ -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[

View file

@ -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