mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
Litellm anthropic mcp support (#11474)
* feat(anthropic/): initial commit adding working mcp tool call support pass in mcp tool via `tools` and litellm will handle translating it to the right anthropic param * feat(anthropic/): map openai mcp tool to anthropic mcp tool allows usage within responses api * fix(databricks/transformation.py): fix databricks linting error * test(test_anthropic_chat_transformation.py): fix test * test: update test * fix(anthropic/chat/transformation.py): add dummy tool call
This commit is contained in:
parent
440a6aa3d2
commit
e3c66418b8
7 changed files with 145 additions and 21 deletions
|
|
@ -24,6 +24,7 @@ from litellm.types.llms.anthropic import (
|
|||
AnthropicComputerTool,
|
||||
AnthropicHostedTools,
|
||||
AnthropicInputSchema,
|
||||
AnthropicMcpServerTool,
|
||||
AnthropicMessagesTool,
|
||||
AnthropicMessagesToolChoice,
|
||||
AnthropicSystemMessageContent,
|
||||
|
|
@ -41,6 +42,7 @@ from litellm.types.llms.openai import (
|
|||
ChatCompletionToolCallChunk,
|
||||
ChatCompletionToolCallFunctionChunk,
|
||||
ChatCompletionToolParam,
|
||||
OpenAIMcpServerTool,
|
||||
OpenAIWebSearchOptions,
|
||||
)
|
||||
from litellm.types.utils import CompletionTokensDetailsWrapper
|
||||
|
|
@ -175,8 +177,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
|
||||
def _map_tool_helper(
|
||||
self, tool: ChatCompletionToolParam
|
||||
) -> AllAnthropicToolsValues:
|
||||
) -> Tuple[Optional[AllAnthropicToolsValues], Optional[AnthropicMcpServerTool]]:
|
||||
returned_tool: Optional[AllAnthropicToolsValues] = None
|
||||
mcp_server: Optional[AnthropicMcpServerTool] = None
|
||||
|
||||
if tool["type"] == "function" or tool["type"] == "custom":
|
||||
_input_schema: dict = tool["function"].get(
|
||||
|
|
@ -239,33 +242,77 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
returned_tool = AnthropicHostedTools(
|
||||
type=tool["type"], name=function_name, **additional_tool_params # type: ignore
|
||||
)
|
||||
if returned_tool is None:
|
||||
elif tool["type"] == "url": # mcp server tool
|
||||
mcp_server = AnthropicMcpServerTool(**tool) # type: ignore
|
||||
elif tool["type"] == "mcp":
|
||||
mcp_server = self._map_openai_mcp_server_tool(
|
||||
cast(OpenAIMcpServerTool, tool)
|
||||
)
|
||||
if returned_tool is None and mcp_server is None:
|
||||
raise ValueError(f"Unsupported tool type: {tool['type']}")
|
||||
|
||||
## check if cache_control is set in the tool
|
||||
_cache_control = tool.get("cache_control", None)
|
||||
_cache_control_function = tool.get("function", {}).get("cache_control", None)
|
||||
if _cache_control is not None:
|
||||
returned_tool["cache_control"] = _cache_control
|
||||
elif _cache_control_function is not None and isinstance(
|
||||
_cache_control_function, dict
|
||||
):
|
||||
returned_tool["cache_control"] = ChatCompletionCachedContent(
|
||||
**_cache_control_function # type: ignore
|
||||
if returned_tool is not None:
|
||||
if _cache_control is not None:
|
||||
returned_tool["cache_control"] = _cache_control
|
||||
elif _cache_control_function is not None and isinstance(
|
||||
_cache_control_function, dict
|
||||
):
|
||||
returned_tool["cache_control"] = ChatCompletionCachedContent(
|
||||
**_cache_control_function # type: ignore
|
||||
)
|
||||
|
||||
return returned_tool, mcp_server
|
||||
|
||||
def _map_openai_mcp_server_tool(
|
||||
self, tool: OpenAIMcpServerTool
|
||||
) -> AnthropicMcpServerTool:
|
||||
from litellm.types.llms.anthropic import AnthropicMcpServerToolConfiguration
|
||||
|
||||
allowed_tools = tool.get("allowed_tools", None)
|
||||
tool_configuration: Optional[AnthropicMcpServerToolConfiguration] = None
|
||||
if allowed_tools is not None:
|
||||
tool_configuration = AnthropicMcpServerToolConfiguration(
|
||||
allowed_tools=tool.get("allowed_tools", None),
|
||||
)
|
||||
|
||||
return returned_tool
|
||||
headers = tool.get("headers", {})
|
||||
authorization_token: Optional[str] = None
|
||||
if headers is not None:
|
||||
bearer_token = headers.get("Authorization", None)
|
||||
if bearer_token is not None:
|
||||
authorization_token = bearer_token.replace("Bearer ", "")
|
||||
|
||||
def _map_tools(self, tools: List) -> List[AllAnthropicToolsValues]:
|
||||
initial_tool = AnthropicMcpServerTool(
|
||||
type="url",
|
||||
url=tool["server_url"],
|
||||
name=tool["server_label"],
|
||||
)
|
||||
|
||||
if tool_configuration is not None:
|
||||
initial_tool["tool_configuration"] = tool_configuration
|
||||
if authorization_token is not None:
|
||||
initial_tool["authorization_token"] = authorization_token
|
||||
return initial_tool
|
||||
|
||||
def _map_tools(
|
||||
self, tools: List
|
||||
) -> Tuple[List[AllAnthropicToolsValues], List[AnthropicMcpServerTool]]:
|
||||
anthropic_tools = []
|
||||
mcp_servers = []
|
||||
for tool in tools:
|
||||
if "input_schema" in tool: # assume in anthropic format
|
||||
anthropic_tools.append(tool)
|
||||
else: # assume openai tool call
|
||||
new_tool = self._map_tool_helper(tool)
|
||||
new_tool, mcp_server_tool = self._map_tool_helper(tool)
|
||||
|
||||
anthropic_tools.append(new_tool)
|
||||
return anthropic_tools
|
||||
if new_tool is not None:
|
||||
anthropic_tools.append(new_tool)
|
||||
if mcp_server_tool is not None:
|
||||
mcp_servers.append(mcp_server_tool)
|
||||
return anthropic_tools, mcp_servers
|
||||
|
||||
def _map_stop_sequences(
|
||||
self, stop: Optional[Union[str, List[str]]]
|
||||
|
|
@ -389,10 +436,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
optional_params["max_tokens"] = value
|
||||
if param == "tools":
|
||||
# check if optional params already has tools
|
||||
tool_value = self._map_tools(value)
|
||||
anthropic_tools, mcp_servers = self._map_tools(value)
|
||||
optional_params = self._add_tools_to_optional_params(
|
||||
optional_params=optional_params, tools=tool_value
|
||||
optional_params=optional_params, tools=anthropic_tools
|
||||
)
|
||||
if mcp_servers:
|
||||
optional_params["mcp_servers"] = mcp_servers
|
||||
if param == "tool_choice" or param == "parallel_tool_calls":
|
||||
_tool_choice: Optional[
|
||||
AnthropicMessagesToolChoice
|
||||
|
|
@ -591,7 +640,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
and has_tool_call_blocks(messages)
|
||||
):
|
||||
if litellm.modify_params:
|
||||
optional_params["tools"] = self._map_tools(
|
||||
optional_params["tools"], _ = self._map_tools(
|
||||
add_dummy_tool(custom_llm_provider="anthropic")
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
)
|
||||
from litellm.llms.base_llm.base_utils import BaseLLMModelInfo
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.types.llms.anthropic import AllAnthropicToolsValues
|
||||
from litellm.types.llms.anthropic import AllAnthropicToolsValues, AnthropicMcpServerTool
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
|
||||
|
|
@ -51,6 +51,15 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
file_ids = get_file_ids_from_messages(messages)
|
||||
return len(file_ids) > 0
|
||||
|
||||
def is_mcp_server_used(
|
||||
self, mcp_servers: Optional[List[AnthropicMcpServerTool]]
|
||||
) -> bool:
|
||||
if mcp_servers is None:
|
||||
return False
|
||||
if mcp_servers:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_computer_tool_used(
|
||||
self, tools: Optional[List[AllAnthropicToolsValues]]
|
||||
) -> bool:
|
||||
|
|
@ -92,6 +101,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
prompt_caching_set: bool = False,
|
||||
pdf_used: bool = False,
|
||||
file_id_used: bool = False,
|
||||
mcp_server_used: bool = False,
|
||||
is_vertex_request: bool = False,
|
||||
user_anthropic_beta_headers: Optional[List[str]] = None,
|
||||
) -> dict:
|
||||
|
|
@ -105,6 +115,9 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
if file_id_used:
|
||||
betas.add("files-api-2025-04-14")
|
||||
betas.add("code-execution-2025-05-22")
|
||||
if mcp_server_used:
|
||||
betas.add("mcp-client-2025-04-04")
|
||||
|
||||
headers = {
|
||||
"anthropic-version": anthropic_version or "2023-06-01",
|
||||
"x-api-key": api_key,
|
||||
|
|
@ -143,6 +156,9 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
tools = optional_params.get("tools")
|
||||
prompt_caching_set = self.is_cache_control_set(messages=messages)
|
||||
computer_tool_used = self.is_computer_tool_used(tools=tools)
|
||||
mcp_server_used = self.is_mcp_server_used(
|
||||
mcp_servers=optional_params.get("mcp_servers")
|
||||
)
|
||||
pdf_used = self.is_pdf_used(messages=messages)
|
||||
file_id_used = self.is_file_id_used(messages=messages)
|
||||
user_anthropic_beta_headers = self._get_user_anthropic_beta_headers(
|
||||
|
|
@ -156,6 +172,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
file_id_used=file_id_used,
|
||||
is_vertex_request=optional_params.get("is_vertex_request", False),
|
||||
user_anthropic_beta_headers=user_anthropic_beta_headers,
|
||||
mcp_server_used=mcp_server_used,
|
||||
)
|
||||
|
||||
headers = {**headers, **anthropic_headers}
|
||||
|
|
|
|||
|
|
@ -184,7 +184,9 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
return tools
|
||||
|
||||
# if claude, convert to anthropic tool and then to databricks tool
|
||||
anthropic_tools = self._map_tools(tools=tools)
|
||||
anthropic_tools, _ = self._map_tools(
|
||||
tools=tools
|
||||
) # unclear how mcp tool calling on databricks works
|
||||
databricks_tools = [
|
||||
cast(DatabricksTool, self.convert_anthropic_tool_to_databricks_tool(tool))
|
||||
for tool in anthropic_tools
|
||||
|
|
|
|||
|
|
@ -72,6 +72,18 @@ AllAnthropicToolsValues = Union[
|
|||
]
|
||||
|
||||
|
||||
class AnthropicMcpServerToolConfiguration(TypedDict, total=False):
|
||||
allowed_tools: Optional[List[str]]
|
||||
|
||||
|
||||
class AnthropicMcpServerTool(TypedDict, total=False):
|
||||
type: Required[Literal["url"]]
|
||||
url: Required[str]
|
||||
name: Required[str]
|
||||
tool_configuration: AnthropicMcpServerToolConfiguration
|
||||
authorization_token: str
|
||||
|
||||
|
||||
class AnthropicMessagesTextParam(TypedDict, total=False):
|
||||
type: Required[Literal["text"]]
|
||||
text: Required[str]
|
||||
|
|
@ -216,6 +228,7 @@ class AnthropicMessagesRequestOptionalParams(TypedDict, total=False):
|
|||
tools: Optional[List[Union[AllAnthropicToolsValues, Dict]]]
|
||||
top_k: Optional[int]
|
||||
top_p: Optional[float]
|
||||
mcp_servers: Optional[List[AnthropicMcpServerTool]]
|
||||
|
||||
|
||||
class AnthropicMessagesRequest(AnthropicMessagesRequestOptionalParams, total=False):
|
||||
|
|
|
|||
|
|
@ -1641,3 +1641,12 @@ class OpenAIRealtimeTurnDetection(TypedDict, total=False):
|
|||
silence_duration_ms: int
|
||||
threshold: int
|
||||
type: str
|
||||
|
||||
|
||||
class OpenAIMcpServerTool(TypedDict, total=False):
|
||||
type: Required[Literal["mcp"]]
|
||||
server_label: Required[str]
|
||||
server_url: Required[str]
|
||||
require_approval: str
|
||||
allowed_tools: Optional[List[str]]
|
||||
headers: Optional[Dict[str, str]]
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ def test_anthropic_tool_helper(cache_control_location):
|
|||
else:
|
||||
tool["cache_control"] = {"type": "ephemeral"}
|
||||
|
||||
tool = AnthropicConfig()._map_tool_helper(tool=tool)
|
||||
tool, _ = AnthropicConfig()._map_tool_helper(tool=tool)
|
||||
|
||||
assert tool["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
|
|
@ -1273,3 +1273,37 @@ def test_anthropic_text_editor():
|
|||
|
||||
assert response is not None
|
||||
|
||||
@pytest.mark.parametrize("spec", ["anthropic", "openai"])
|
||||
def test_anthropic_mcp_server_tool_use(spec: str):
|
||||
litellm._turn_on_debug()
|
||||
|
||||
if spec == "anthropic":
|
||||
tools = [
|
||||
{
|
||||
"type": "url",
|
||||
"url": "https://mcp.deepwiki.com/mcp",
|
||||
"name": "deepwiki-mcp",
|
||||
}
|
||||
]
|
||||
elif spec == "openai":
|
||||
tools=[
|
||||
{
|
||||
"type": "mcp",
|
||||
"server_label": "deepwiki",
|
||||
"server_url": "https://mcp.deepwiki.com/mcp",
|
||||
"require_approval": "never",
|
||||
},
|
||||
]
|
||||
|
||||
params = {
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
"messages": [{"role": "user", "content": "Who won the World Cup in 2022?"}],
|
||||
"tools": tools
|
||||
}
|
||||
|
||||
try:
|
||||
response = litellm.completion(**params)
|
||||
except litellm.InternalServerError as e:
|
||||
print(e)
|
||||
|
||||
assert response is not None
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ def test_map_tool_helper():
|
|||
|
||||
tool = {"type": "web_search_20250305", "name": "web_search", "max_uses": 5}
|
||||
|
||||
result = config._map_tool_helper(tool)
|
||||
result, _ = config._map_tool_helper(tool)
|
||||
assert result is not None
|
||||
assert result["name"] == "web_search"
|
||||
assert result["max_uses"] == 5
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue