mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix(mcp_semantic_filter): keep tool names whole in filter response header (#32282)
The x-litellm-semantic-filter-tools response header was sliced mid-name at MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH with a trailing "...", so the admin UI test panel rendered the last selected tool name chopped. Truncate the CSV at a tool name boundary instead so the header only ever carries complete names, and note in the test panel how many selected tools did not fit in the header
This commit is contained in:
parent
8449ecee6a
commit
5e73994441
4 changed files with 96 additions and 5 deletions
|
|
@ -24,6 +24,18 @@ if TYPE_CHECKING:
|
|||
from litellm.router import Router
|
||||
|
||||
|
||||
def _truncate_csv_at_tool_name_boundary(tool_names_csv: str, max_length: int) -> str:
|
||||
"""Cap a CSV of tool names to max_length, dropping any name that does not fit whole."""
|
||||
if len(tool_names_csv) <= max_length:
|
||||
return tool_names_csv
|
||||
|
||||
head = tool_names_csv[: max_length + 1]
|
||||
if "," not in head:
|
||||
return ""
|
||||
|
||||
return head.rsplit(",", 1)[0]
|
||||
|
||||
|
||||
class SemanticToolFilterHook(CustomLogger):
|
||||
"""
|
||||
Pre-call hook that filters MCP tools semantically.
|
||||
|
|
@ -327,11 +339,12 @@ class SemanticToolFilterHook(CustomLogger):
|
|||
|
||||
# Add CSV of filtered tool names (nginx-safe length)
|
||||
tool_names_csv = metadata.get("litellm_semantic_filter_tools", "")
|
||||
if tool_names_csv:
|
||||
if len(tool_names_csv) > MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH:
|
||||
tool_names_csv = tool_names_csv[: MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH - 3] + "..."
|
||||
|
||||
headers["x-litellm-semantic-filter-tools"] = tool_names_csv
|
||||
header_safe_csv = _truncate_csv_at_tool_name_boundary(
|
||||
tool_names_csv=tool_names_csv,
|
||||
max_length=MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH,
|
||||
)
|
||||
if header_safe_csv:
|
||||
headers["x-litellm-semantic-filter-tools"] = header_safe_csv
|
||||
|
||||
return headers
|
||||
|
||||
|
|
|
|||
|
|
@ -1050,3 +1050,64 @@ class TestGetToolsByNames:
|
|||
)
|
||||
|
||||
assert matched == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_filter_headers_hook_emits_only_complete_tool_names():
|
||||
"""
|
||||
Regression test for LIT-4215.
|
||||
|
||||
The x-litellm-semantic-filter-tools header used to be sliced mid-name at
|
||||
MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH with a "..." suffix, so the UI
|
||||
rendered a chopped tool name as the last entry. The header must only ever
|
||||
contain complete tool names, in their original order, within the cap.
|
||||
"""
|
||||
from litellm.constants import MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH
|
||||
from litellm.proxy._experimental.mcp_server.semantic_tool_filter import (
|
||||
SemanticMCPToolFilter,
|
||||
)
|
||||
from litellm.proxy.hooks.mcp_semantic_filter import SemanticToolFilterHook
|
||||
|
||||
filter_instance = SemanticMCPToolFilter(
|
||||
embedding_model="text-embedding-3-small",
|
||||
litellm_router_instance=Mock(),
|
||||
top_k=10,
|
||||
similarity_threshold=0.3,
|
||||
enabled=True,
|
||||
)
|
||||
hook = SemanticToolFilterHook(filter_instance)
|
||||
|
||||
tool_names = [f"metrics_mcp-very_long_tool_name_for_header_{i:02d}" for i in range(8)]
|
||||
data = {
|
||||
"metadata": {
|
||||
"litellm_semantic_filter_stats": "40->8",
|
||||
"litellm_semantic_filter_tools": ",".join(tool_names),
|
||||
}
|
||||
}
|
||||
|
||||
headers = await hook.async_post_call_response_headers_hook(
|
||||
data=data,
|
||||
user_api_key_dict=Mock(),
|
||||
response=None,
|
||||
)
|
||||
|
||||
assert headers is not None
|
||||
assert headers["x-litellm-semantic-filter"] == "40->8"
|
||||
|
||||
tools_header = headers["x-litellm-semantic-filter-tools"]
|
||||
assert len(tools_header) <= MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH
|
||||
emitted_names = tools_header.split(",")
|
||||
assert emitted_names == tool_names[: len(emitted_names)]
|
||||
assert 0 < len(emitted_names) < len(tool_names)
|
||||
|
||||
|
||||
def test_truncate_csv_at_tool_name_boundary_edges():
|
||||
from litellm.proxy.hooks.mcp_semantic_filter.hook import (
|
||||
_truncate_csv_at_tool_name_boundary,
|
||||
)
|
||||
|
||||
assert _truncate_csv_at_tool_name_boundary(tool_names_csv="a,b,c", max_length=150) == "a,b,c"
|
||||
assert _truncate_csv_at_tool_name_boundary(tool_names_csv="abc,def", max_length=3) == "abc"
|
||||
assert _truncate_csv_at_tool_name_boundary(tool_names_csv="ab,cd,ef", max_length=5) == "ab,cd"
|
||||
assert _truncate_csv_at_tool_name_boundary(tool_names_csv="ab,cd,ef", max_length=4) == "ab"
|
||||
assert _truncate_csv_at_tool_name_boundary(tool_names_csv="single_name_longer_than_cap", max_length=10) == ""
|
||||
|
|
|
|||
|
|
@ -103,6 +103,18 @@ describe("MCPSemanticFilterTestPanel", () => {
|
|||
expect(screen.getByText("wiki-fetch")).toBeInTheDocument();
|
||||
expect(screen.getByText("github-search")).toBeInTheDocument();
|
||||
expect(screen.getByText("slack-post")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/more selected tools not shown/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should note how many selected tools are missing when the header list is incomplete", () => {
|
||||
const testResult: TestResult = {
|
||||
totalTools: 40,
|
||||
selectedTools: 8,
|
||||
tools: ["metrics_mcp-node_query_by_id", "metrics_mcp-latency_query_api", "inventory_mcp-site_lookup"],
|
||||
};
|
||||
render(<MCPSemanticFilterTestPanel {...buildProps({ testResult })} />);
|
||||
|
||||
expect(screen.getByText("+5 more selected tools not shown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render the results section when testResult is null", () => {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,11 @@ export default function MCPSemanticFilterTestPanel({
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{testResult.selectedTools > testResult.tools.length && (
|
||||
<Typography.Text type="secondary" style={{ display: "block", marginTop: 8 }}>
|
||||
+{testResult.selectedTools - testResult.tools.length} more selected tools not shown
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue