mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(mcp): expose shared SDK timeout normalization
This commit is contained in:
parent
9b6c7c8bf0
commit
dbf9490229
4 changed files with 18 additions and 17 deletions
|
|
@ -150,8 +150,8 @@ _SDK_READ_TIMEOUT_CODE: Final = int(httpx.codes.REQUEST_TIMEOUT)
|
|||
otherwise carries JSON-RPC error codes."""
|
||||
|
||||
|
||||
def _as_read_timeout(exc: BaseException) -> TimeoutError | None:
|
||||
"""The session read timeout elapsing, re-expressed as a ``TimeoutError``, or ``None``.
|
||||
def as_mcp_read_timeout(exc: BaseException) -> TimeoutError | None:
|
||||
"""Normalize an MCP SDK read timeout for client and gateway diagnostics, or return ``None``.
|
||||
|
||||
The SDK reports its own elapsed read timeout as ``McpError`` carrying an HTTP status code in a
|
||||
field that otherwise holds JSON-RPC error codes, and it relays an upstream's JSON-RPC error
|
||||
|
|
@ -522,7 +522,7 @@ class MCPClient:
|
|||
transport_ctx, http_client = self._create_transport_context()
|
||||
return await self._execute_session_operation(transport_ctx, operation)
|
||||
except Exception as e:
|
||||
read_timeout: Final = _as_read_timeout(e)
|
||||
read_timeout: Final = as_mcp_read_timeout(e)
|
||||
if read_timeout is not None:
|
||||
verbose_logger.warning(
|
||||
"MCP client timed out after %ss waiting for a valid MCP response from %s",
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ if MCP_AVAILABLE:
|
|||
from mcp.shared.exceptions import McpError
|
||||
from mcp.types import Tool as MCPTool
|
||||
|
||||
from litellm.experimental_mcp_client.client import MCPClient, _as_read_timeout
|
||||
from litellm.experimental_mcp_client.client import MCPClient, as_mcp_read_timeout
|
||||
from litellm.llms.litellm_proxy.skills.skill_search import (
|
||||
DEFAULT_SKILL_SEARCH_TOP_K,
|
||||
)
|
||||
|
|
@ -1410,7 +1410,7 @@ if MCP_AVAILABLE:
|
|||
effective_timeout: Final = (
|
||||
min(request.timeout if request.timeout is not None else MCP_CLIENT_TIMEOUT, timeout_seconds)
|
||||
if any(
|
||||
isinstance(cause, McpError) and _as_read_timeout(cause) is not None
|
||||
isinstance(cause, McpError) and as_mcp_read_timeout(cause) is not None
|
||||
for cause in iter_exception_tree(e)
|
||||
)
|
||||
else timeout_seconds
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ import litellm.experimental_mcp_client.client as mcp_client_module
|
|||
from litellm.experimental_mcp_client.client import (
|
||||
MCP_STREAMABLE_HTTP_REQUIREMENT,
|
||||
MCPClient,
|
||||
_as_read_timeout,
|
||||
_first_non_cancelled_cause,
|
||||
_TransportContext,
|
||||
as_mcp_read_timeout,
|
||||
missing_streamable_http_client_error,
|
||||
strip_auth_scheme,
|
||||
)
|
||||
|
|
@ -867,25 +867,25 @@ def _raise_mcp_error_while_handling_a_timeout(code: int, message: str) -> McpErr
|
|||
return raised
|
||||
|
||||
|
||||
def test_as_read_timeout_separates_the_sdk_timeout_from_a_relayed_upstream_error():
|
||||
def test_as_mcp_read_timeout_separates_the_sdk_timeout_from_a_relayed_upstream_error():
|
||||
"""Neither signal alone is enough. The code alone cannot separate the SDK's own timeout from an
|
||||
upstream JSON-RPC error that happens to use 408, and the context chain alone cannot separate it
|
||||
from any other relayed error that surfaces while a timeout is being handled, so both must hold.
|
||||
"""
|
||||
timeout_code = int(httpx.codes.REQUEST_TIMEOUT)
|
||||
|
||||
translated = _as_read_timeout(_raise_mcp_error_while_handling_a_timeout(timeout_code, "Timed out while waiting"))
|
||||
translated = as_mcp_read_timeout(_raise_mcp_error_while_handling_a_timeout(timeout_code, "Timed out while waiting"))
|
||||
assert isinstance(translated, TimeoutError)
|
||||
assert str(translated) == "Timed out while waiting"
|
||||
|
||||
relayed_408 = McpError(ErrorData(code=timeout_code, message="upstream said 408"))
|
||||
assert _as_read_timeout(relayed_408) is None, "an upstream 408 with no elapsed timeout is not our timeout"
|
||||
assert as_mcp_read_timeout(relayed_408) is None, "an upstream 408 with no elapsed timeout is not our timeout"
|
||||
|
||||
relayed_other = _raise_mcp_error_while_handling_a_timeout(-32603, "upstream internal error")
|
||||
assert _as_read_timeout(relayed_other) is None, "a non-timeout code is not our timeout, whatever the chain"
|
||||
assert as_mcp_read_timeout(relayed_other) is None, "a non-timeout code is not our timeout, whatever the chain"
|
||||
|
||||
assert _as_read_timeout(McpError(ErrorData(code=-32603, message="boom"))) is None
|
||||
assert _as_read_timeout(RuntimeError("not an McpError")) is None
|
||||
assert as_mcp_read_timeout(McpError(ErrorData(code=-32603, message="boom"))) is None
|
||||
assert as_mcp_read_timeout(RuntimeError("not an McpError")) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -1619,7 +1619,7 @@ async def test_transport_completion_and_normal_messages(transport: MCPTransport,
|
|||
if mode == "closed":
|
||||
assert "connection was closed" in _connection_error_message(caught.value, client.server_url, 0.2)
|
||||
else:
|
||||
assert isinstance(_as_read_timeout(caught.value), TimeoutError)
|
||||
assert isinstance(as_mcp_read_timeout(caught.value), TimeoutError)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -1685,4 +1685,4 @@ async def test_empty_http_event_stream_uses_the_existing_request_deadline() -> N
|
|||
),
|
||||
timeout=3,
|
||||
)
|
||||
assert isinstance(_as_read_timeout(caught.value), TimeoutError)
|
||||
assert isinstance(as_mcp_read_timeout(caught.value), TimeoutError)
|
||||
|
|
|
|||
|
|
@ -3453,7 +3453,8 @@ class TestConnectionErrorMessage:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("sdk_timeout", [True, False])
|
||||
async def test_timeout_message_uses_the_deadline_that_expired(self, sdk_timeout: bool) -> None:
|
||||
@pytest.mark.parametrize("read_timeout", [0, 1])
|
||||
async def test_timeout_message_uses_the_deadline_that_expired(self, sdk_timeout: bool, read_timeout: int) -> None:
|
||||
from mcp import McpError
|
||||
from mcp.types import ErrorData
|
||||
|
||||
|
|
@ -3469,10 +3470,10 @@ class TestConnectionErrorMessage:
|
|||
raise TimeoutError() from sdk_error
|
||||
|
||||
payload: Final = NewMCPServerRequest(
|
||||
server_name="timeout", url="https://example.com", auth_type=MCPAuth.none, timeout=1
|
||||
server_name="timeout", url="https://example.com", auth_type=MCPAuth.none, timeout=read_timeout
|
||||
)
|
||||
result: Final = await rest_endpoints._execute_with_mcp_client(payload, operation, timeout_seconds=30)
|
||||
assert ("within 1s" if sdk_timeout else "within 30s") in result["message"]
|
||||
assert (f"within {read_timeout}s" if sdk_timeout else "within 30s") in result["message"]
|
||||
assert "secret" not in result["message"]
|
||||
|
||||
def test_unknown_error_falls_back_to_generic(self):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue