From 9df050140c4f9b0828d42192157af535bc68c9d2 Mon Sep 17 00:00:00 2001 From: yryzhan Date: Wed, 20 May 2026 15:03:55 +0200 Subject: [PATCH] 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. --- .../adapters/transformation.py | 10 +++- ...al_pass_through_adapters_transformation.py | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 0e198daf089..8857c68e528 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -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 diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 11465e6f718..96b08a65c55 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -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}"