mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-31 06:56:59 +00:00
fix(anthropic): keep advisor_20260301 tool native through guardrails
This commit is contained in:
parent
97a13ce676
commit
f80cf658b9
4 changed files with 79 additions and 17 deletions
|
|
@ -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']}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue