mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix(anthropic): preserve native tool format when guardrails convert tools for Anthropic Messages API
- Keep Anthropic-native tools (tool_search_tool_regex, web_search, bash, etc.) in original format when translating to OpenAI format for guardrails - Convert guardrail-returned tools back from OpenAI to Anthropic format (type=custom for user tools) - Add TOOL_SEARCH_TOOL to ANTHROPIC_HOSTED_TOOLS enum; use prefix matching for native tool detection - Set type=custom explicitly when mapping OpenAI function tools to AnthropicMessagesTool - Add test for Anthropic native tools with guardrails Made-with: Cursor
This commit is contained in:
parent
9cd7ad2634
commit
45ba9e1f7e
5 changed files with 87 additions and 2 deletions
|
|
@ -127,7 +127,15 @@ class AnthropicMessagesHandler(BaseTranslation):
|
|||
guardrailed_texts = guardrailed_inputs.get("texts", [])
|
||||
guardrailed_tools = guardrailed_inputs.get("tools")
|
||||
if guardrailed_tools is not None:
|
||||
data["tools"] = guardrailed_tools
|
||||
# Convert tools back from OpenAI format to Anthropic format
|
||||
anthropic_config = AnthropicConfig()
|
||||
anthropic_tools: List[AllAnthropicToolsValues] = []
|
||||
for tool in guardrailed_tools:
|
||||
converted_tool, mcp_server = anthropic_config._map_tool_helper(tool)
|
||||
if converted_tool is not None:
|
||||
anthropic_tools.append(converted_tool)
|
||||
# Note: MCP servers are handled separately in the main transformation
|
||||
data["tools"] = anthropic_tools
|
||||
|
||||
# Step 3: Map guardrail responses back to original message structure
|
||||
await self._apply_guardrail_responses_to_input(
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@ from litellm.types.utils import (
|
|||
CompletionTokensDetailsWrapper,
|
||||
)
|
||||
from litellm.types.utils import Message as LitellmMessage
|
||||
from litellm.types.utils import PromptTokensDetailsWrapper, ServerToolUse
|
||||
from litellm.types.utils import (
|
||||
PromptTokensDetailsWrapper,
|
||||
ServerToolUse,
|
||||
)
|
||||
from litellm.utils import (
|
||||
ModelResponse,
|
||||
Usage,
|
||||
|
|
@ -420,6 +423,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
_tool = AnthropicMessagesTool(
|
||||
name=tool["function"]["name"],
|
||||
input_schema=input_anthropic_schema,
|
||||
type="custom",
|
||||
)
|
||||
|
||||
_description = tool["function"].get("description")
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
parse_tool_call_arguments,
|
||||
)
|
||||
from litellm.types.llms.anthropic import (
|
||||
ANTHROPIC_HOSTED_TOOLS,
|
||||
AllAnthropicToolsValues,
|
||||
AnthopicMessagesAssistantMessageParam,
|
||||
AnthropicFinishReason,
|
||||
|
|
@ -771,7 +772,15 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
new_tools: List[ChatCompletionToolParam] = []
|
||||
tool_name_mapping: Dict[str, str] = {}
|
||||
mapped_tool_params = ["name", "input_schema", "description", "cache_control"]
|
||||
|
||||
for tool in tools:
|
||||
# Check if this is an Anthropic-native tool that should be kept as-is
|
||||
tool_type = tool.get("type", "")
|
||||
if any(tool_type.startswith(t.value) for t in ANTHROPIC_HOSTED_TOOLS):
|
||||
# Keep Anthropic-native tools in their original format
|
||||
new_tools.append(tool) # type: ignore[arg-type]
|
||||
continue
|
||||
|
||||
original_name = tool["name"]
|
||||
truncated_name = truncate_tool_name(original_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -639,6 +639,7 @@ class ANTHROPIC_HOSTED_TOOLS(str, Enum):
|
|||
CODE_EXECUTION = "code_execution"
|
||||
WEB_FETCH = "web_fetch"
|
||||
MEMORY = "memory"
|
||||
TOOL_SEARCH_TOOL = "tool_search_tool"
|
||||
|
||||
|
||||
class ANTHROPIC_BETA_HEADER_VALUES(str, Enum):
|
||||
|
|
|
|||
|
|
@ -231,6 +231,69 @@ class TestAnthropicMessagesHandlerInputProcessing:
|
|||
assert result == responses_so_far
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_input_messages_with_anthropic_native_tools(self):
|
||||
"""Test that Anthropic native tools (tool_search_tool_regex) are preserved correctly
|
||||
|
||||
This test verifies the fix for the bug where Anthropic native tools like
|
||||
tool_search_tool_regex_20251119 were being converted to OpenAI format and then
|
||||
not properly converted back, causing API errors.
|
||||
|
||||
The guardrail converts tools to OpenAI format for processing, then they need to be
|
||||
converted back to Anthropic format. Native Anthropic tools should be preserved as-is,
|
||||
while regular tools should be converted to type="custom".
|
||||
"""
|
||||
handler = AnthropicMessagesHandler()
|
||||
guardrail = MockPassThroughGuardrail(guardrail_name="test")
|
||||
|
||||
data = {
|
||||
"model": "claude-opus-4-6",
|
||||
"messages": [{"role": "user", "content": "What is the weather in San Francisco?"}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "tool_search_tool_regex_20251119",
|
||||
"name": "tool_search_tool_regex"
|
||||
},
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "Get the weather at a specific location",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["celsius", "fahrenheit"]
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
},
|
||||
"defer_loading": True
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
result = await handler.process_input_messages(
|
||||
data=data,
|
||||
guardrail_to_apply=guardrail,
|
||||
litellm_logging_obj=MagicMock()
|
||||
)
|
||||
|
||||
# Verify tools are in correct Anthropic format
|
||||
tools = result["tools"]
|
||||
assert len(tools) == 2
|
||||
|
||||
# First tool should be preserved as Anthropic native tool
|
||||
assert tools[0]["type"] == "tool_search_tool_regex_20251119"
|
||||
assert tools[0]["name"] == "tool_search_tool_regex"
|
||||
|
||||
# Second tool should be converted to Anthropic custom tool format
|
||||
assert tools[1]["type"] == "custom"
|
||||
assert tools[1]["name"] == "get_weather"
|
||||
assert tools[1]["description"] == "Get the weather at a specific location"
|
||||
assert "input_schema" in tools[1]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the tests
|
||||
pytest.main([__file__, "-v"])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue