mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
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:
parent
e59e34bed3
commit
9df050140c
2 changed files with 63 additions and 2 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue