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
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ def _build_tool_result_message(tool_results: Sequence[Mapping[str, object]]) ->
"""Turn executed tool results into the user message Anthropic expects."""
return AnthropicMessagesUserMessageParam(
role="user",
content=tuple(
content=[
AnthropicMessagesToolResultParam(
type="tool_result",
tool_use_id=str(result.get("tool_call_id") or ""),
content=str(result.get("result") or ""),
)
for result in tool_results
),
],
)


Expand Down
11 changes: 0 additions & 11 deletions tests/integration/mcp/test_mcp_llm_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,16 +258,6 @@ def _peer_add_calls(peer: McpPeer) -> tuple[dict[str, object], ...]:
)


def _skip_if_bridge_drops_tool_result(
rig: Rig, requests: tuple[tuple[str, ...], ...], calls: tuple[object, ...]
) -> None:
if rig.surface == "messages_bridge" and len(calls) > 1 and len(requests) > 2:
pytest.skip(
"BUG: /v1/messages MCP tool loop over a non-Anthropic model drops the tool_result message, "
"so the tool is re-executed until the iteration cap"
)


@pytest.mark.parametrize("surface", SURFACES)
def test_auto_approved_gateway_tool_is_listed_executed_once_and_fed_back(gateway: Gateway, surface: Surface) -> None:
with _rig(gateway, surface) as rig:
Expand All @@ -276,7 +266,6 @@ def test_auto_approved_gateway_tool_is_listed_executed_once_and_fed_back(gateway
assert response.status_code == 200, response.text
calls: Final = _peer_add_calls(rig.peer)
requests: Final = rig.upstream_tools()
_skip_if_bridge_drops_tool_result(rig, requests, calls)
assert [call["body"]["params"]["name"] for call in calls] == ["add"], calls
assert calls[0]["body"]["params"]["arguments"] == ADD, calls
assert len(requests) == 2, requests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

import pytest


from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
LiteLLMAnthropicMessagesAdapter,
)
from litellm.llms.anthropic.experimental_pass_through.messages.handler import (
anthropic_messages_handler,
)
Expand Down Expand Up @@ -59,7 +61,7 @@ def test_anthropic_messages_handler_skips_the_gateway_on_recursion():
"litellm.llms.anthropic.experimental_pass_through.messages.mcp_handler.anthropic_messages_with_mcp",
new=AsyncMock(return_value={"routed": True}),
) as routed:
with pytest.raises(ValueError, match='anthropic_messages_handler is not implemented for sync calls'):
with pytest.raises(ValueError, match="anthropic_messages_handler is not implemented for sync calls"):
anthropic_messages_handler(
max_tokens=100,
messages=[{"role": "user", "content": "hi"}],
Expand All @@ -78,7 +80,7 @@ def test_anthropic_messages_handler_leaves_native_tools_alone():
"litellm.llms.anthropic.experimental_pass_through.messages.mcp_handler.anthropic_messages_with_mcp",
new=AsyncMock(return_value={"routed": True}),
) as routed:
with pytest.raises(ValueError, match='anthropic_messages_handler is not implemented for sync calls'):
with pytest.raises(ValueError, match="anthropic_messages_handler is not implemented for sync calls"):
anthropic_messages_handler(
max_tokens=100,
messages=[{"role": "user", "content": "hi"}],
Expand Down Expand Up @@ -115,8 +117,31 @@ def test_build_tool_result_message_uses_anthropic_tool_result_blocks():
message = _build_tool_result_message([{"tool_call_id": "toolu_1", "result": "9 sections", "name": "read_wiki"}])

assert message["role"] == "user"
assert list(message["content"]) == [
{"type": "tool_result", "tool_use_id": "toolu_1", "content": "9 sections"}
assert message["content"] == [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "9 sections"}]


def test_build_tool_result_message_survives_the_chat_completions_bridge():
"""
Regression test (LIT-8474): a non-Anthropic model behind /v1/messages must see
the executed tool result as a role="tool" message keyed by the tool_call_id.

The bridge only translates list content, so a tuple-shaped user message was
dropped and the model re-requested the tool until the iteration cap.
"""
message = _build_tool_result_message(
[
{"tool_call_id": "call_1", "result": "5", "name": "add"},
{"tool_call_id": "call_2", "result": "7", "name": "add"},
]
)

translated = LiteLLMAnthropicMessagesAdapter().translate_anthropic_messages_to_openai(
[message], model="hosted_vllm/gpt-4o-mini", custom_llm_provider="hosted_vllm"
)

assert translated == [
{"role": "tool", "tool_call_id": "call_1", "content": "5"},
{"role": "tool", "tool_call_id": "call_2", "content": "7"},
]


Expand Down Expand Up @@ -157,19 +182,23 @@ async def test_anthropic_messages_with_mcp_forwards_the_callers_mcp_credentials(
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "done"}]},
]

with patch.object(MCPRequestContext, "resolve", return_value=context), patch.object(
mcp_handler.LiteLLM_Proxy_MCP_Handler
if hasattr(mcp_handler, "LiteLLM_Proxy_MCP_Handler")
else __import__(
"litellm.responses.mcp.litellm_proxy_mcp_handler", fromlist=["LiteLLM_Proxy_MCP_Handler"]
).LiteLLM_Proxy_MCP_Handler,
"_process_mcp_tools_without_openai_transform",
new=process,
), patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler, "_execute_tool_calls",
new=execute,
), patch(
"litellm.anthropic_messages", new=AsyncMock(side_effect=responses)
with (
patch.object(MCPRequestContext, "resolve", return_value=context),
patch.object(
mcp_handler.LiteLLM_Proxy_MCP_Handler
if hasattr(mcp_handler, "LiteLLM_Proxy_MCP_Handler")
else __import__(
"litellm.responses.mcp.litellm_proxy_mcp_handler", fromlist=["LiteLLM_Proxy_MCP_Handler"]
).LiteLLM_Proxy_MCP_Handler,
"_process_mcp_tools_without_openai_transform",
new=process,
),
patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler,
"_execute_tool_calls",
new=execute,
),
patch("litellm.anthropic_messages", new=AsyncMock(side_effect=responses)),
):
await mcp_handler.anthropic_messages_with_mcp(
max_tokens=100,
Expand Down Expand Up @@ -220,16 +249,19 @@ async def test_anthropic_messages_with_mcp_stops_when_every_tool_call_is_skipped
}
anthropic_messages_mock = AsyncMock(return_value=tool_use_response)

with patch.object(
MCPRequestContext, "resolve", return_value=MCPRequestContext(user_api_key_auth="auth")
), patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler, "_process_mcp_tools_without_openai_transform",
new=AsyncMock(return_value=([], {})),
), patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler, "_execute_tool_calls",
new=AsyncMock(return_value=[]),
), patch(
"litellm.anthropic_messages", new=anthropic_messages_mock
with (
patch.object(MCPRequestContext, "resolve", return_value=MCPRequestContext(user_api_key_auth="auth")),
patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler,
"_process_mcp_tools_without_openai_transform",
new=AsyncMock(return_value=([], {})),
),
patch.object(
import_module("litellm.responses.mcp.litellm_proxy_mcp_handler").LiteLLM_Proxy_MCP_Handler,
"_execute_tool_calls",
new=AsyncMock(return_value=[]),
),
patch("litellm.anthropic_messages", new=anthropic_messages_mock),
):
result = await mcp_handler.anthropic_messages_with_mcp(
max_tokens=100,
Expand Down
Loading