mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-10 17:04:47 +00:00
Preserve forwarding server side called tools
This commit is contained in:
@@ -1766,6 +1766,7 @@ def convert_function_to_anthropic_tool_invoke(
|
||||
def convert_to_anthropic_tool_invoke(
|
||||
tool_calls: List[ChatCompletionAssistantToolCall],
|
||||
web_search_results: Optional[List[Any]] = None,
|
||||
tool_results: Optional[List[Any]] = None,
|
||||
) -> List[Union[AnthropicMessagesToolUseParam, Dict[str, Any]]]:
|
||||
"""
|
||||
OpenAI tool invokes:
|
||||
@@ -1840,12 +1841,18 @@ def convert_to_anthropic_tool_invoke(
|
||||
}
|
||||
anthropic_tool_invoke.append(_anthropic_server_tool_use)
|
||||
|
||||
# Add corresponding web_search_tool_result if available
|
||||
# Add corresponding tool result if available.
|
||||
# Check both web_search_results (web_search_tool_result / web_fetch_tool_result)
|
||||
# and tool_results (bash_code_execution_tool_result, etc.)
|
||||
_all_tool_results: List[Any] = []
|
||||
if web_search_results:
|
||||
for result in web_search_results:
|
||||
if result.get("tool_use_id") == tool_id:
|
||||
anthropic_tool_invoke.append(result)
|
||||
break
|
||||
_all_tool_results.extend(web_search_results)
|
||||
if tool_results:
|
||||
_all_tool_results.extend(tool_results)
|
||||
for result in _all_tool_results:
|
||||
if result.get("tool_use_id") == tool_id:
|
||||
anthropic_tool_invoke.append(result)
|
||||
break
|
||||
else:
|
||||
# Regular tool_use
|
||||
sanitized_tool_id = _sanitize_anthropic_tool_use_id(tool_id)
|
||||
@@ -2472,9 +2479,10 @@ def anthropic_messages_pt( # noqa: PLR0915
|
||||
# Pass through as-is since these are Anthropic-native content types
|
||||
elif m.get("type", "") == "server_tool_use":
|
||||
assistant_content.append(m) # type: ignore
|
||||
# handle tool_search_tool_result blocks
|
||||
# handle all *_tool_result blocks (tool_search_tool_result,
|
||||
# web_search_tool_result, bash_code_execution_tool_result, etc.)
|
||||
# Pass through as-is since these are Anthropic-native content types
|
||||
elif m.get("type", "") == "tool_search_tool_result":
|
||||
elif m.get("type", "").endswith("_tool_result"):
|
||||
assistant_content.append(m) # type: ignore
|
||||
elif (
|
||||
"content" in assistant_content_block
|
||||
@@ -2504,7 +2512,8 @@ 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
|
||||
# Get web_search_results and tool_results from provider_specific_fields
|
||||
# for server_tool_use reconstruction.
|
||||
# Fixes: https://github.com/BerriAI/litellm/issues/17737
|
||||
_provider_specific_fields_raw = assistant_content_block.get(
|
||||
"provider_specific_fields"
|
||||
@@ -2517,9 +2526,11 @@ def anthropic_messages_pt( # noqa: PLR0915
|
||||
_web_search_results = _provider_specific_fields.get(
|
||||
"web_search_results"
|
||||
)
|
||||
_tool_results = _provider_specific_fields.get("tool_results")
|
||||
tool_invoke_results = convert_to_anthropic_tool_invoke(
|
||||
assistant_tool_calls,
|
||||
web_search_results=_web_search_results,
|
||||
tool_results=_tool_results,
|
||||
)
|
||||
|
||||
# Prevent "tool_use ids must be unique" errors by filtering duplicates
|
||||
|
||||
@@ -1214,6 +1214,181 @@ def test_anthropic_messages_pt_with_server_tool_use():
|
||||
assert tool_use["id"] == "toolu_01XYZ789"
|
||||
|
||||
|
||||
def test_convert_to_anthropic_tool_invoke_with_tool_results():
|
||||
"""
|
||||
Test that non-web-search *_tool_result blocks (e.g. bash_code_execution_tool_result)
|
||||
stored in provider_specific_fields["tool_results"] are paired with their server_tool_use
|
||||
block when reconstructing assistant history.
|
||||
|
||||
Regression for: server tool result blocks dropped on multi-turn replay
|
||||
(bash_code_execution_tool_result, text_editor_code_execution_tool_result, etc.)
|
||||
"""
|
||||
tool_calls = [
|
||||
{
|
||||
"id": "srvtoolu_01BASH",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "bash_code_execution",
|
||||
"arguments": '{"command": "python3 -c \\"print(2)\\""}',
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
tool_results = [
|
||||
{
|
||||
"type": "bash_code_execution_tool_result",
|
||||
"tool_use_id": "srvtoolu_01BASH",
|
||||
"content": {
|
||||
"type": "bash_code_execution_result",
|
||||
"stdout": "2\n",
|
||||
"stderr": "",
|
||||
"return_code": 0,
|
||||
"content": [],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
result = convert_to_anthropic_tool_invoke(tool_calls, tool_results=tool_results)
|
||||
|
||||
assert len(result) == 2
|
||||
# First: server_tool_use
|
||||
assert result[0]["type"] == "server_tool_use"
|
||||
assert result[0]["id"] == "srvtoolu_01BASH"
|
||||
assert result[0]["name"] == "bash_code_execution"
|
||||
# Second: bash_code_execution_tool_result paired correctly
|
||||
assert result[1]["type"] == "bash_code_execution_tool_result"
|
||||
assert result[1]["tool_use_id"] == "srvtoolu_01BASH"
|
||||
|
||||
|
||||
def test_anthropic_messages_pt_raw_bash_tool_result_passthrough():
|
||||
"""
|
||||
Test that raw assistant content lists containing bash_code_execution_tool_result
|
||||
blocks are passed through intact to Anthropic.
|
||||
|
||||
Regression: the raw-block passthrough only handled tool_search_tool_result;
|
||||
bash_code_execution_tool_result and other *_tool_result types were silently dropped.
|
||||
"""
|
||||
messages = [
|
||||
{"role": "user", "content": "What is 1+1?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "server_tool_use",
|
||||
"id": "srvtoolu_01BASH",
|
||||
"name": "bash_code_execution",
|
||||
"input": {"command": "python3 -c \"print(1+1)\""},
|
||||
},
|
||||
{
|
||||
"type": "bash_code_execution_tool_result",
|
||||
"tool_use_id": "srvtoolu_01BASH",
|
||||
"content": {
|
||||
"type": "bash_code_execution_result",
|
||||
"stdout": "2\n",
|
||||
"stderr": "",
|
||||
"return_code": 0,
|
||||
"content": [],
|
||||
},
|
||||
},
|
||||
{"type": "text", "text": "The answer is 2."},
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "Thanks!"},
|
||||
]
|
||||
|
||||
result = anthropic_messages_pt(
|
||||
messages, model="claude-sonnet-4-5", llm_provider="anthropic"
|
||||
)
|
||||
|
||||
assistant_msg = next(m for m in result if m["role"] == "assistant")
|
||||
content = assistant_msg["content"]
|
||||
types = [c.get("type") for c in content]
|
||||
|
||||
assert "server_tool_use" in types, "server_tool_use block must be preserved"
|
||||
assert (
|
||||
"bash_code_execution_tool_result" in types
|
||||
), "bash_code_execution_tool_result block must not be dropped"
|
||||
assert "text" in types
|
||||
|
||||
# Result must immediately follow its server_tool_use
|
||||
srv_idx = types.index("server_tool_use")
|
||||
result_idx = types.index("bash_code_execution_tool_result")
|
||||
assert result_idx == srv_idx + 1
|
||||
|
||||
bash_result = next(
|
||||
c for c in content if c.get("type") == "bash_code_execution_tool_result"
|
||||
)
|
||||
assert bash_result["tool_use_id"] == "srvtoolu_01BASH"
|
||||
|
||||
|
||||
def test_anthropic_messages_pt_with_bash_tool_result_in_provider_specific_fields():
|
||||
"""
|
||||
Test that anthropic_messages_pt correctly reconstructs bash_code_execution_tool_result
|
||||
from provider_specific_fields["tool_results"] when replaying LiteLLM response objects.
|
||||
|
||||
Regression: only web_search_results were read from provider_specific_fields;
|
||||
tool_results (bash_code_execution_tool_result, etc.) were silently lost.
|
||||
"""
|
||||
messages = [
|
||||
{"role": "user", "content": "What is 1+1?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "srvtoolu_01BASH",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "bash_code_execution",
|
||||
"arguments": '{"command": "python3 -c \\"print(1+1)\\""}',
|
||||
},
|
||||
}
|
||||
],
|
||||
"provider_specific_fields": {
|
||||
"tool_results": [
|
||||
{
|
||||
"type": "bash_code_execution_tool_result",
|
||||
"tool_use_id": "srvtoolu_01BASH",
|
||||
"content": {
|
||||
"type": "bash_code_execution_result",
|
||||
"stdout": "2\n",
|
||||
"stderr": "",
|
||||
"return_code": 0,
|
||||
"content": [],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
{"role": "user", "content": "Thanks!"},
|
||||
]
|
||||
|
||||
result = anthropic_messages_pt(
|
||||
messages, model="claude-sonnet-4-5", llm_provider="anthropic"
|
||||
)
|
||||
|
||||
assistant_msg = next(m for m in result if m["role"] == "assistant")
|
||||
content = assistant_msg["content"]
|
||||
types = [c.get("type") for c in content]
|
||||
|
||||
assert "server_tool_use" in types, "server_tool_use block must be reconstructed"
|
||||
assert (
|
||||
"bash_code_execution_tool_result" in types
|
||||
), "bash_code_execution_tool_result must be paired from provider_specific_fields['tool_results']"
|
||||
|
||||
# Result must immediately follow its server_tool_use
|
||||
srv_idx = types.index("server_tool_use")
|
||||
result_idx = types.index("bash_code_execution_tool_result")
|
||||
assert result_idx == srv_idx + 1
|
||||
|
||||
srv = next(c for c in content if c.get("type") == "server_tool_use")
|
||||
assert srv["id"] == "srvtoolu_01BASH"
|
||||
bash_result = next(
|
||||
c for c in content if c.get("type") == "bash_code_execution_tool_result"
|
||||
)
|
||||
assert bash_result["tool_use_id"] == "srvtoolu_01BASH"
|
||||
|
||||
|
||||
# ============ parse_tool_call_arguments Tests ============
|
||||
# Tests for the shared utility that parses tool call JSON arguments
|
||||
|
||||
|
||||
Reference in New Issue
Block a user