From 2b3d57eb183912b61db85b158d933307067f8d38 Mon Sep 17 00:00:00 2001 From: Christopher Parker Date: Fri, 16 Oct 2026 08:25:09 -0500 Subject: [PATCH] fix(anthropic): keep advisor_20260301 tool native through guardrails The guardrail translation for /v1/messages converts tools to OpenAI format and back. translate_anthropic_tools_to_openai only keeps a tool in Anthropic format when its type starts with an ANTHROPIC_HOSTED_TOOLS member, and advisor was never added when the advisor type landed, so with any guardrail configured the advisor came back as {"name": "advisor", "type": "custom", "input_schema": {...empty...}}. Anthropic then treated it as a client tool and Claude Code failed with "No such tool available: advisor". Add advisor to ANTHROPIC_HOSTED_TOOLS so the adapter passes it through verbatim, and move the advisor branch in _map_tool_helper above the hosted-tools prefix match so the advisor keeps its model validation. --- litellm/llms/anthropic/chat/transformation.py | 35 +++++++------- litellm/types/llms/anthropic.py | 1 + .../test_anthropic_guardrail_handler.py | 46 +++++++++++++++++++ ...al_pass_through_adapters_transformation.py | 14 ++++++ 4 files changed, 79 insertions(+), 17 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 0b2f75e352b..9795ee4893e 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -999,6 +999,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): @@ -1043,23 +1061,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 c3d257117ed..1636faa6633 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -780,6 +780,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 892736b5043..69992a8b18d 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 @@ -1499,6 +1499,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/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 2f0a7ad070b..cd5d0ffbb06 100644 --- a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -2832,6 +2832,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()