diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 4b6617fbeac..cdddfb14697 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -767,14 +767,18 @@ class LiteLLMAnthropicMessagesAdapter: function_chunk = ChatCompletionToolParamFunctionChunk( name=truncated_name, ) - if "input_schema" in tool: - function_chunk["parameters"] = tool["input_schema"] # type: ignore + extra_tool_params = {k: v for k, v in tool.items() if k not in mapped_tool_params} + input_schema = tool.get("input_schema") + if isinstance(input_schema, dict): + function_chunk["parameters"] = { + **copy.deepcopy(input_schema), + **extra_tool_params, + } + elif extra_tool_params: + function_chunk["parameters"] = dict(extra_tool_params) if "description" in tool: function_chunk["description"] = tool["description"] # type: ignore - for k, v in tool.items(): - if k not in mapped_tool_params: # pass additional computer kwargs - function_chunk.setdefault("parameters", {}).update({k: v}) tool_param = ChatCompletionToolParam(type="function", function=function_chunk) self._add_cache_control_if_applicable(tool, tool_param, model) new_tools.append(tool_param) # type: ignore[arg-type] 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 dfe7e0c3a51..9c9d1c90a58 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 @@ -1,3 +1,4 @@ +import copy import os import sys from typing import Any, cast @@ -3147,3 +3148,34 @@ def test_translate_anthropic_tools_to_openai_preserves_parameters_type(): params = new_tools[0]["function"]["parameters"] assert params["type"] == "object" assert new_tools[0]["type"] == "function" + + +def test_translate_anthropic_tools_to_openai_does_not_mutate_input_schema(): + """Regression for #34510: translation must not alias or mutate the caller's + input_schema; extra top-level tool keys must not leak back into it.""" + adapter = LiteLLMAnthropicMessagesAdapter() + input_schema = {"type": "object", "properties": {"a": {"type": "string"}}} + tools = [ + { + "type": "computer_20250124", + "name": "computer", + "input_schema": input_schema, + "display_width_px": 1024, + "display_height_px": 768, + } + ] + + original_tools = copy.deepcopy(tools) + new_tools, _ = adapter.translate_anthropic_tools_to_openai(tools=tools) + + assert input_schema == {"type": "object", "properties": {"a": {"type": "string"}}} + assert tools == original_tools + + params = new_tools[0]["function"]["parameters"] + assert params is not input_schema + assert params["display_width_px"] == 1024 + assert params["display_height_px"] == 768 + assert params["type"] == "object" + + second_pass, _ = adapter.translate_anthropic_tools_to_openai(tools=tools) + assert second_pass == new_tools