mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(mcp): fall back to top_k MCP tools when zero matches and no non-MCP tools
When all tools are MCP and zero semantic matches are found, return the first top_k MCP tools instead of an empty list. Avoids sending an empty tool list to the LLM.
This commit is contained in:
parent
a87329d2a5
commit
24ae278990
2 changed files with 60 additions and 0 deletions
|
|
@ -245,6 +245,16 @@ class SemanticToolFilterHook(CustomLogger):
|
|||
available_tools=mcp_tools, # type: ignore
|
||||
)
|
||||
|
||||
# If no MCP tools matched and no non-MCP tools exist, fall back
|
||||
# to the first top_k MCP tools to avoid an empty tool list.
|
||||
if not filtered_mcp_tools and not non_mcp_tools:
|
||||
limit = self.filter.top_k
|
||||
filtered_mcp_tools = mcp_tools[:limit]
|
||||
verbose_proxy_logger.warning(
|
||||
f"No semantic matches and no non-MCP tools — "
|
||||
f"falling back to first {limit} MCP tools"
|
||||
)
|
||||
|
||||
filtered_tools = filtered_mcp_tools + non_mcp_tools
|
||||
|
||||
# Always update tools and emit header (even if count unchanged)
|
||||
|
|
|
|||
|
|
@ -434,6 +434,56 @@ async def test_semantic_filter_hook_skips_non_mcp_only_tools():
|
|||
assert result is None, "Hook should skip when no MCP tools are present"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hook_falls_back_to_top_k_when_only_mcp_and_zero_matches():
|
||||
"""
|
||||
Test that when all tools are MCP and zero semantic matches are found,
|
||||
the hook falls back to the first top_k MCP tools instead of returning empty.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.semantic_tool_filter import (
|
||||
SemanticMCPToolFilter,
|
||||
)
|
||||
from litellm.proxy.hooks.mcp_semantic_filter import SemanticToolFilterHook
|
||||
|
||||
mock_router = Mock()
|
||||
filter_instance = SemanticMCPToolFilter(
|
||||
embedding_model="text-embedding-3-small",
|
||||
litellm_router_instance=mock_router,
|
||||
top_k=3,
|
||||
similarity_threshold=0.3,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
# Mock the semantic router to return no matches
|
||||
filter_instance.tool_router = Mock(return_value=[])
|
||||
|
||||
hook = SemanticToolFilterHook(filter_instance)
|
||||
|
||||
# Only MCP tools (all prefixed), no non-MCP tools
|
||||
mcp_only_tools = [
|
||||
MCPTool(name=f"server-tool_{i}", description=f"MCP tool {i}", inputSchema={"type": "object"})
|
||||
for i in range(10)
|
||||
]
|
||||
|
||||
data = {
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"tools": mcp_only_tools,
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
result = await hook.async_pre_call_hook(
|
||||
user_api_key_dict=Mock(),
|
||||
cache=Mock(),
|
||||
data=data,
|
||||
call_type="completion",
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
# Should fall back to first top_k (3) MCP tools, not empty
|
||||
assert len(result["tools"]) == 3, f"Expected 3 tools (top_k), got {len(result['tools'])}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_zero_matches_returns_empty():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue