mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* fix(mcp): honor server_id for REST tool calls with shared upstream URLs When multiple MCP server entries point at the same backend URL and tool name, REST /mcp-rest/tools/call now routes and applies auth from the requested server_id instead of the global unprefixed tool-name mapping. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): classify prefixed REST tool names against full registry Use all registered MCP server prefixes for prefix detection so unauthorized prefixed names still trigger tool_server_mismatch, and reject ambiguous hyphenated REST tool names with server_id. Co-authored-by: Cursor <cursoragent@cursor.com> * test(mcp): cover server_id fallback for unresolved prefixed REST tool names execute_mcp_tool left the prefix-retry and requested-server fallback branches uncovered, dropping diff coverage below the project target. Add a regression test for a REST call that passes server_id with a prefixed tool name that resolves to no managed tool; it must still dispatch to the server identified by server_id rather than the server named by the prefix. * test(mcp): scope global tool-name mapping mutation with patch.dict * test(mcp): cover server_id guard on prefix-retry tool resolution The prefix-retry branch in execute_mcp_tool re-prefixes the tool name with the requested server's known prefixes when the bare lookup misses. The candidate-found path that assigns mcp_server from that lookup stayed uncovered, so codecov patch coverage remained below the diff target. Add a regression test where the re-prefixed lookup resolves a server whose server_id differs from the requested server_id; the tool_server_mismatch 403 guard must still fire instead of being silently bypassed. * test(mcp): assert requested server credentials injected on cross-server REST routing * perf(mcp): scan registry prefixes only when server_id is supplied * fix(mcp): allow hyphenated upstream tool names when REST server_id is authoritative * perf(mcp): skip registry prefix scan for separator-free REST tool names * test(http_handler): drop httpbin dependence from per-request timeout test The per-request timeout test posted to https://httpbin.org/delay/10 and asserted a Timeout was raised. httpbin's free /delay endpoint intermittently returns 503 even when the /get reachability guard succeeds, so local_testing_part1 flaked on that 503 instead of the expected timeout (failed identically across an initial run and a rerun-from-failed). Serve the slow response from a local ThreadingHTTPServer so the timeout fires deterministically with no third-party network dependence. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
67 lines
2 KiB
Python
67 lines
2 KiB
Python
"""
|
|
``_get_httpx_client`` + ``HTTPHandler.post`` (same pattern as Azure Anthropic sync path:
|
|
``_get_httpx_client(params={"timeout": ...})`` then ``post(..., timeout=...)``).
|
|
|
|
A local server stalls longer than the per-request ``timeout`` but well under the client
|
|
default, so the handler must raise :class:`~litellm.exceptions.Timeout` from the per-request
|
|
override rather than completing under the (much larger) client default.
|
|
|
|
Lives under ``local_testing`` (not ``make test-unit``).
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
|
|
|
from litellm.exceptions import Timeout as LitellmTimeout
|
|
from litellm.llms.custom_httpx.http_handler import (
|
|
MaskedHTTPStatusError,
|
|
_get_httpx_client,
|
|
)
|
|
|
|
_SERVER_DELAY_S = 5
|
|
_PER_REQUEST_TIMEOUT_S = 1.0
|
|
_CLIENT_DEFAULT_TIMEOUT_S = 60.0
|
|
|
|
|
|
class _SlowHandler(BaseHTTPRequestHandler):
|
|
def do_POST(self):
|
|
time.sleep(_SERVER_DELAY_S)
|
|
try:
|
|
self.send_response(200)
|
|
self.end_headers()
|
|
self.wfile.write(b"{}")
|
|
except OSError:
|
|
pass
|
|
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
|
|
def test_post_delay_exceeds_per_request_timeout_raises():
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), _SlowHandler)
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
host, port = server.server_address
|
|
|
|
handler = _get_httpx_client(params={"timeout": _CLIENT_DEFAULT_TIMEOUT_S})
|
|
try:
|
|
with pytest.raises(LitellmTimeout):
|
|
handler.post(
|
|
f"http://{host}:{port}/delay",
|
|
headers={"content-type": "application/json"},
|
|
data=json.dumps({"model": "claude", "messages": []}),
|
|
timeout=_PER_REQUEST_TIMEOUT_S,
|
|
)
|
|
except MaskedHTTPStatusError as e:
|
|
pytest.skip(f"httpbin.org unavailable: {e}")
|
|
finally:
|
|
handler.close()
|
|
server.shutdown()
|
|
server.server_close()
|