From ff2cfa640ba7d5d2f80111fd62f9b3af5efd7a62 Mon Sep 17 00:00:00 2001 From: PRABHU KIRAN VANDRANKI <72809214+VANDRANKI@users.noreply.github.com> Date: Tue, 5 May 2026 14:49:26 -0400 Subject: [PATCH] fix(mcp-semantic-filter): use chat format when expanding MCP tools for /v1/chat/completions When mcp_semantic_tool_filter expanded MCP references, it called _process_mcp_tools_to_openai_format with the default target_format="responses", which returns flat {"type":"function","name":...} objects (Responses API format). For /v1/chat/completions backends these must be in the nested {"type":"function","function":{...}} schema, or strict OpenAI-compatible APIs (LM Studio, etc.) return 400 BadRequest. Pass target_format through from _expand_mcp_tools -> _process_mcp_tools_to_openai_format. In async_pre_call_hook, default to "chat" format and only use "responses" when call_type=="aresponses". Also fix _get_tool_names_csv to extract tool names from both the nested (chat) and flat (responses) schemas. --- .../proxy/hooks/mcp_semantic_filter/hook.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/hooks/mcp_semantic_filter/hook.py b/litellm/proxy/hooks/mcp_semantic_filter/hook.py index 4075641d63b..bee3ecccde2 100644 --- a/litellm/proxy/hooks/mcp_semantic_filter/hook.py +++ b/litellm/proxy/hooks/mcp_semantic_filter/hook.py @@ -64,12 +64,19 @@ class SemanticToolFilterHook(CustomLogger): self, tools: List[Any], user_api_key_dict: "UserAPIKeyAuth", + target_format: str = "chat", ) -> List[Dict[str, Any]]: """ Expand MCP references to actual tool definitions. Reuses LiteLLM_Proxy_MCP_Handler._process_mcp_tools_to_openai_format which internally does: parse -> fetch -> filter -> deduplicate -> transform + + Args: + tools: List of tool objects which may include MCP references. + user_api_key_dict: User auth for access control. + target_format: "chat" for ChatCompletions nested ``function`` schema + (default), "responses" for Responses API flat schema. """ from litellm.responses.mcp.litellm_proxy_mcp_handler import ( LiteLLM_Proxy_MCP_Handler, @@ -87,7 +94,9 @@ class SemanticToolFilterHook(CustomLogger): openai_tools, _, ) = await LiteLLM_Proxy_MCP_Handler._process_mcp_tools_to_openai_format( - user_api_key_auth=user_api_key_dict, mcp_tools_with_litellm_proxy=mcp_tools + user_api_key_auth=user_api_key_dict, + mcp_tools_with_litellm_proxy=mcp_tools, + target_format=target_format, # type: ignore[arg-type] ) # Convert Pydantic models to dicts for compatibility @@ -171,7 +180,12 @@ class SemanticToolFilterHook(CustomLogger): ) try: - expanded_tools = await self._expand_mcp_tools(tools, user_api_key_dict) + # Use "chat" schema (nested function key) for chat completions; + # Responses API endpoints expect the flat format. + _target_format = "responses" if call_type == "aresponses" else "chat" + expanded_tools = await self._expand_mcp_tools( + tools, user_api_key_dict, target_format=_target_format + ) if not expanded_tools: verbose_proxy_logger.warning( @@ -293,11 +307,15 @@ class SemanticToolFilterHook(CustomLogger): tool_names = [] for tool in tools: - name = ( - tool.get("name", "") - if isinstance(tool, dict) - else getattr(tool, "name", "") - ) + if isinstance(tool, dict): + # ChatCompletions format: {"type": "function", "function": {"name": ...}} + # Responses API format: {"type": "function", "name": ...} + func = tool.get("function") + name = ( + func.get("name", "") if isinstance(func, dict) else tool.get("name", "") + ) + else: + name = getattr(tool, "name", "") if name: tool_names.append(name)