fix(anthropic/adapter): stop tool-level "type" leaking into parameters

Add "type" to mapped_tool_params exclusion list so Anthropic's tool-level
type (e.g. "custom") does not overwrite input_schema's "type": "object"
in the translated parameters dict. Deep-copy input_schema to prevent any
downstream mutation from corrupting the original tool definition.
This commit is contained in:
yryzhan 2026-05-20 15:03:55 +02:00
parent e59e34bed3
commit 9df050140c
2 changed files with 63 additions and 2 deletions

View file

@ -805,7 +805,13 @@ 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 idx, tool in enumerate(tools):
# Check if this is an Anthropic-native tool that should be kept as-is
@ -832,7 +838,7 @@ class LiteLLMAnthropicMessagesAdapter:
name=truncated_name,
)
if "input_schema" in tool:
function_chunk["parameters"] = tool["input_schema"] # type: ignore
function_chunk["parameters"] = copy.deepcopy(tool["input_schema"]) # type: ignore
if "description" in tool:
function_chunk["description"] = tool["description"] # type: ignore

View file

@ -2427,3 +2427,58 @@ def test_translate_anthropic_tool_choice_none():
result = adapter.translate_anthropic_tool_choice_to_openai({"type": "none"})
assert result == "none"
def test_translate_anthropic_tools_type_not_leaked_into_parameters():
"""
Regression: tool-level "type" key (e.g. "custom") must not leak into
function_chunk["parameters"] via the catch-all loop. Additionally,
input_schema must be shallow-copied so mutations to parameters don't
affect the original tool dict.
"""
original_input_schema = {"type": "object", "properties": {"q": {"type": "string"}}}
tools = [
{
"name": "my_tool",
"type": "custom",
"description": "A custom tool",
"input_schema": original_input_schema,
}
]
adapter = LiteLLMAnthropicMessagesAdapter()
result, _ = adapter.translate_anthropic_tools_to_openai(tools=tools, model=None)
assert len(result) == 1
params = result[0]["function"]["parameters"]
# "type" in parameters should be from input_schema ("object"), not tool-level ("custom")
assert params["type"] == "object"
assert "custom" not in params.values()
# Deep copy guarantee: modifying nested keys doesn't affect original
params["properties"]["injected"] = {"type": "string"}
assert "injected" not in original_input_schema["properties"]
def test_translate_anthropic_tools_type_not_leaked_without_input_schema():
"""
When a tool has "type" but no "input_schema", "type" must still not
appear in the output parameters dict via the catch-all loop.
"""
tools = [
{
"name": "bare_tool",
"type": "custom",
"description": "Tool without input_schema",
}
]
adapter = LiteLLMAnthropicMessagesAdapter()
result, _ = adapter.translate_anthropic_tools_to_openai(tools=tools, model=None)
assert len(result) == 1
func = result[0]["function"]
assert func["name"] == "bare_tool"
# With no input_schema and all top-level keys excluded, parameters should be empty.
params = func.get("parameters", {})
assert params == {}, f"expected empty parameters, got {params!r}"