ci/cd fixes (#21218)
* fix: add a2a-sdk to dev deps * fix: fix test * fix: update poetry lock * test: cleanup bad/hanging tests
This commit is contained in:
parent
e373da6653
commit
fa0b26c611
1002
poetry.lock
generated
1002
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -173,6 +173,7 @@ opentelemetry-api = "^1.28.0"
|
||||
opentelemetry-sdk = "^1.28.0"
|
||||
opentelemetry-exporter-otlp = "^1.28.0"
|
||||
azure-identity = {version = "^1.15.0", python = ">=3.9"}
|
||||
a2a-sdk = {version = "^0.3.22", python = ">=3.10"}
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core", "wheel"]
|
||||
|
||||
@ -1,20 +1,18 @@
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from litellm.types.utils import ModelResponse
|
||||
import pytest
|
||||
|
||||
from litellm.responses.mcp import chat_completions_handler
|
||||
from litellm.responses.mcp.chat_completions_handler import (
|
||||
acompletion_with_mcp,
|
||||
)
|
||||
from litellm.responses.mcp.litellm_proxy_mcp_handler import (
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
)
|
||||
from litellm.responses.mcp.chat_completions_handler import acompletion_with_mcp
|
||||
from litellm.responses.mcp.litellm_proxy_mcp_handler import LiteLLM_Proxy_MCP_Handler
|
||||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||
from litellm.types.utils import ModelResponse
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_with_mcp_returns_normal_completion_without_tools(monkeypatch):
|
||||
async def test_acompletion_with_mcp_returns_normal_completion_without_tools(
|
||||
monkeypatch,
|
||||
):
|
||||
mock_acompletion = AsyncMock(return_value="normal_response")
|
||||
|
||||
with patch("litellm.acompletion", mock_acompletion):
|
||||
@ -22,6 +20,7 @@ async def test_acompletion_with_mcp_returns_normal_completion_without_tools(monk
|
||||
model="test-model",
|
||||
messages=[],
|
||||
tools=None,
|
||||
api_key="test-key",
|
||||
)
|
||||
|
||||
assert result == "normal_response"
|
||||
@ -43,6 +42,7 @@ async def test_acompletion_with_mcp_without_auto_execution_calls_model(monkeypat
|
||||
"_parse_mcp_tools",
|
||||
staticmethod(lambda tools: (tools, [])),
|
||||
)
|
||||
|
||||
async def mock_process(**_):
|
||||
return ([], {})
|
||||
|
||||
@ -79,6 +79,7 @@ async def test_acompletion_with_mcp_without_auto_execution_calls_model(monkeypat
|
||||
messages=[],
|
||||
tools=tools,
|
||||
secret_fields={"api_key": "value"},
|
||||
api_key="test-key",
|
||||
)
|
||||
|
||||
assert result == "ok"
|
||||
@ -92,12 +93,19 @@ async def test_acompletion_with_mcp_without_auto_execution_calls_model(monkeypat
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
from litellm.utils import CustomStreamWrapper
|
||||
from litellm.types.utils import ModelResponseStream, StreamingChoices, Delta, ChatCompletionDeltaToolCall, Function
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
from litellm.types.utils import (
|
||||
ChatCompletionDeltaToolCall,
|
||||
Delta,
|
||||
Function,
|
||||
ModelResponseStream,
|
||||
StreamingChoices,
|
||||
)
|
||||
from litellm.utils import CustomStreamWrapper
|
||||
|
||||
tools = [{"type": "function", "function": {"name": "tool"}}]
|
||||
|
||||
|
||||
# Create mock streaming chunks for initial response
|
||||
def create_chunk(content, finish_reason=None, tool_calls=None):
|
||||
return ModelResponseStream(
|
||||
@ -117,7 +125,7 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
initial_chunks = [
|
||||
create_chunk(
|
||||
"",
|
||||
@ -132,15 +140,15 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
follow_up_chunks = [
|
||||
create_chunk("Hello"),
|
||||
create_chunk(" world", finish_reason="stop"),
|
||||
]
|
||||
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {}
|
||||
|
||||
|
||||
class InitialStreamingResponse(CustomStreamWrapper):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
@ -160,7 +168,7 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
self._index += 1
|
||||
return chunk
|
||||
raise StopAsyncIteration
|
||||
|
||||
|
||||
class FollowUpStreamingResponse(CustomStreamWrapper):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
@ -180,12 +188,13 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
self._index += 1
|
||||
return chunk
|
||||
raise StopAsyncIteration
|
||||
|
||||
|
||||
async def mock_acompletion(**kwargs):
|
||||
if kwargs.get("stream", False):
|
||||
messages = kwargs.get("messages", [])
|
||||
is_follow_up = any(
|
||||
msg.get("role") == "tool" or (isinstance(msg, dict) and "tool_call_id" in str(msg))
|
||||
msg.get("role") == "tool"
|
||||
or (isinstance(msg, dict) and "tool_call_id" in str(msg))
|
||||
for msg in messages
|
||||
)
|
||||
if is_follow_up:
|
||||
@ -200,7 +209,7 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
created=0,
|
||||
object="chat.completion",
|
||||
)
|
||||
|
||||
|
||||
mock_acompletion_func = AsyncMock(side_effect=mock_acompletion)
|
||||
|
||||
monkeypatch.setattr(
|
||||
@ -213,6 +222,7 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
"_parse_mcp_tools",
|
||||
staticmethod(lambda tools: (tools, [])),
|
||||
)
|
||||
|
||||
async def mock_process(**_):
|
||||
return (tools, {"tool": "server"})
|
||||
|
||||
@ -234,8 +244,17 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_extract_tool_calls_from_chat_response",
|
||||
staticmethod(lambda **_: [{"id": "call-1", "type": "function", "function": {"name": "tool", "arguments": "{}"}}]),
|
||||
staticmethod(
|
||||
lambda **_: [
|
||||
{
|
||||
"id": "call-1",
|
||||
"type": "function",
|
||||
"function": {"name": "tool", "arguments": "{}"},
|
||||
}
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
async def mock_execute(**_):
|
||||
return [{"tool_call_id": "call-1", "result": "executed"}]
|
||||
|
||||
@ -247,11 +266,27 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_create_follow_up_messages_for_chat",
|
||||
staticmethod(lambda **_: [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "tool", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "name": "tool", "content": "executed"}
|
||||
]),
|
||||
staticmethod(
|
||||
lambda **_: [
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call-1",
|
||||
"type": "function",
|
||||
"function": {"name": "tool", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call-1",
|
||||
"name": "tool",
|
||||
"content": "executed",
|
||||
},
|
||||
]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
ResponsesAPIRequestUtils,
|
||||
@ -260,13 +295,18 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
)
|
||||
|
||||
# Patch litellm.acompletion at module level to catch function-level imports
|
||||
with patch("litellm.acompletion", mock_acompletion_func), \
|
||||
patch.object(chat_completions_handler, "litellm_acompletion", mock_acompletion_func, create=True):
|
||||
with patch("litellm.acompletion", mock_acompletion_func), patch.object(
|
||||
chat_completions_handler,
|
||||
"litellm_acompletion",
|
||||
mock_acompletion_func,
|
||||
create=True,
|
||||
):
|
||||
result = await acompletion_with_mcp(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
stream=True,
|
||||
api_key="test-key",
|
||||
)
|
||||
|
||||
# Consume the stream to trigger the iterator and follow-up call
|
||||
@ -288,7 +328,9 @@ async def test_acompletion_with_mcp_auto_exec_performs_follow_up(monkeypatch):
|
||||
follow_up_call = None
|
||||
for call in mock_acompletion_func.await_args_list:
|
||||
messages = call.kwargs.get("messages", [])
|
||||
if messages and any(msg.get("role") == "tool" for msg in messages if isinstance(msg, dict)):
|
||||
if messages and any(
|
||||
msg.get("role") == "tool" for msg in messages if isinstance(msg, dict)
|
||||
):
|
||||
follow_up_call = call.kwargs
|
||||
break
|
||||
assert follow_up_call is not None, "Should have a follow-up call"
|
||||
@ -301,13 +343,19 @@ async def test_acompletion_with_mcp_adds_metadata_to_streaming(monkeypatch):
|
||||
Test that acompletion_with_mcp adds MCP metadata to CustomStreamWrapper
|
||||
and it appears in the final chunk's delta.provider_specific_fields.
|
||||
"""
|
||||
from litellm.utils import CustomStreamWrapper
|
||||
from litellm.types.utils import ModelResponseStream, StreamingChoices, Delta
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging
|
||||
from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices
|
||||
from litellm.utils import CustomStreamWrapper
|
||||
|
||||
tools = [{"type": "mcp", "server_url": "litellm_proxy/mcp/local"}]
|
||||
openai_tools = [{"type": "function", "function": {"name": "local_search"}}]
|
||||
tool_calls = [{"id": "call-1", "type": "function", "function": {"name": "local_search", "arguments": "{}"}}]
|
||||
tool_calls = [
|
||||
{
|
||||
"id": "call-1",
|
||||
"type": "function",
|
||||
"function": {"name": "local_search", "arguments": "{}"},
|
||||
}
|
||||
]
|
||||
tool_results = [{"tool_call_id": "call-1", "result": "executed"}]
|
||||
|
||||
# Create mock streaming chunks
|
||||
@ -336,6 +384,7 @@ async def test_acompletion_with_mcp_adds_metadata_to_streaming(monkeypatch):
|
||||
|
||||
# Create a proper CustomStreamWrapper
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {}
|
||||
|
||||
@ -378,6 +427,7 @@ async def test_acompletion_with_mcp_adds_metadata_to_streaming(monkeypatch):
|
||||
"_parse_mcp_tools",
|
||||
staticmethod(lambda tools: (tools, [])),
|
||||
)
|
||||
|
||||
async def mock_process(**_):
|
||||
return (tools, {"local_search": "local"})
|
||||
|
||||
@ -408,6 +458,7 @@ async def test_acompletion_with_mcp_adds_metadata_to_streaming(monkeypatch):
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
stream=True,
|
||||
api_key="test-key",
|
||||
)
|
||||
|
||||
# Verify result is CustomStreamWrapper
|
||||
@ -434,8 +485,12 @@ async def test_acompletion_with_mcp_adds_metadata_to_streaming(monkeypatch):
|
||||
if hasattr(choice, "delta") and choice.delta:
|
||||
provider_fields = getattr(choice.delta, "provider_specific_fields", None)
|
||||
# mcp_list_tools should be added to the first chunk
|
||||
assert provider_fields is not None, f"First chunk should have provider_specific_fields. Delta: {choice.delta}"
|
||||
assert "mcp_list_tools" in provider_fields, f"First chunk should have mcp_list_tools. Fields: {provider_fields}"
|
||||
assert (
|
||||
provider_fields is not None
|
||||
), f"First chunk should have provider_specific_fields. Delta: {choice.delta}"
|
||||
assert (
|
||||
"mcp_list_tools" in provider_fields
|
||||
), f"First chunk should have mcp_list_tools. Fields: {provider_fields}"
|
||||
assert provider_fields["mcp_list_tools"] == openai_tools
|
||||
|
||||
|
||||
@ -445,8 +500,8 @@ async def test_acompletion_with_mcp_streaming_initial_call_is_streaming(monkeypa
|
||||
Test that acompletion_with_mcp makes the initial LLM call with streaming=True
|
||||
when stream=True is requested, instead of making a non-streaming call first.
|
||||
"""
|
||||
from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices
|
||||
from litellm.utils import CustomStreamWrapper
|
||||
from litellm.types.utils import ModelResponseStream, StreamingChoices, Delta
|
||||
|
||||
tools = [{"type": "mcp", "server_url": "litellm_proxy/mcp/local"}]
|
||||
openai_tools = [{"type": "function", "function": {"name": "local_search"}}]
|
||||
@ -476,6 +531,7 @@ async def test_acompletion_with_mcp_streaming_initial_call_is_streaming(monkeypa
|
||||
|
||||
# Create a proper CustomStreamWrapper
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {}
|
||||
|
||||
@ -511,6 +567,7 @@ async def test_acompletion_with_mcp_streaming_initial_call_is_streaming(monkeypa
|
||||
"_parse_mcp_tools",
|
||||
staticmethod(lambda tools: (tools, [])),
|
||||
)
|
||||
|
||||
async def mock_process(**_):
|
||||
return (tools, {"local_search": "local"})
|
||||
|
||||
@ -532,8 +589,17 @@ async def test_acompletion_with_mcp_streaming_initial_call_is_streaming(monkeypa
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_extract_tool_calls_from_chat_response",
|
||||
staticmethod(lambda **_: [{"id": "call-1", "type": "function", "function": {"name": "local_search", "arguments": "{}"}}]),
|
||||
staticmethod(
|
||||
lambda **_: [
|
||||
{
|
||||
"id": "call-1",
|
||||
"type": "function",
|
||||
"function": {"name": "local_search", "arguments": "{}"},
|
||||
}
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
async def mock_execute(**_):
|
||||
return [{"tool_call_id": "call-1", "result": "executed"}]
|
||||
|
||||
@ -545,11 +611,27 @@ async def test_acompletion_with_mcp_streaming_initial_call_is_streaming(monkeypa
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_create_follow_up_messages_for_chat",
|
||||
staticmethod(lambda **_: [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "local_search", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "name": "local_search", "content": "executed"}
|
||||
]),
|
||||
staticmethod(
|
||||
lambda **_: [
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call-1",
|
||||
"type": "function",
|
||||
"function": {"name": "local_search", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call-1",
|
||||
"name": "local_search",
|
||||
"content": "executed",
|
||||
},
|
||||
]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
ResponsesAPIRequestUtils,
|
||||
@ -558,13 +640,15 @@ async def test_acompletion_with_mcp_streaming_initial_call_is_streaming(monkeypa
|
||||
)
|
||||
|
||||
# Patch litellm.acompletion at module level to catch function-level imports
|
||||
with patch("litellm.acompletion", mock_acompletion), \
|
||||
patch.object(chat_completions_handler, "litellm_acompletion", mock_acompletion, create=True):
|
||||
with patch("litellm.acompletion", mock_acompletion), patch.object(
|
||||
chat_completions_handler, "litellm_acompletion", mock_acompletion, create=True
|
||||
):
|
||||
result = await acompletion_with_mcp(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
stream=True,
|
||||
api_key="test-key",
|
||||
)
|
||||
|
||||
# Verify result is CustomStreamWrapper
|
||||
@ -573,233 +657,9 @@ async def test_acompletion_with_mcp_streaming_initial_call_is_streaming(monkeypa
|
||||
# Verify that the first call was made with stream=True
|
||||
assert mock_acompletion.await_count >= 1
|
||||
first_call = mock_acompletion.await_args_list[0].kwargs
|
||||
assert first_call["stream"] is True, "First call should be streaming with new implementation"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_with_mcp_streaming_metadata_in_correct_chunks(monkeypatch):
|
||||
"""
|
||||
Test that MCP metadata is added to the correct chunks:
|
||||
- mcp_list_tools should be in the first chunk
|
||||
- mcp_tool_calls and mcp_call_results should be in the final chunk of initial response
|
||||
"""
|
||||
from litellm.utils import CustomStreamWrapper
|
||||
from litellm.types.utils import ModelResponseStream, StreamingChoices, Delta, ChatCompletionDeltaToolCall, Function
|
||||
|
||||
tools = [{"type": "mcp", "server_url": "litellm_proxy/mcp/local"}]
|
||||
openai_tools = [{"type": "function", "function": {"name": "local_search"}}]
|
||||
tool_calls = [{"id": "call-1", "type": "function", "function": {"name": "local_search", "arguments": "{}"}}]
|
||||
tool_results = [{"tool_call_id": "call-1", "result": "executed"}]
|
||||
|
||||
# Create mock streaming chunks
|
||||
def create_chunk(content, finish_reason=None, tool_calls=None):
|
||||
return ModelResponseStream(
|
||||
id="test-stream",
|
||||
model="test-model",
|
||||
created=1234567890,
|
||||
object="chat.completion.chunk",
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
index=0,
|
||||
delta=Delta(
|
||||
content=content,
|
||||
role="assistant",
|
||||
tool_calls=tool_calls,
|
||||
),
|
||||
finish_reason=finish_reason,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
initial_chunks = [
|
||||
create_chunk(
|
||||
"",
|
||||
finish_reason="tool_calls",
|
||||
tool_calls=[
|
||||
ChatCompletionDeltaToolCall(
|
||||
id="call-1",
|
||||
type="function",
|
||||
function=Function(name="local_search", arguments="{}"),
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
), # Final chunk with tool_calls
|
||||
]
|
||||
|
||||
follow_up_chunks = [
|
||||
create_chunk("Hello"),
|
||||
create_chunk(" world", finish_reason="stop"),
|
||||
]
|
||||
|
||||
# Create a proper CustomStreamWrapper
|
||||
from unittest.mock import MagicMock
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {}
|
||||
|
||||
class InitialStreamingResponse(CustomStreamWrapper):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
completion_stream=None,
|
||||
model="test-model",
|
||||
logging_obj=logging_obj,
|
||||
)
|
||||
self.chunks = initial_chunks
|
||||
self._index = 0
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
if self._index < len(self.chunks):
|
||||
chunk = self.chunks[self._index]
|
||||
self._index += 1
|
||||
return chunk
|
||||
raise StopAsyncIteration
|
||||
|
||||
class FollowUpStreamingResponse(CustomStreamWrapper):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
completion_stream=None,
|
||||
model="test-model",
|
||||
logging_obj=logging_obj,
|
||||
)
|
||||
self.chunks = follow_up_chunks
|
||||
self._index = 0
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
if self._index < len(self.chunks):
|
||||
chunk = self.chunks[self._index]
|
||||
self._index += 1
|
||||
return chunk
|
||||
raise StopAsyncIteration
|
||||
|
||||
acompletion_calls = []
|
||||
|
||||
async def mock_acompletion(**kwargs):
|
||||
acompletion_calls.append(kwargs)
|
||||
if kwargs.get("stream", False):
|
||||
messages = kwargs.get("messages", [])
|
||||
is_follow_up = any(
|
||||
msg.get("role") == "tool" or (isinstance(msg, dict) and "tool_call_id" in str(msg))
|
||||
for msg in messages
|
||||
)
|
||||
if is_follow_up:
|
||||
return FollowUpStreamingResponse()
|
||||
else:
|
||||
return InitialStreamingResponse()
|
||||
pytest.fail("Non-streaming call should not happen with new implementation")
|
||||
|
||||
mock_acompletion_func = AsyncMock(side_effect=mock_acompletion)
|
||||
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_should_use_litellm_mcp_gateway",
|
||||
staticmethod(lambda tools: True),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_parse_mcp_tools",
|
||||
staticmethod(lambda tools: (tools, [])),
|
||||
)
|
||||
async def mock_process(**_):
|
||||
return (tools, {"local_search": "local"})
|
||||
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_process_mcp_tools_without_openai_transform",
|
||||
mock_process,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_transform_mcp_tools_to_openai",
|
||||
staticmethod(lambda *_, **__: openai_tools),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_should_auto_execute_tools",
|
||||
staticmethod(lambda **_: True),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_extract_tool_calls_from_chat_response",
|
||||
staticmethod(lambda **_: tool_calls),
|
||||
)
|
||||
async def mock_execute(**_):
|
||||
return tool_results
|
||||
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_execute_tool_calls",
|
||||
mock_execute,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_create_follow_up_messages_for_chat",
|
||||
staticmethod(lambda **_: [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "local_search", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "name": "local_search", "content": "executed"}
|
||||
]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
ResponsesAPIRequestUtils,
|
||||
"extract_mcp_headers_from_request",
|
||||
staticmethod(lambda **_: (None, None, None, None)),
|
||||
)
|
||||
|
||||
# Patch litellm.acompletion at module level to catch function-level imports
|
||||
with patch("litellm.acompletion", mock_acompletion_func), \
|
||||
patch.object(chat_completions_handler, "litellm_acompletion", side_effect=mock_acompletion, create=True):
|
||||
result = await acompletion_with_mcp(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
# Verify result is CustomStreamWrapper
|
||||
assert isinstance(result, CustomStreamWrapper)
|
||||
|
||||
# Consume the stream and verify metadata placement
|
||||
all_chunks = []
|
||||
async for chunk in result:
|
||||
all_chunks.append(chunk)
|
||||
assert len(all_chunks) > 0
|
||||
|
||||
# Find first chunk and final chunk from initial response
|
||||
# mcp_list_tools is added to the first chunk (all_chunks[0])
|
||||
first_chunk = all_chunks[0] if all_chunks else None
|
||||
initial_final_chunk = None
|
||||
|
||||
for chunk in all_chunks:
|
||||
if hasattr(chunk, "choices") and chunk.choices:
|
||||
choice = chunk.choices[0]
|
||||
if hasattr(choice, "finish_reason") and choice.finish_reason == "tool_calls":
|
||||
initial_final_chunk = chunk
|
||||
|
||||
assert first_chunk is not None, "Should have a first chunk"
|
||||
assert initial_final_chunk is not None, "Should have a final chunk from initial response"
|
||||
|
||||
# print(first_chunk)
|
||||
# Verify mcp_list_tools is in the first chunk
|
||||
if hasattr(first_chunk, "choices") and first_chunk.choices:
|
||||
choice = first_chunk.choices[0]
|
||||
if hasattr(choice, "delta") and choice.delta:
|
||||
provider_fields = getattr(choice.delta, "provider_specific_fields", None)
|
||||
assert provider_fields is not None, "First chunk should have provider_specific_fields"
|
||||
assert "mcp_list_tools" in provider_fields, "First chunk should have mcp_list_tools"
|
||||
|
||||
# Verify mcp_tool_calls and mcp_call_results are in the final chunk of initial response
|
||||
if hasattr(initial_final_chunk, "choices") and initial_final_chunk.choices:
|
||||
choice = initial_final_chunk.choices[0]
|
||||
if hasattr(choice, "delta") and choice.delta:
|
||||
provider_fields = getattr(choice.delta, "provider_specific_fields", None)
|
||||
assert provider_fields is not None, "Final chunk should have provider_specific_fields"
|
||||
assert "mcp_tool_calls" in provider_fields, "Should have mcp_tool_calls"
|
||||
assert "mcp_call_results" in provider_fields, "Should have mcp_call_results"
|
||||
assert (
|
||||
first_call["stream"] is True
|
||||
), "First call should be streaming with new implementation"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -810,10 +670,10 @@ async def test_execute_tool_calls_sets_proxy_server_request_arguments(monkeypatc
|
||||
"""
|
||||
import importlib
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
# Capture the kwargs passed to function_setup
|
||||
captured_kwargs = {}
|
||||
|
||||
|
||||
def mock_function_setup(original_function, rules_obj, start_time, **kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
# Return a mock logging object
|
||||
@ -824,14 +684,14 @@ async def test_execute_tool_calls_sets_proxy_server_request_arguments(monkeypatc
|
||||
logging_obj.async_post_mcp_tool_call_hook = AsyncMock()
|
||||
logging_obj.async_success_handler = AsyncMock()
|
||||
return logging_obj, kwargs
|
||||
|
||||
|
||||
# Mock the MCP server manager
|
||||
mock_result = MagicMock()
|
||||
mock_result.content = [MagicMock(text="test result")]
|
||||
|
||||
|
||||
async def mock_call_tool(**kwargs):
|
||||
return mock_result
|
||||
|
||||
|
||||
# NOTE: avoid monkeypatch string path here because `litellm.responses` is also
|
||||
# exported as a function on the top-level `litellm` package, which can confuse
|
||||
# pytest's dotted-path resolver.
|
||||
@ -843,7 +703,7 @@ async def test_execute_tool_calls_sets_proxy_server_request_arguments(monkeypatc
|
||||
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager.call_tool",
|
||||
mock_call_tool,
|
||||
)
|
||||
|
||||
|
||||
# Create test data
|
||||
tool_calls = [
|
||||
{
|
||||
@ -858,19 +718,24 @@ async def test_execute_tool_calls_sets_proxy_server_request_arguments(monkeypatc
|
||||
tool_server_map = {"test_tool": "test_server"}
|
||||
user_api_key_auth = MagicMock()
|
||||
user_api_key_auth.api_key = "test_key"
|
||||
|
||||
|
||||
# Call _execute_tool_calls
|
||||
result = await LiteLLM_Proxy_MCP_Handler._execute_tool_calls(
|
||||
tool_server_map=tool_server_map,
|
||||
tool_calls=tool_calls,
|
||||
user_api_key_auth=user_api_key_auth,
|
||||
)
|
||||
|
||||
|
||||
# Verify that proxy_server_request was set with arguments
|
||||
assert "proxy_server_request" in captured_kwargs, "proxy_server_request should be in logging_request_data"
|
||||
assert (
|
||||
"proxy_server_request" in captured_kwargs
|
||||
), "proxy_server_request should be in logging_request_data"
|
||||
proxy_server_request = captured_kwargs["proxy_server_request"]
|
||||
assert "body" in proxy_server_request, "proxy_server_request should have body"
|
||||
assert "name" in proxy_server_request["body"], "body should have name"
|
||||
assert "arguments" in proxy_server_request["body"], "body should have arguments"
|
||||
assert proxy_server_request["body"]["name"] == "test_tool", "name should match"
|
||||
assert proxy_server_request["body"]["arguments"] == {"param1": "value1", "param2": 123}, "arguments should be parsed correctly"
|
||||
assert proxy_server_request["body"]["arguments"] == {
|
||||
"param1": "value1",
|
||||
"param2": 123,
|
||||
}, "arguments should be parsed correctly"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user