diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 92e7c84031c..3f0f3ee1d22 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1027,6 +1027,24 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): _computer_tool["display_number"] = _display_number returned_tool = _computer_tool + # Before the hosted-tools prefix match so the advisor keeps its model validation. + elif tool["type"] == ANTHROPIC_ADVISOR_TOOL_TYPE: + from litellm.types.llms.anthropic import AnthropicAdvisorTool + + _tool_dict: Final = cast(dict, tool) + advisor_model: Final = _tool_dict.get("model") + if not isinstance(advisor_model, str): + raise ValueError("Advisor tool must have a valid model") + _advisor_tool: Final = AnthropicAdvisorTool( + type=ANTHROPIC_ADVISOR_TOOL_TYPE, + name="advisor", + model=advisor_model, + ) + if _tool_dict.get("max_uses") is not None: + _advisor_tool["max_uses"] = _tool_dict["max_uses"] + if _tool_dict.get("caching") is not None: + _advisor_tool["caching"] = _tool_dict["caching"] + returned_tool = _advisor_tool elif any(tool["type"].startswith(t) for t in ANTHROPIC_HOSTED_TOOLS): function_name_obj: Final = tool.get("name", tool.get("function", {}).get("name")) if function_name_obj is None or not isinstance(function_name_obj, str): @@ -1071,23 +1089,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): type="tool_search_tool_bm25_20251119", name=tool_name, ) - elif tool["type"] == ANTHROPIC_ADVISOR_TOOL_TYPE: - from litellm.types.llms.anthropic import AnthropicAdvisorTool - - _tool_dict: Final = cast(dict, tool) - advisor_model: Final = _tool_dict.get("model") - if not isinstance(advisor_model, str): - raise ValueError("Advisor tool must have a valid model") - _advisor_tool: Final = AnthropicAdvisorTool( - type=ANTHROPIC_ADVISOR_TOOL_TYPE, - name="advisor", - model=advisor_model, - ) - if _tool_dict.get("max_uses") is not None: - _advisor_tool["max_uses"] = _tool_dict["max_uses"] - if _tool_dict.get("caching") is not None: - _advisor_tool["caching"] = _tool_dict["caching"] - returned_tool = _advisor_tool if returned_tool is None and mcp_server is None: raise ValueError(f"Unsupported tool type: {tool['type']}") diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index de83e6ea66e..f58d86ac65c 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -797,6 +797,7 @@ class ANTHROPIC_HOSTED_TOOLS(str, Enum): WEB_FETCH = "web_fetch" MEMORY = "memory" TOOL_SEARCH_TOOL = "tool_search_tool" + ADVISOR = "advisor" class ANTHROPIC_BETA_HEADER_VALUES(str, Enum): diff --git a/tests/unit/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py b/tests/unit/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py index 9e88b59adc9..1f703d4479e 100644 --- a/tests/unit/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py +++ b/tests/unit/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py @@ -1602,6 +1602,52 @@ class TestAnthropicMessagesHandlerInputProcessing: assert tools[1]["description"] == "Get the weather at a specific location" assert "input_schema" in tools[1] + @pytest.mark.asyncio + async def test_process_input_messages_keeps_advisor_tool_native(self): + """The advisor_20260301 server tool must survive the guardrail round trip untouched. + + Claude Code sends the advisor beside its function tools. The guardrail path + translates tools to OpenAI format and back; before the fix the advisor was not + recognised as a hosted tool and came back as an empty custom tool, so Anthropic + treated it as a client tool and Claude Code failed with + "No such tool available: advisor". + """ + handler = AnthropicMessagesHandler() + guardrail = MockPassThroughGuardrail(guardrail_name="test") + advisor_tool = { + "type": "advisor_20260301", + "name": "advisor", + "model": "claude-opus-4-6", + "defer_loading": True, + } + data = { + "model": "claude-sonnet-4-6", + "messages": [{"role": "user", "content": "Consult the advisor, then answer: what is 2+2?"}], + "tools": [ + advisor_tool, + { + "name": "Bash", + "description": "Run a shell command", + "input_schema": { + "type": "object", + "properties": {"command": {"type": "string"}}, + "required": ["command"], + }, + }, + ], + } + + result = await handler.process_input_messages( + data=data, guardrail_to_apply=guardrail, litellm_logging_obj=MagicMock() + ) + + tools = result["tools"] + assert len(tools) == 2 + assert tools[0] == advisor_tool + assert tools[1]["type"] == "custom" + assert tools[1]["name"] == "Bash" + assert tools[1]["description"] == "Run a shell command" + class ToolAppendingGuardrail(CustomGuardrail): """Guardrail that appends a new OpenAI-format function tool, mimicking a diff --git a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py index 4e4b89b764b..5182493ffdd 100644 --- a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py @@ -2835,6 +2835,20 @@ def test_translate_anthropic_tools_to_openai_passes_openai_function_tools_throug assert result == [openai_tool] +def test_translate_anthropic_tools_to_openai_keeps_advisor_tool_native(): + """The advisor_20260301 server tool has no OpenAI equivalent and must pass through verbatim.""" + advisor_tool = { + "type": "advisor_20260301", + "name": "advisor", + "model": "claude-opus-4-6", + "defer_loading": True, + } + adapter = LiteLLMAnthropicMessagesAdapter() + result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai(tools=[advisor_tool], model=None) + assert result == [advisor_tool] + assert tool_name_mapping == {} + + 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()