diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index ed49943b7fe..d5ff48e22d6 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -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 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 ae970e1ff06..bf81fd98dd1 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 @@ -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" + )