mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-18 04:44:37 +00:00
fix(anthropic): translate the OpenAI tool_search tool on /v1/chat/completions for Claude models
This commit is contained in:
parent
6083ce413f
commit
266c4aa924
2 changed files with 154 additions and 1 deletions
|
|
@ -138,6 +138,31 @@ else:
|
|||
_ANTHROPIC_TOOL_NAME_INVALID_CHARS: Final = re.compile(r"[^a-zA-Z0-9_-]")
|
||||
_ANTHROPIC_TOOL_NAME_MAX_LEN: Final = 128
|
||||
_DEFER_LOADING_ADAPTER: Final = TypeAdapter(bool)
|
||||
_TOOL_FIELDS_ADAPTER: Final = TypeAdapter(Mapping[str, object])
|
||||
_TOOLS_ADAPTER: Final = TypeAdapter(list[object])
|
||||
_DEFERRAL_CARRYING_TOOL_KEYS: Final = frozenset({"function", "tools"})
|
||||
|
||||
|
||||
def _is_openai_tool_search_tool(tool: object) -> bool:
|
||||
match tool:
|
||||
case {"type": "tool_search"}:
|
||||
return True
|
||||
case _:
|
||||
return False
|
||||
|
||||
|
||||
def _without_deferral(tool: object) -> object:
|
||||
match tool:
|
||||
case Mapping():
|
||||
return {
|
||||
key: _without_deferral(value) if key in _DEFERRAL_CARRYING_TOOL_KEYS else value
|
||||
for key, value in _TOOL_FIELDS_ADAPTER.validate_python(tool).items()
|
||||
if key != "defer_loading"
|
||||
}
|
||||
case list():
|
||||
return [_without_deferral(member) for member in _TOOLS_ADAPTER.validate_python(tool)]
|
||||
case _:
|
||||
return tool
|
||||
|
||||
|
||||
def _validated_defer_loading(value: object) -> bool:
|
||||
|
|
@ -296,6 +321,11 @@ DROP_UNSUPPORTED_SPEED_WARNING: Final = (
|
|||
"Dropping unsupported `speed` for model=%s (drop_params=True). Fast mode is only supported on select Opus models."
|
||||
)
|
||||
|
||||
DROP_UNSUPPORTED_TOOL_SEARCH_WARNING: Final = (
|
||||
"Dropping the OpenAI `tool_search` tool and the `defer_loading` of the other tools for model=%s on %s "
|
||||
"(drop_params=True): this route cannot run tool search."
|
||||
)
|
||||
|
||||
|
||||
class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
||||
"""
|
||||
|
|
@ -1000,6 +1030,32 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
initial_tool["authorization_token"] = authorization_token
|
||||
return initial_tool
|
||||
|
||||
def _tools_with_openai_tool_search_mapped(
|
||||
self, tools: Sequence[object], model: str, drop_params: bool
|
||||
) -> Sequence[object]:
|
||||
from litellm.responses.litellm_completion_transformation.tool_search import (
|
||||
anthropic_tool_search_chat_tool,
|
||||
is_server_tool_search_tool,
|
||||
)
|
||||
|
||||
search_tools: Final = tuple(tool for tool in tools if _is_openai_tool_search_tool(tool))
|
||||
if not search_tools:
|
||||
return tools
|
||||
if all(is_server_tool_search_tool(tool) for tool in search_tools) and litellm.supports_tool_search(
|
||||
model, self._resolved_provider
|
||||
):
|
||||
return [anthropic_tool_search_chat_tool() if _is_openai_tool_search_tool(tool) else tool for tool in tools]
|
||||
if not (litellm.drop_params or drop_params):
|
||||
raise litellm.utils.UnsupportedParamsError(
|
||||
message=(
|
||||
f"{model} on {self._resolved_provider} cannot run the OpenAI `tool_search` tool. To drop it "
|
||||
"and the `defer_loading` of the other tools, set `litellm.drop_params = True`."
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
litellm.verbose_logger.warning(DROP_UNSUPPORTED_TOOL_SEARCH_WARNING, model, self._resolved_provider)
|
||||
return [_without_deferral(tool) for tool in tools if not _is_openai_tool_search_tool(tool)]
|
||||
|
||||
def _map_tools(
|
||||
self,
|
||||
tools: list,
|
||||
|
|
@ -1568,7 +1624,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
if param == "max_tokens" or param == "max_completion_tokens":
|
||||
optional_params["max_tokens"] = value if isinstance(value, int) else max(1, int(round(value)))
|
||||
elif param == "tools":
|
||||
anthropic_tools, mcp_servers = self._map_tools(value)
|
||||
anthropic_tools, mcp_servers = self._map_tools(
|
||||
list(self._tools_with_openai_tool_search_mapped(tools=value, model=model, drop_params=drop_params))
|
||||
)
|
||||
optional_params = self._add_tools_to_optional_params(
|
||||
optional_params=optional_params, tools=anthropic_tools
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8345,3 +8345,98 @@ def test_completion_surfaces_response_citations_in_provider_specific_fields(resp
|
|||
assert response.choices[0].message.provider_specific_fields["citations"] == [
|
||||
[{**citation, "supported_text": "the grass is green"}]
|
||||
]
|
||||
|
||||
|
||||
def _openai_tool_search_with_deferred_functions(execution: str = "server") -> list[dict[str, object]]:
|
||||
return [
|
||||
{"type": "tool_search", "execution": execution},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather", "parameters": {"type": "object", "properties": {}}},
|
||||
"defer_loading": True,
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_stock_price",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
"defer_loading": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "namespace",
|
||||
"name": "crm",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "lookup_customer",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
"defer_loading": True,
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
_DEFERRED_FUNCTION_NAMES: Final = ["get_weather", "get_stock_price", "lookup_customer"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "model", "provider"),
|
||||
[
|
||||
pytest.param(AnthropicConfig(), "claude-sonnet-5-5", "anthropic", id="anthropic"),
|
||||
pytest.param(AmazonAnthropicClaudeConfig(), "us.anthropic.claude-sonnet-5-5", "bedrock", id="bedrock-invoke"),
|
||||
pytest.param(VertexAIAnthropicConfig(), "claude-sonnet-5-5", "vertex_ai", id="vertex_ai"),
|
||||
],
|
||||
)
|
||||
def test_map_openai_params_translates_the_openai_tool_search_tool_on_a_route_that_runs_it(config, model, provider):
|
||||
from litellm.responses.litellm_completion_transformation.tool_search import anthropic_tool_search_chat_tool
|
||||
|
||||
assert litellm.supports_tool_search(model, provider)
|
||||
mapped: Final = config.map_openai_params(
|
||||
non_default_params={"tools": _openai_tool_search_with_deferred_functions()},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
)
|
||||
tools: Final = mapped["tools"]
|
||||
assert {key: tools[0][key] for key in ("type", "name")} == anthropic_tool_search_chat_tool()
|
||||
assert [tool["name"] for tool in tools[1:]] == _DEFERRED_FUNCTION_NAMES
|
||||
assert all(tool["defer_loading"] is True for tool in tools[1:])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "model", "provider", "execution"),
|
||||
[
|
||||
pytest.param(AzureAnthropicConfig(), "claude-sonnet-5-5", "azure_ai", "server", id="route-without-tool-search"),
|
||||
pytest.param(AnthropicConfig(), "claude-3-haiku-20240307", "anthropic", "server", id="model-without-tool-search"),
|
||||
pytest.param(AnthropicConfig(), "claude-sonnet-5-5", None, "client", id="client-executed-search"),
|
||||
],
|
||||
)
|
||||
def test_map_openai_params_answers_400_for_an_openai_tool_search_tool_it_cannot_run(
|
||||
monkeypatch, config, model, provider, execution
|
||||
):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
assert provider is None or not litellm.supports_tool_search(model, provider)
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError) as raised:
|
||||
config.map_openai_params(
|
||||
non_default_params={"tools": _openai_tool_search_with_deferred_functions(execution=execution)},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
)
|
||||
assert raised.value.status_code == 400
|
||||
assert "tool_search" in str(raised.value)
|
||||
|
||||
|
||||
def test_map_openai_params_drops_the_openai_tool_search_tool_and_every_deferral_under_drop_params(monkeypatch):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
assert not litellm.supports_tool_search("claude-sonnet-5-5", "azure_ai")
|
||||
mapped: Final = AzureAnthropicConfig().map_openai_params(
|
||||
non_default_params={"tools": _openai_tool_search_with_deferred_functions()},
|
||||
optional_params={},
|
||||
model="claude-sonnet-5-5",
|
||||
drop_params=True,
|
||||
)
|
||||
assert [tool["name"] for tool in mapped["tools"]] == _DEFERRED_FUNCTION_NAMES
|
||||
assert all("defer_loading" not in tool for tool in mapped["tools"])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue