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:
Sameer Kankute 2026-03-13 11:34:18 +05:30
parent 9cd7ad2634
commit 45ba9e1f7e
5 changed files with 87 additions and 2 deletions

View file

@ -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(

View file

@ -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")

View file

@ -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)

View file

@ -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):

View file

@ -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"])