mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(anthropic): preserve MCP tool results in the non-Anthropic Messages bridge (#42783)
The tool_result user message built by the /v1/messages MCP loop used tuple content, which the Messages to Chat Completions adapter silently dropped, so non-Anthropic models re-requested the tool until the iteration cap or the provider rejected the follow-up. Emit list content so the existing tool_result branch translates it into a role tool message keyed by tool_call_id. Resolves LIT-8474 Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: bot_apk <apk@cognition.ai>
This commit is contained in:
parent
e0af9917a1
commit
5db8543817
3 changed files with 62 additions and 41 deletions
|
|
@ -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
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ from unittest.mock import AsyncMock, patch
|
|||
|
||||
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,
|
||||
)
|
||||
|
|
@ -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"}],
|
||||
|
|
@ -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"}],
|
||||
|
|
@ -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"},
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue