Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 62 additions & 24 deletions litellm/litellm_core_utils/prompt_templates/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1623,7 +1623,8 @@ def convert_function_to_anthropic_tool_invoke(

def convert_to_anthropic_tool_invoke(
tool_calls: List[ChatCompletionAssistantToolCall],
) -> List[AnthropicMessagesToolUseParam]:
web_search_results: Optional[List[Any]] = None,
) -> List[Union[AnthropicMessagesToolUseParam, Dict[str, Any]]]:
"""
OpenAI tool invokes:
{
Expand Down Expand Up @@ -1659,38 +1660,68 @@ def convert_to_anthropic_tool_invoke(
}
]
}

For server-side tools (web_search), we need to reconstruct:
- server_tool_use blocks (id starts with "srvtoolu_")
- web_search_tool_result blocks (from provider_specific_fields)

Fixes: https://github.com/BerriAI/litellm/issues/17737
"""
anthropic_tool_invoke = []
anthropic_tool_invoke: List[Union[AnthropicMessagesToolUseParam, Dict[str, Any]]] = []

for tool in tool_calls:
if not get_attribute_or_key(tool, "type") == "function":
continue

_anthropic_tool_use_param = AnthropicMessagesToolUseParam(
type="tool_use",
id=cast(str, get_attribute_or_key(tool, "id")),
name=cast(
str,
get_attribute_or_key(get_attribute_or_key(tool, "function"), "name"),
),
input=json.loads(
get_attribute_or_key(
get_attribute_or_key(tool, "function"), "arguments"
)
),
tool_id = cast(str, get_attribute_or_key(tool, "id"))
tool_name = cast(
str,
get_attribute_or_key(get_attribute_or_key(tool, "function"), "name"),
)

_content_element = add_cache_control_to_content(
anthropic_content_element=_anthropic_tool_use_param,
original_content_element=dict(tool),
tool_input = json.loads(
get_attribute_or_key(
get_attribute_or_key(tool, "function"), "arguments"
)
)

if "cache_control" in _content_element:
_anthropic_tool_use_param["cache_control"] = _content_element[
"cache_control"
]
# Check if this is a server-side tool (web_search, tool_search, etc.)
# Server tool IDs start with "srvtoolu_"
if tool_id.startswith("srvtoolu_"):
# Create server_tool_use block instead of tool_use
_anthropic_server_tool_use: Dict[str, Any] = {
"type": "server_tool_use",
"id": tool_id,
"name": tool_name,
"input": tool_input,
}
anthropic_tool_invoke.append(_anthropic_server_tool_use)

# Add corresponding web_search_tool_result if available
if web_search_results:
for result in web_search_results:
if result.get("tool_use_id") == tool_id:
anthropic_tool_invoke.append(result)
break
else:
# Regular tool_use
_anthropic_tool_use_param = AnthropicMessagesToolUseParam(
type="tool_use",
id=tool_id,
name=tool_name,
input=tool_input,
)

_content_element = add_cache_control_to_content(
anthropic_content_element=_anthropic_tool_use_param,
original_content_element=dict(tool),
)

anthropic_tool_invoke.append(_anthropic_tool_use_param)
if "cache_control" in _content_element:
_anthropic_tool_use_param["cache_control"] = _content_element[
"cache_control"
]

anthropic_tool_invoke.append(_anthropic_tool_use_param)

return anthropic_tool_invoke

Expand Down Expand Up @@ -2052,8 +2083,15 @@ def anthropic_messages_pt( # noqa: PLR0915
if (
assistant_tool_calls is not None
): # support assistant tool invoke conversion
# Get web_search_results from provider_specific_fields for server_tool_use reconstruction
# Fixes: https://github.com/BerriAI/litellm/issues/17737
_provider_specific_fields = assistant_content_block.get("provider_specific_fields") or {}
_web_search_results = _provider_specific_fields.get("web_search_results")
assistant_content.extend(
convert_to_anthropic_tool_invoke(assistant_tool_calls)
convert_to_anthropic_tool_invoke(
assistant_tool_calls,
web_search_results=_web_search_results,
)
)

assistant_function_call = assistant_content_block.get("function_call")
Expand Down
12 changes: 11 additions & 1 deletion litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,7 @@ def extract_response_content(self, completion_response: dict) -> Tuple[
],
Optional[str],
List[ChatCompletionToolCallChunk],
Optional[List[Any]],
]:
text_content = ""
citations: Optional[List[Any]] = None
Expand All @@ -1092,6 +1093,7 @@ def extract_response_content(self, completion_response: dict) -> Tuple[
] = None
reasoning_content: Optional[str] = None
tool_calls: List[ChatCompletionToolCallChunk] = []
web_search_results: Optional[List[Any]] = None
for idx, content in enumerate(completion_response["content"]):
if content["type"] == "text":
text_content += content["text"]
Expand All @@ -1117,6 +1119,11 @@ def extract_response_content(self, completion_response: dict) -> Tuple[
# This block contains tool_references that were discovered
# We don't need to include this in the response as it's internal metadata
pass
## WEB SEARCH TOOL RESULT - preserve web search results for multi-turn conversations
elif content["type"] == "web_search_tool_result":
if web_search_results is None:
web_search_results = []
web_search_results.append(content)
elif content.get("thinking", None) is not None:
if thinking_blocks is None:
thinking_blocks = []
Expand Down Expand Up @@ -1148,7 +1155,7 @@ def extract_response_content(self, completion_response: dict) -> Tuple[
if thinking_content is not None:
reasoning_content += thinking_content

return text_content, citations, thinking_blocks, reasoning_content, tool_calls
return text_content, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results

def calculate_usage(
self,
Expand Down Expand Up @@ -1288,6 +1295,7 @@ def transform_parsed_response(
thinking_blocks,
reasoning_content,
tool_calls,
web_search_results,
) = self.extract_response_content(completion_response=completion_response)

if (
Expand All @@ -1307,6 +1315,8 @@ def transform_parsed_response(
}
if context_management is not None:
provider_specific_fields["context_management"] = context_management
if web_search_results is not None:
provider_specific_fields["web_search_results"] = web_search_results

_message = litellm.Message(
tool_calls=tool_calls,
Expand Down
215 changes: 215 additions & 0 deletions tests/llm_translation/test_prompt_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
anthropic_pt,
claude_2_1_pt,
convert_to_anthropic_image_obj,
convert_to_anthropic_tool_invoke,
convert_url_to_base64,
create_anthropic_image_param,
llama_2_chat_pt,
Expand Down Expand Up @@ -947,3 +948,217 @@ def test_ollama_pt():
]
prompt = ollama_pt(model="ollama/llama3.1", messages=messages)
print(prompt)


# ============ Server Tool Use Reconstruction Tests ============
# Fixes: https://github.com/BerriAI/litellm/issues/17737


def test_convert_to_anthropic_tool_invoke_regular_tool():
"""Test that regular tool_use is converted correctly."""
tool_calls = [
{
"id": "toolu_01ABC123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "San Francisco"}'
}
}
]

result = convert_to_anthropic_tool_invoke(tool_calls)

assert len(result) == 1
assert result[0]["type"] == "tool_use"
assert result[0]["id"] == "toolu_01ABC123"
assert result[0]["name"] == "get_weather"
assert result[0]["input"] == {"location": "San Francisco"}


def test_convert_to_anthropic_tool_invoke_server_tool():
"""
Test that server_tool_use (srvtoolu_) is reconstructed as server_tool_use.

Fixes: https://github.com/BerriAI/litellm/issues/17737
"""
tool_calls = [
{
"id": "srvtoolu_01ABC123",
"type": "function",
"function": {
"name": "web_search",
"arguments": '{"query": "elephant weight"}'
}
}
]

result = convert_to_anthropic_tool_invoke(tool_calls)

assert len(result) == 1
assert result[0]["type"] == "server_tool_use" # NOT tool_use
assert result[0]["id"] == "srvtoolu_01ABC123"
assert result[0]["name"] == "web_search"
assert result[0]["input"] == {"query": "elephant weight"}


def test_convert_to_anthropic_tool_invoke_with_web_search_results():
"""
Test that web_search_tool_result is included after server_tool_use.

Fixes: https://github.com/BerriAI/litellm/issues/17737
"""
tool_calls = [
{
"id": "srvtoolu_01ABC123",
"type": "function",
"function": {
"name": "web_search",
"arguments": '{"query": "elephant weight"}'
}
}
]

web_search_results = [
{
"type": "web_search_tool_result",
"tool_use_id": "srvtoolu_01ABC123",
"content": [
{
"type": "web_search_result",
"url": "https://example.com",
"title": "Elephant Facts",
"snippet": "Elephants weigh 5000 kg"
}
]
}
]

result = convert_to_anthropic_tool_invoke(tool_calls, web_search_results=web_search_results)

assert len(result) == 2
# First: server_tool_use
assert result[0]["type"] == "server_tool_use"
assert result[0]["id"] == "srvtoolu_01ABC123"
# Second: web_search_tool_result
assert result[1]["type"] == "web_search_tool_result"
assert result[1]["tool_use_id"] == "srvtoolu_01ABC123"


def test_convert_to_anthropic_tool_invoke_mixed_tools():
"""
Test that mixed server and regular tools are reconstructed correctly.

Fixes: https://github.com/BerriAI/litellm/issues/17737
"""
tool_calls = [
{
"id": "srvtoolu_01ABC123",
"type": "function",
"function": {
"name": "web_search",
"arguments": '{"query": "elephant weight"}'
}
},
{
"id": "toolu_01XYZ789",
"type": "function",
"function": {
"name": "add_numbers",
"arguments": '{"a": 5000, "b": 100}'
}
}
]

web_search_results = [
{
"type": "web_search_tool_result",
"tool_use_id": "srvtoolu_01ABC123",
"content": [{"url": "https://example.com", "title": "Test"}]
}
]

result = convert_to_anthropic_tool_invoke(tool_calls, web_search_results=web_search_results)

assert len(result) == 3
# First: server_tool_use
assert result[0]["type"] == "server_tool_use"
assert result[0]["id"] == "srvtoolu_01ABC123"
# Second: web_search_tool_result
assert result[1]["type"] == "web_search_tool_result"
# Third: regular tool_use
assert result[2]["type"] == "tool_use"
assert result[2]["id"] == "toolu_01XYZ789"


def test_anthropic_messages_pt_with_server_tool_use():
"""
Test that anthropic_messages_pt correctly reconstructs server_tool_use from provider_specific_fields.

Fixes: https://github.com/BerriAI/litellm/issues/17737
"""
messages = [
{"role": "user", "content": "Search for elephant weight and add 100"},
{
"role": "assistant",
"content": "Let me search for that.",
"tool_calls": [
{
"id": "srvtoolu_01ABC123",
"type": "function",
"function": {
"name": "web_search",
"arguments": '{"query": "elephant weight"}'
}
},
{
"id": "toolu_01XYZ789",
"type": "function",
"function": {
"name": "add_numbers",
"arguments": '{"a": 5000, "b": 100}'
}
}
],
"provider_specific_fields": {
"web_search_results": [
{
"type": "web_search_tool_result",
"tool_use_id": "srvtoolu_01ABC123",
"content": [{"url": "https://example.com", "title": "Test", "snippet": "5000 kg"}]
}
]
}
},
{
"role": "tool",
"tool_call_id": "toolu_01XYZ789",
"content": "5100"
}
]

result = anthropic_messages_pt(messages, model="claude-sonnet-4-5", llm_provider="anthropic")

# Find the assistant message
assistant_msg = next(m for m in result if m["role"] == "assistant")
content = assistant_msg["content"]

# Should have: text, server_tool_use, web_search_tool_result, tool_use
types = [c.get("type") for c in content]
assert "text" in types
assert "server_tool_use" in types
assert "web_search_tool_result" in types
assert "tool_use" in types

# Verify server_tool_use
server_tool = next(c for c in content if c.get("type") == "server_tool_use")
assert server_tool["id"] == "srvtoolu_01ABC123"

# Verify web_search_tool_result comes after server_tool_use
server_idx = types.index("server_tool_use")
web_result_idx = types.index("web_search_tool_result")
assert web_result_idx == server_idx + 1

# Verify regular tool_use
tool_use = next(c for c in content if c.get("type") == "tool_use")
assert tool_use["id"] == "toolu_01XYZ789"
Loading
Loading