Skip to content

Commit d01adf5

Browse files
authored
Merge pull request #21484 from BerriAI/fix/mcp-test-isolation
fix(tests): resolve MCP test isolation failures in parallel execution
2 parents b29ee63 + 7b6ffbb commit d01adf5

File tree

6 files changed

+120
-86
lines changed

6 files changed

+120
-86
lines changed

tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import os
2-
from unittest.mock import patch
2+
from unittest.mock import MagicMock, patch
33

44
from litellm.integrations.langfuse.langfuse_prompt_management import (
55
LangfusePromptManagement,
66
)
77

88

99
class TestLangfusePromptManagement:
10+
def setup_method(self):
11+
# Mock langfuse package to avoid triggering real import.
12+
# The real langfuse import fails on Python 3.14 due to pydantic v1 incompatibility.
13+
# This also prevents test-ordering issues when earlier tests remove sys.modules["langfuse"].
14+
self._mock_langfuse = MagicMock()
15+
self._mock_langfuse.version.__version__ = "3.0.0"
16+
self._langfuse_patcher = patch.dict(
17+
"sys.modules", {"langfuse": self._mock_langfuse}
18+
)
19+
self._langfuse_patcher.start()
20+
21+
def teardown_method(self):
22+
self._langfuse_patcher.stop()
23+
1024
def test_get_prompt_from_id(self):
1125
langfuse_prompt_management = LangfusePromptManagement()
1226
with patch.object(

tests/test_litellm/integrations/test_langfuse.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,15 @@ def test_max_langfuse_clients_limit():
472472
"""
473473
Test that the max langfuse clients limit is respected when initializing multiple clients
474474
"""
475+
# Mock langfuse package to avoid triggering real import.
476+
# The real langfuse import fails on Python 3.14 due to pydantic v1 incompatibility,
477+
# and sys.modules["langfuse"] may be absent after other tests in the suite clean up.
478+
mock_langfuse = MagicMock()
479+
mock_langfuse.version.__version__ = "3.0.0"
475480
# Set max clients to 2 for testing
476-
with patch.object(langfuse_module, "MAX_LANGFUSE_INITIALIZED_CLIENTS", 2):
481+
with patch.dict("sys.modules", {"langfuse": mock_langfuse}), patch.object(
482+
langfuse_module, "MAX_LANGFUSE_INITIALIZED_CLIENTS", 2
483+
):
477484
# Reset the counter
478485
litellm.initialized_langfuse_clients = 0
479486

tests/test_litellm/integrations/test_langfuse_otel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def add_metadata_from_header(litellm_params, metadata):
161161
# Use monkeypatch so the real module is restored after the test runs,
162162
# preventing sys.modules corruption that would break patch() targets in
163163
# later tests (the patch would hit the stub while the real module's
164-
# globals remain unpatch-ed).
164+
# globals remain unpatched).
165165
monkeypatch.setitem(sys.modules, "litellm.integrations.langfuse.langfuse", stub_module) # type: ignore
166166

167167
kwargs = {"litellm_params": {"metadata": {"foo": "bar"}}}

tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_debug.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ async def mock_send(message):
230230
)
231231

232232
message = {"type": "http.response.start", "status": 200, "headers": []}
233-
asyncio.get_event_loop().run_until_complete(wrapped(message))
233+
asyncio.run(wrapped(message))
234234

235235
assert len(captured) == 1
236236
headers = dict(captured[0]["headers"])
@@ -247,6 +247,6 @@ async def mock_send(message):
247247
)
248248

249249
body_msg = {"type": "http.response.body", "body": b"hello"}
250-
asyncio.get_event_loop().run_until_complete(wrapped(body_msg))
250+
asyncio.run(wrapped(body_msg))
251251

252252
assert captured[0] == body_msg

tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,16 @@ def _reload_mcp_manager_module():
3939
"litellm.proxy._experimental.mcp_server.mcp_server_manager"
4040
]
4141
importlib.reload(utils_module)
42-
return importlib.reload(manager_module)
42+
reloaded = importlib.reload(manager_module)
43+
# After reload, server.py still holds a stale reference to the old
44+
# global_mcp_server_manager. Update it so tests that exercise server.py
45+
# functions (e.g. _get_tools_from_mcp_servers) use the fresh instance.
46+
server_module = sys.modules.get(
47+
"litellm.proxy._experimental.mcp_server.server"
48+
)
49+
if server_module is not None and hasattr(server_module, "global_mcp_server_manager"):
50+
server_module.global_mcp_server_manager = reloaded.global_mcp_server_manager
51+
return reloaded
4352

4453

4554
class TestMCPServerManager:

tests/test_litellm/proxy/test_litellm_pre_call_utils.py

Lines changed: 84 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,91 +1014,95 @@ async def test_add_litellm_metadata_from_request_headers():
10141014
# Set up test logger
10151015
litellm._turn_on_debug()
10161016
test_logger = TestCustomLogger()
1017+
original_callbacks = litellm.callbacks
10171018
litellm.callbacks = [test_logger]
10181019

1019-
# Prepare test data (ensure no streaming, add mock_response and api_key to route to litellm.acompletion)
1020-
headers = {"x-litellm-spend-logs-metadata": '{"user_id": "12345", "project_id": "proj_abc", "request_type": "chat_completion", "timestamp": "2025-09-02T10:30:00Z"}'}
1021-
data = {"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}], "stream": False, "mock_response": "Hi", "api_key": "fake-key"}
1022-
1023-
# Create mock request with headers
1024-
mock_request = MagicMock(spec=Request)
1025-
mock_request.headers = headers
1026-
mock_request.url.path = "/chat/completions"
1027-
1028-
# Create mock response
1029-
mock_fastapi_response = MagicMock(spec=Response)
1030-
1031-
# Create mock user API key dict
1032-
mock_user_api_key_dict = UserAPIKeyAuth(
1033-
api_key="test-key",
1034-
user_id="test-user",
1035-
org_id="test-org"
1036-
)
1037-
1038-
# Create mock proxy logging object
1039-
mock_proxy_logging_obj = MagicMock(spec=ProxyLogging)
1040-
1041-
# Create async functions for the hooks
1042-
async def mock_during_call_hook(*args, **kwargs):
1043-
return None
1044-
1045-
async def mock_pre_call_hook(*args, **kwargs):
1046-
return data
1047-
1048-
async def mock_post_call_success_hook(*args, **kwargs):
1049-
# Return the response unchanged
1050-
return kwargs.get('response', args[2] if len(args) > 2 else None)
1051-
1052-
mock_proxy_logging_obj.during_call_hook = mock_during_call_hook
1053-
mock_proxy_logging_obj.pre_call_hook = mock_pre_call_hook
1054-
mock_proxy_logging_obj.post_call_success_hook = mock_post_call_success_hook
1055-
1056-
# Create mock proxy config
1057-
mock_proxy_config = MagicMock()
1058-
1059-
# Create mock general settings
1060-
general_settings = {}
1061-
1062-
# Create mock select_data_generator with correct signature
1063-
def mock_select_data_generator(response=None, user_api_key_dict=None, request_data=None):
1064-
async def mock_generator():
1065-
yield "data: " + json.dumps({"choices": [{"delta": {"content": "Hello"}}]}) + "\n\n"
1066-
yield "data: [DONE]\n\n"
1067-
return mock_generator()
1068-
1069-
# Create the processor
1070-
processor = ProxyBaseLLMRequestProcessing(data=data)
1071-
1072-
# Call base_process_llm_request (it will use the mock_response="Hi" parameter)
1073-
result = await processor.base_process_llm_request(
1074-
request=mock_request,
1075-
fastapi_response=mock_fastapi_response,
1076-
user_api_key_dict=mock_user_api_key_dict,
1077-
route_type="acompletion",
1078-
proxy_logging_obj=mock_proxy_logging_obj,
1079-
general_settings=general_settings,
1080-
proxy_config=mock_proxy_config,
1081-
select_data_generator=mock_select_data_generator,
1082-
llm_router=None,
1083-
model="gpt-4",
1084-
is_streaming_request=False
1085-
)
1086-
1087-
# Sleep for 3 seconds to allow logging to complete
1088-
await asyncio.sleep(3)
1089-
1090-
# Check if standard_logging_object was set
1091-
assert test_logger.standard_logging_object is not None, "standard_logging_object should be populated after LLM request"
1092-
1093-
# Verify the logging object contains expected metadata
1094-
standard_logging_obj = test_logger.standard_logging_object
1020+
try:
1021+
# Prepare test data (ensure no streaming, add mock_response and api_key to route to litellm.acompletion)
1022+
headers = {"x-litellm-spend-logs-metadata": '{"user_id": "12345", "project_id": "proj_abc", "request_type": "chat_completion", "timestamp": "2025-09-02T10:30:00Z"}'}
1023+
data = {"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}], "stream": False, "mock_response": "Hi", "api_key": "fake-key"}
1024+
1025+
# Create mock request with headers
1026+
mock_request = MagicMock(spec=Request)
1027+
mock_request.headers = headers
1028+
mock_request.url.path = "/chat/completions"
1029+
1030+
# Create mock response
1031+
mock_fastapi_response = MagicMock(spec=Response)
1032+
1033+
# Create mock user API key dict
1034+
mock_user_api_key_dict = UserAPIKeyAuth(
1035+
api_key="test-key",
1036+
user_id="test-user",
1037+
org_id="test-org"
1038+
)
10951039

1096-
print(f"Standard logging object captured: {json.dumps(standard_logging_obj, indent=4, default=str)}")
1040+
# Create mock proxy logging object
1041+
mock_proxy_logging_obj = MagicMock(spec=ProxyLogging)
1042+
1043+
# Create async functions for the hooks
1044+
async def mock_during_call_hook(*args, **kwargs):
1045+
return None
1046+
1047+
async def mock_pre_call_hook(*args, **kwargs):
1048+
return data
1049+
1050+
async def mock_post_call_success_hook(*args, **kwargs):
1051+
# Return the response unchanged
1052+
return kwargs.get('response', args[2] if len(args) > 2 else None)
1053+
1054+
mock_proxy_logging_obj.during_call_hook = mock_during_call_hook
1055+
mock_proxy_logging_obj.pre_call_hook = mock_pre_call_hook
1056+
mock_proxy_logging_obj.post_call_success_hook = mock_post_call_success_hook
1057+
1058+
# Create mock proxy config
1059+
mock_proxy_config = MagicMock()
1060+
1061+
# Create mock general settings
1062+
general_settings = {}
1063+
1064+
# Create mock select_data_generator with correct signature
1065+
def mock_select_data_generator(response=None, user_api_key_dict=None, request_data=None):
1066+
async def mock_generator():
1067+
yield "data: " + json.dumps({"choices": [{"delta": {"content": "Hello"}}]}) + "\n\n"
1068+
yield "data: [DONE]\n\n"
1069+
return mock_generator()
1070+
1071+
# Create the processor
1072+
processor = ProxyBaseLLMRequestProcessing(data=data)
1073+
1074+
# Call base_process_llm_request (it will use the mock_response="Hi" parameter)
1075+
result = await processor.base_process_llm_request(
1076+
request=mock_request,
1077+
fastapi_response=mock_fastapi_response,
1078+
user_api_key_dict=mock_user_api_key_dict,
1079+
route_type="acompletion",
1080+
proxy_logging_obj=mock_proxy_logging_obj,
1081+
general_settings=general_settings,
1082+
proxy_config=mock_proxy_config,
1083+
select_data_generator=mock_select_data_generator,
1084+
llm_router=None,
1085+
model="gpt-4",
1086+
is_streaming_request=False
1087+
)
1088+
1089+
# Sleep for 3 seconds to allow logging to complete
1090+
await asyncio.sleep(3)
1091+
1092+
# Check if standard_logging_object was set
1093+
assert test_logger.standard_logging_object is not None, "standard_logging_object should be populated after LLM request"
1094+
1095+
# Verify the logging object contains expected metadata
1096+
standard_logging_obj = test_logger.standard_logging_object
1097+
1098+
print(f"Standard logging object captured: {json.dumps(standard_logging_obj, indent=4, default=str)}")
1099+
1100+
SPEND_LOGS_METADATA = standard_logging_obj["metadata"]["spend_logs_metadata"]
1101+
assert SPEND_LOGS_METADATA == dict(json.loads(headers["x-litellm-spend-logs-metadata"])), "spend_logs_metadata should be the same as the headers"
1102+
finally:
1103+
litellm.callbacks = original_callbacks
10971104

1098-
SPEND_LOGS_METADATA = standard_logging_obj["metadata"]["spend_logs_metadata"]
1099-
assert SPEND_LOGS_METADATA == dict(json.loads(headers["x-litellm-spend-logs-metadata"])), "spend_logs_metadata should be the same as the headers"
11001105

1101-
11021106

11031107
def test_get_internal_user_header_from_mapping_returns_expected_header():
11041108
mappings = [

0 commit comments

Comments
 (0)