fix(anthropic): prevent tool type leaking into input_schema during guardrail translation

When translating Anthropic custom tools to OpenAI format for guardrails,
the extra-params loop in translate_anthropic_tools_to_openai() copied
every unmapped key into function.parameters. Since "type" was not in
mapped_tool_params, the tool-level type:"custom" was written into
parameters, overwriting input_schema.type from "object" to "custom".

This caused Anthropic/Bedrock to reject the request with:
  tools.0.custom.input_schema.type: Input should be 'object'

Two fixes:
1. Add "type" to mapped_tool_params so it's excluded from the loop
2. Use copy.copy() on input_schema to prevent shallow-copy mutation
   of the original request data

Fixes: tools rejected after guardrail pre_call on /v1/messages endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Or Yaacov 2026-04-01 14:31:34 +03:00
parent 9343aeefca
commit b0a37e59e7
2 changed files with 62 additions and 2 deletions

View file

@ -794,7 +794,7 @@ class LiteLLMAnthropicMessagesAdapter:
"""
new_tools: List[ChatCompletionToolParam] = []
tool_name_mapping: Dict[str, str] = {}
mapped_tool_params = ["name", "input_schema", "description", "cache_control"]
mapped_tool_params = ["name", "input_schema", "description", "cache_control", "type"]
for tool in tools:
# Check if this is an Anthropic-native tool that should be kept as-is
@ -815,7 +815,7 @@ class LiteLLMAnthropicMessagesAdapter:
name=truncated_name,
)
if "input_schema" in tool:
function_chunk["parameters"] = tool["input_schema"] # type: ignore
function_chunk["parameters"] = copy.copy(tool["input_schema"]) # type: ignore
if "description" in tool:
function_chunk["description"] = tool["description"] # type: ignore

View file

@ -2111,3 +2111,63 @@ class TestTranslateAnthropicOutputFormatToOpenAI:
assert self.adapter.translate_anthropic_output_format_to_openai("invalid") is None
assert self.adapter.translate_anthropic_output_format_to_openai({"type": "text"}) is None
assert self.adapter.translate_anthropic_output_format_to_openai({"type": "json_schema"}) is None
class TestTranslateAnthropicToolsToOpenAI:
"""Tests for translate_anthropic_tools_to_openai tool format preservation."""
def setup_method(self):
self.adapter = LiteLLMAnthropicMessagesAdapter()
def test_custom_tool_type_not_leaked_into_parameters(self):
"""
Regression: the 'type' field from a custom Anthropic tool must NOT
leak into function.parameters. Previously, the extra-params loop
copied every unmapped key into parameters, overwriting
input_schema.type from 'object' to 'custom'.
"""
tools = [
{
"type": "custom",
"name": "Write",
"input_schema": {
"type": "object",
"properties": {
"file_path": {"type": "string"},
"content": {"type": "string"},
},
"required": ["file_path", "content"],
},
}
]
result, _ = self.adapter.translate_anthropic_tools_to_openai(tools=tools)
assert len(result) == 1
params = result[0]["function"]["parameters"]
assert params["type"] == "object", (
f"Expected parameters.type='object', got '{params.get('type')}'. "
"The tool-level 'type' field leaked into parameters."
)
def test_input_schema_not_mutated_by_translation(self):
"""
The original input_schema dict must not be mutated when
translating to OpenAI format (prevents shallow-copy corruption).
"""
original_schema = {
"type": "object",
"properties": {"q": {"type": "string"}},
}
tools = [
{
"type": "custom",
"name": "Search",
"input_schema": original_schema,
}
]
self.adapter.translate_anthropic_tools_to_openai(tools=tools)
assert original_schema["type"] == "object", (
"Original input_schema was mutated by translation"
)
assert "name" not in original_schema, (
"Extra keys leaked into the original input_schema"
)