From b0a37e59e7343e60712f8cfe89565602b9e4ad30 Mon Sep 17 00:00:00 2001 From: Or Yaacov Date: Wed, 1 Apr 2026 14:31:34 +0300 Subject: [PATCH] fix(anthropic): prevent tool type leaking into input_schema during guardrail translation When translating Anthropic custom tools to OpenAI format for guardrails, the extra-params loop in translate_anthropic_tools_to_openai() copied every unmapped key into function.parameters. Since "type" was not in mapped_tool_params, the tool-level type:"custom" was written into parameters, overwriting input_schema.type from "object" to "custom". This caused Anthropic/Bedrock to reject the request with: tools.0.custom.input_schema.type: Input should be 'object' Two fixes: 1. Add "type" to mapped_tool_params so it's excluded from the loop 2. Use copy.copy() on input_schema to prevent shallow-copy mutation of the original request data Fixes: tools rejected after guardrail pre_call on /v1/messages endpoint Co-Authored-By: Claude Opus 4.6 (1M context) --- .../adapters/transformation.py | 4 +- ...al_pass_through_adapters_transformation.py | 60 +++++++++++++++++++ 2 files changed, 62 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 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" + )