fix(anthropic): keep advisor_20260301 tool native through guardrails

This commit is contained in:
Christopher Parker 2026-10-28 12:09:03 -05:00
parent 97a13ce676
commit f80cf658b9
4 changed files with 79 additions and 17 deletions

View file

@ -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']}")

View file

@ -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):

View file

@ -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

View file

@ -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()