diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 109017bda27..ca90df2ff75 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -18,6 +18,22 @@ TOOL_NAME_PREFIX_LENGTH: Final = OPENAI_MAX_TOOL_NAME_LENGTH - TOOL_NAME_HASH_LE PROVIDERS_PROXYING_AN_UNKNOWN_BACKEND: Final = frozenset({"litellm_proxy"}) +_ANTHROPIC_TOOL_SCHEMA_KEYS: Final = frozenset( + {"name", "type", "input_schema", "description", "cache_control", "strict"} +) + + +def _is_openai_function_tool(tool: Mapping[str, object]) -> bool: + return tool.get("type") == "function" and "function" in tool + + +def _is_provider_native_tool_dict(tool: Mapping[str, object]) -> bool: + if len(tool) != 1: + return False + key, value = next(iter(tool.items())) + return key not in _ANTHROPIC_TOOL_SCHEMA_KEYS and isinstance(value, dict) + + def truncate_tool_name(name: str) -> str: """ Truncate tool names that exceed OpenAI's 64-character limit. @@ -770,6 +786,10 @@ class LiteLLMAnthropicMessagesAdapter: new_tools.append(tool) continue + if _is_openai_function_tool(tool) or _is_provider_native_tool_dict(tool): + new_tools.append(cast(ChatCompletionToolParam, tool)) # cast-ok: passed through verbatim to provider + continue + raw_name = tool.get("name") if raw_name is None or (isinstance(raw_name, str) and not str(raw_name).strip()): original_name = f"litellm_unnamed_tool_{idx}" 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 ee09baf28b6..7073d702db4 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 @@ -16,6 +16,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import ( ) from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( OPENAI_MAX_TOOL_NAME_LENGTH, + AnthropicAdapter, LiteLLMAnthropicMessagesAdapter, create_tool_name_mapping, truncate_tool_name, @@ -2307,6 +2308,53 @@ def test_translate_anthropic_tools_to_openai_fills_missing_tool_name(): assert result[1]["function"]["name"] == "litellm_unnamed_tool_1" +def test_translate_anthropic_tools_to_openai_passes_provider_native_tool_dicts_through(): + """Deployment-level provider-native tools (e.g. Gemini googleMaps) must reach the provider transformation verbatim (LIT-6286).""" + tools = [ + {"googleMaps": {}}, + {"googleSearch": {}}, + { + "name": "get_weather", + "input_schema": {"type": "object", "properties": {"location": {"type": "string"}}}, + }, + ] + adapter = LiteLLMAnthropicMessagesAdapter() + result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai(tools=tools, model=None) + assert result[0] == {"googleMaps": {}} + assert result[1] == {"googleSearch": {}} + assert result[2]["function"]["name"] == "get_weather" + assert tool_name_mapping == {} + + +def test_translate_anthropic_tools_to_openai_passes_openai_function_tools_through(): + """A tool already in OpenAI function format must pass through unchanged instead of becoming litellm_unnamed_tool_N.""" + openai_tool = { + "type": "function", + "function": { + "name": "get_weather", + "parameters": {"type": "object", "properties": {"location": {"type": "string"}}}, + }, + } + adapter = LiteLLMAnthropicMessagesAdapter() + result, _ = adapter.translate_anthropic_tools_to_openai(tools=[openai_tool], model=None) + assert result == [openai_tool] + + +def test_translate_completion_input_params_keeps_provider_native_tools(): + """/v1/messages request translation must keep router-merged provider-native tools in kwargs['tools'] (LIT-6286).""" + adapter = AnthropicAdapter() + translated = adapter.translate_completion_input_params( + { + "model": "gemini/gemini-2.5-flash", + "max_tokens": 1024, + "messages": [{"role": "user", "content": "coffee shops near Union Square"}], + "tools": [{"googleMaps": {}}], + } + ) + assert translated is not None + assert translated["tools"] == [{"googleMaps": {}}] + + def test_translate_openai_content_to_anthropic_reasoning_content_without_thinking_blocks(): """ Test that reasoning_content is converted to thinking block when thinking_blocks is not present.