mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(anthropic-passthrough): strip mcp__<server>__ namespace prefix for non-Anthropic tool routes
When the wallet (or any claude-agent-sdk-based caller) registers tools via an MCP server, the SDK exposes tool names with an MCP namespace prefix of the form `mcp__<server>__<tool>` (e.g., `mcp__wallet__transfer`). Anthropic-native models call these correctly. Gemini-2.5-flash-lite (and likely some other lower-tier providers routed through this Anthropic→OpenAI translator) intermittently drops the prefix and emits just the base name (`transfer`), which the SDK then fails to find with `No such tool available`. The full §5 paper-experiment audit of 162 failed Gemini sessions (cryptitalk-wallet-litellm#10 audit) shows this is the single biggest failure mode — ~62% of failures and ~36% of all tool-call events drop the prefix. Fix: in translate_anthropic_tools_to_openai, strip the `mcp__<server>__` prefix before forwarding to the OpenAI-flavor provider, and record the original<->stripped mapping in the existing tool_name_mapping table. The response-side restorer (_translate_openai_content_to_anthropic) already uses this mapping to map returned tool_use.name values back to the SDK-registered original, so no response-side change is needed. Collision-safe: if two tools would strip to the same base name (e.g., mcp__wallet__balance and mcp__bank__balance both -> "balance"), the strip is skipped for both. The mapping table preserves uniqueness. Test plan: smoke-tested the strip helper against MCP-prefixed names, non-prefixed names, multi-underscore tool names, and dash-containing server names. Collision detection verified. Refs: cryptitalk-wallet-litellm#10 #11
This commit is contained in:
parent
b3cea19d60
commit
fc71f84feb
1 changed files with 73 additions and 2 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
|
|
@ -46,6 +47,41 @@ def truncate_tool_name(name: str) -> str:
|
|||
return f"{name[:TOOL_NAME_PREFIX_LENGTH]}_{name_hash}"
|
||||
|
||||
|
||||
# Pattern: mcp__<server_name>__<tool_name>
|
||||
# Server name and tool name may contain letters, digits, dashes, single
|
||||
# underscores — but the double-underscore separator is the boundary marker.
|
||||
_MCP_NAMESPACE_PATTERN = re.compile(r"^mcp__([^_]+(?:_[^_]+)*)__(.+)$")
|
||||
|
||||
|
||||
def strip_mcp_namespace(name: str) -> str:
|
||||
"""Strip the ``mcp__<server>__`` prefix from a tool name.
|
||||
|
||||
claude-agent-sdk's MCP-server tool registration produces names of the
|
||||
form ``mcp__<server>__<tool>`` (e.g., ``mcp__wallet__transfer``).
|
||||
Some downstream model providers — Gemini-2.5-flash-lite in particular —
|
||||
intermittently drop the prefix and emit only the base tool name (e.g.,
|
||||
``transfer``). Without rewriting on the gateway side, the SDK then
|
||||
fails the tool lookup with ``No such tool available``.
|
||||
|
||||
Solution: strip the prefix in the request to the model, store the
|
||||
forward mapping (``transfer`` -> ``mcp__wallet__transfer``), and
|
||||
restore the full name on the response side before returning to the
|
||||
SDK. Same general pattern as truncate_tool_name's mapping table.
|
||||
|
||||
Args:
|
||||
name: The original tool name (possibly mcp__-prefixed)
|
||||
|
||||
Returns:
|
||||
The base tool name with the ``mcp__<server>__`` prefix removed,
|
||||
or the original name unchanged if no prefix is present.
|
||||
"""
|
||||
match = _MCP_NAMESPACE_PATTERN.match(name)
|
||||
if match is None:
|
||||
return name
|
||||
base = match.group(2)
|
||||
return base or name
|
||||
|
||||
|
||||
def create_tool_name_mapping(
|
||||
tools: List[Dict[str, Any]],
|
||||
) -> Dict[str, str]:
|
||||
|
|
@ -807,6 +843,30 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
tool_name_mapping: Dict[str, str] = {}
|
||||
mapped_tool_params = ["name", "input_schema", "description", "cache_control"]
|
||||
|
||||
# First pass: detect MCP-namespace-strip collisions. If two tools
|
||||
# would strip to the same base name (e.g., mcp__wallet__balance and
|
||||
# mcp__bank__balance both strip to "balance"), we must NOT strip
|
||||
# — collisions would corrupt the tool_name_mapping reverse lookup.
|
||||
_strip_candidates: Dict[str, str] = {} # original_name -> stripped_name
|
||||
_strip_collision: set = set() # original_names that collide
|
||||
_stripped_counts: Dict[str, int] = {}
|
||||
for idx, tool in enumerate(tools):
|
||||
tool_type = tool.get("type", "")
|
||||
if any(tool_type.startswith(t.value) for t in ANTHROPIC_HOSTED_TOOLS):
|
||||
continue
|
||||
raw = tool.get("name")
|
||||
if raw is None or (isinstance(raw, str) and not str(raw).strip()):
|
||||
continue
|
||||
orig = str(raw)
|
||||
stripped = strip_mcp_namespace(orig)
|
||||
if stripped != orig:
|
||||
_strip_candidates[orig] = stripped
|
||||
_stripped_counts[stripped] = _stripped_counts.get(stripped, 0) + 1
|
||||
# Mark any candidate whose stripped name appears more than once.
|
||||
for orig, stripped in _strip_candidates.items():
|
||||
if _stripped_counts.get(stripped, 0) > 1:
|
||||
_strip_collision.add(orig)
|
||||
|
||||
for idx, tool in enumerate(tools):
|
||||
# Check if this is an Anthropic-native tool that should be kept as-is
|
||||
tool_type = tool.get("type", "")
|
||||
|
|
@ -822,9 +882,20 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
original_name = f"litellm_unnamed_tool_{idx}"
|
||||
else:
|
||||
original_name = str(raw_name)
|
||||
truncated_name = truncate_tool_name(original_name)
|
||||
# Strip MCP namespace prefix unless it would collide. Then
|
||||
# truncate. The mapping table records short<->original so the
|
||||
# response-side restorer can reverse it back to the SDK's
|
||||
# expected name.
|
||||
if original_name in _strip_collision:
|
||||
stripped_name = original_name
|
||||
else:
|
||||
stripped_name = strip_mcp_namespace(original_name)
|
||||
truncated_name = truncate_tool_name(stripped_name)
|
||||
|
||||
# Store mapping if name was truncated
|
||||
# Store mapping if name changed (either MCP-prefix-stripped or
|
||||
# truncated). On the response side, the tool_use.name returned
|
||||
# by the model will be looked up here to restore the full
|
||||
# SDK-registered name.
|
||||
if truncated_name != original_name:
|
||||
tool_name_mapping[truncated_name] = original_name
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue