mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-17 04:34:25 +00:00
fix(anthropic): run tool search on azure_ai, gate it on Databricks Claude, and strip deferral without recursion
Anthropic's Foundry docs list no tool search exclusion for either hosting option, so the claude-tool-search rule now fills azure_ai. Databricks Claude models route their tools through the same unsupported-tool-search gate, so a tool_search tool answers 400 or drops under drop_params instead of a 500. _without_deferral strips two fixed levels instead of walking recursively.
This commit is contained in:
parent
07516c334b
commit
bf3de2f5cb
8 changed files with 105 additions and 31 deletions
|
|
@ -147,7 +147,6 @@ _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 _tool_type(tool: object) -> str | None:
|
||||
|
|
@ -166,18 +165,47 @@ def _is_anthropic_tool_search_tool(tool: object) -> bool:
|
|||
return _tool_type(tool) in ANTHROPIC_TOOL_SEARCH_TOOL_TYPES
|
||||
|
||||
|
||||
def _without_deferral(tool: object) -> object:
|
||||
match tool:
|
||||
def _without_defer_loading(fields: object) -> object:
|
||||
match fields:
|
||||
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()
|
||||
key: value
|
||||
for key, value in _TOOL_FIELDS_ADAPTER.validate_python(fields).items()
|
||||
if key != "defer_loading"
|
||||
}
|
||||
case list():
|
||||
return [_without_deferral(member) for member in _TOOLS_ADAPTER.validate_python(tool)]
|
||||
case _:
|
||||
return tool
|
||||
return fields
|
||||
|
||||
|
||||
def _member_without_deferral(member: object) -> object:
|
||||
match member:
|
||||
case Mapping():
|
||||
return {
|
||||
key: _without_defer_loading(value) if key == "function" else value
|
||||
for key, value in _TOOL_FIELDS_ADAPTER.validate_python(member).items()
|
||||
if key != "defer_loading"
|
||||
}
|
||||
case _:
|
||||
return member
|
||||
|
||||
|
||||
def _namespace_members_without_deferral(members: object) -> object:
|
||||
match members:
|
||||
case list():
|
||||
return [_member_without_deferral(member) for member in _TOOLS_ADAPTER.validate_python(members)]
|
||||
case _:
|
||||
return members
|
||||
|
||||
|
||||
def _without_deferral(tool: object) -> object:
|
||||
match _member_without_deferral(tool):
|
||||
case Mapping() as stripped:
|
||||
return {
|
||||
key: _namespace_members_without_deferral(value) if key == "tools" else value
|
||||
for key, value in _TOOL_FIELDS_ADAPTER.validate_python(stripped).items()
|
||||
}
|
||||
case stripped:
|
||||
return stripped
|
||||
|
||||
|
||||
def _validated_defer_loading(value: object) -> bool:
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ from litellm.types.utils import (
|
|||
from ...anthropic.chat.transformation import (
|
||||
REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT,
|
||||
AnthropicConfig,
|
||||
tools_without_unsupported_tool_search,
|
||||
)
|
||||
from ...openai_like.chat.transformation import OpenAILikeChatConfig
|
||||
from ..common_utils import DatabricksBase, DatabricksException
|
||||
|
|
@ -65,6 +66,7 @@ from ..common_utils import DatabricksBase, DatabricksException
|
|||
_RESPONSE_FORMAT_ADAPTER: ( # mutable-ok: mirrors the dict return contract of get_json_schema_from_pydantic_object
|
||||
Final[TypeAdapter[dict[str, object] | None]]
|
||||
) = TypeAdapter(dict[str, object] | None)
|
||||
_TOOLS_ADAPTER: Final = TypeAdapter(list[object])
|
||||
|
||||
|
||||
def _is_bare_assistant_message(message_dict: Mapping[str, object]) -> bool:
|
||||
|
|
@ -317,13 +319,22 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
function=function_params,
|
||||
)
|
||||
|
||||
def _map_openai_to_dbrx_tool(self, model: str, tools: list) -> list[DatabricksTool]:
|
||||
def _map_openai_to_dbrx_tool(self, model: str, tools: list, drop_params: bool) -> list[DatabricksTool]:
|
||||
# if not claude, send as is
|
||||
if "claude" not in model:
|
||||
return tools
|
||||
|
||||
# if claude, convert to anthropic tool and then to databricks tool
|
||||
anthropic_tools, _ = self._map_tools(tools=tools) # unclear how mcp tool calling on databricks works
|
||||
anthropic_tools, _ = self._map_tools(
|
||||
tools=list(
|
||||
tools_without_unsupported_tool_search(
|
||||
tools=_TOOLS_ADAPTER.validate_python(tools),
|
||||
model=model,
|
||||
route="databricks",
|
||||
drop_params=drop_params,
|
||||
)
|
||||
)
|
||||
) # unclear how mcp tool calling on databricks works
|
||||
databricks_tools: Final = [
|
||||
cast(DatabricksTool, self.convert_anthropic_tool_to_databricks_tool(tool)) for tool in anthropic_tools
|
||||
]
|
||||
|
|
@ -374,7 +385,9 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
model=model, optional_params=mapped_params, custom_llm_provider="databricks"
|
||||
)
|
||||
if "tools" in mapped_params:
|
||||
mapped_params["tools"] = self._map_openai_to_dbrx_tool(model=model, tools=mapped_params["tools"])
|
||||
mapped_params["tools"] = self._map_openai_to_dbrx_tool(
|
||||
model=model, tools=mapped_params["tools"], drop_params=drop_params
|
||||
)
|
||||
if "max_completion_tokens" in non_default_params and replace_max_completion_tokens_with_max_tokens:
|
||||
mapped_params["max_tokens"] = non_default_params[
|
||||
"max_completion_tokens"
|
||||
|
|
|
|||
|
|
@ -60472,8 +60472,8 @@
|
|||
{
|
||||
"name": "claude-tool-search",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[5-9]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"fill_missing_for_providers": ["anthropic", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.5 or higher, in any id shape that contains claude-<family>-: minors 4.5 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Anthropic's tool search docs list every Claude 4.5 and newer model as supported and Opus 4.1 and earlier as unsupported, so the flag follows the version instead of a per-model list. azure_ai is left out on purpose: Anthropic documents tool search as unavailable on Azure-hosted Foundry deployments, and the azure_ai/ key cannot tell those from Anthropic-hosted ones.",
|
||||
"fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.5 or higher, in any id shape that contains claude-<family>-: minors 4.5 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Anthropic's tool search docs list every Claude 4.5 and newer model as supported and Opus 4.1 and earlier as unsupported, so the flag follows the version instead of a per-model list. azure_ai is included because Anthropic's Claude in Microsoft Foundry page (platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry, read 2026-10-13) lists no tool search exclusion for either hosting option, and Microsoft's 2026-08-17 Foundry post names Tool search as available on deployments hosted on Azure.",
|
||||
"model_info": {
|
||||
"supports_tool_search": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60472,8 +60472,8 @@
|
|||
{
|
||||
"name": "claude-tool-search",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[5-9]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"fill_missing_for_providers": ["anthropic", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.5 or higher, in any id shape that contains claude-<family>-: minors 4.5 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Anthropic's tool search docs list every Claude 4.5 and newer model as supported and Opus 4.1 and earlier as unsupported, so the flag follows the version instead of a per-model list. azure_ai is left out on purpose: Anthropic documents tool search as unavailable on Azure-hosted Foundry deployments, and the azure_ai/ key cannot tell those from Anthropic-hosted ones.",
|
||||
"fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.5 or higher, in any id shape that contains claude-<family>-: minors 4.5 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Anthropic's tool search docs list every Claude 4.5 and newer model as supported and Opus 4.1 and earlier as unsupported, so the flag follows the version instead of a per-model list. azure_ai is included because Anthropic's Claude in Microsoft Foundry page (platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry, read 2026-10-13) lists no tool search exclusion for either hosting option, and Microsoft's 2026-08-17 Foundry post names Tool search as available on deployments hosted on Azure.",
|
||||
"model_info": {
|
||||
"supports_tool_search": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -970,19 +970,16 @@ def test_shipped_tool_search_rule_version_boundaries(shipped_cost_map, model, pr
|
|||
|
||||
def test_shipped_tool_search_rule_fills_mapped_claude_entries_without_flag(shipped_cost_map):
|
||||
"""A mapped Claude 4.5+ entry with no supports_tool_search key gets it from the rule
|
||||
on Anthropic direct, Vertex and Bedrock, a mapped pre-4.5 entry stays without one,
|
||||
and Azure Foundry and reseller copies of the same model are not touched."""
|
||||
on Anthropic direct, Vertex, Bedrock and Azure Foundry, a mapped pre-4.5 entry stays
|
||||
without one, and reseller copies of the same model are not touched."""
|
||||
for key, model, provider in (
|
||||
("claude-opus-4-7", "claude-opus-4-7", "anthropic"),
|
||||
("vertex_ai/claude-opus-5", "claude-opus-5", "vertex_ai"),
|
||||
("azure_ai/claude-opus-5", "claude-opus-5", "azure_ai"),
|
||||
):
|
||||
assert "supports_tool_search" not in litellm.model_cost[key]
|
||||
assert litellm.get_model_info(model, custom_llm_provider=provider)["supports_tool_search"] is True
|
||||
|
||||
assert "supports_tool_search" not in litellm.model_cost["azure_ai/claude-opus-5"]
|
||||
azure_opus_5_info = litellm.get_model_info("claude-opus-5", custom_llm_provider="azure_ai")
|
||||
assert azure_opus_5_info.get("supports_tool_search") is None
|
||||
|
||||
assert match_fill_missing_generalizations("claude-opus-5", "bedrock")["supports_tool_search"] is True
|
||||
assert "supports_tool_search" not in match_fill_missing_generalizations("claude-opus-5", "azure_ai")
|
||||
assert match_fill_missing_generalizations("claude-opus-5", "azure_ai")["supports_tool_search"] is True
|
||||
assert match_fill_missing_generalizations("claude-opus-5", "perplexity") is None
|
||||
|
|
|
|||
|
|
@ -8808,6 +8808,7 @@ def _deferral_by_function_name(tools: list[dict[str, object]]) -> dict[object, o
|
|||
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"),
|
||||
pytest.param(AzureAnthropicConfig(), "claude-sonnet-5-5", "azure_ai", id="azure_ai"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -8837,8 +8838,8 @@ def test_map_openai_params_sends_one_anthropic_search_tool_and_keeps_deferral_on
|
|||
@pytest.mark.parametrize(
|
||||
("config", "model"),
|
||||
[
|
||||
pytest.param(AnthropicConfig(), "claude-sonnet-5-5", id="route-with-tool-search"),
|
||||
pytest.param(AzureAnthropicConfig(), "claude-sonnet-5-5", id="route-without-tool-search"),
|
||||
pytest.param(AnthropicConfig(), "claude-sonnet-5-5", id="model-with-tool-search"),
|
||||
pytest.param(AnthropicConfig(), "claude-3-haiku-20240307", id="model-without-tool-search"),
|
||||
],
|
||||
)
|
||||
def test_map_openai_params_lets_a_native_anthropic_search_tool_replace_the_openai_one(
|
||||
|
|
@ -8892,7 +8893,6 @@ def test_map_openai_params_judges_tool_search_on_the_real_model_when_tool_based_
|
|||
|
||||
|
||||
_UNRUNNABLE_TOOL_SEARCH_CASES: Final = [
|
||||
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"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2255,3 +2255,39 @@ def test_transform_response_carries_served_service_tier(served_tier: str | None)
|
|||
)
|
||||
|
||||
assert result.to_dict().get("service_tier") == served_tier
|
||||
|
||||
|
||||
_DATABRICKS_TOOLS_WITH_OPENAI_TOOL_SEARCH: Final = [
|
||||
{"type": "tool_search", "execution": "server"},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather", "parameters": {"type": "object", "properties": {}}},
|
||||
"defer_loading": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_databricks_claude_answers_400_for_the_openai_tool_search_tool_without_drop_params(monkeypatch):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError) as raised:
|
||||
DatabricksConfig().map_openai_params(
|
||||
non_default_params={"tools": _DATABRICKS_TOOLS_WITH_OPENAI_TOOL_SEARCH},
|
||||
optional_params={},
|
||||
model="databricks-claude-opus-5-5",
|
||||
drop_params=False,
|
||||
)
|
||||
assert raised.value.status_code == 400
|
||||
assert "tool_search" in str(raised.value)
|
||||
|
||||
|
||||
def test_databricks_claude_drops_the_openai_tool_search_tool_under_drop_params(monkeypatch):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
mapped: Final = DatabricksConfig().map_openai_params(
|
||||
non_default_params={"tools": _DATABRICKS_TOOLS_WITH_OPENAI_TOOL_SEARCH},
|
||||
optional_params={},
|
||||
model="databricks-claude-opus-5-5",
|
||||
drop_params=True,
|
||||
)
|
||||
assert mapped["tools"] == [
|
||||
{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object", "properties": {}}}}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1811,14 +1811,14 @@ class TestToolTransformation:
|
|||
assert [tool["type"] for tool in result["tools"]] == ["function", "function"]
|
||||
|
||||
def test_dropping_the_search_tool_undefers_the_remaining_tools(self) -> None:
|
||||
model: Final = "azure_ai/claude-sonnet-5-5"
|
||||
assert litellm.supports_tool_search(model, "azure_ai") is False
|
||||
model: Final = "anthropic/claude-3-haiku-20240307"
|
||||
assert litellm.supports_tool_search(model, "anthropic") is False
|
||||
|
||||
result: Final = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request(
|
||||
model=model,
|
||||
input="What is the weather in Paris right now?",
|
||||
responses_api_request={"tools": _deferred_tools_with_server_tool_search()},
|
||||
custom_llm_provider="azure_ai",
|
||||
custom_llm_provider="anthropic",
|
||||
)
|
||||
|
||||
assert [tool["type"] for tool in result["tools"]] == ["function", "function"]
|
||||
|
|
@ -1876,8 +1876,8 @@ class TestToolTransformation:
|
|||
assert [tool["defer_loading"] for tool in chat_tools if tool["type"] == "function"] == [True, True]
|
||||
|
||||
def test_native_search_tool_passes_through_routes_without_tool_search(self) -> None:
|
||||
model: Final = "azure_ai/claude-sonnet-5-5"
|
||||
assert litellm.supports_tool_search(model, "azure_ai") is False
|
||||
model: Final = "anthropic/claude-3-haiku-20240307"
|
||||
assert litellm.supports_tool_search(model, "anthropic") is False
|
||||
tools: Final = [
|
||||
{"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"},
|
||||
*_deferred_tools_with_server_tool_search()[1:],
|
||||
|
|
@ -1887,7 +1887,7 @@ class TestToolTransformation:
|
|||
model=model,
|
||||
input="What is the weather in Paris right now?",
|
||||
responses_api_request={"tools": tools},
|
||||
custom_llm_provider="azure_ai",
|
||||
custom_llm_provider="anthropic",
|
||||
)
|
||||
|
||||
chat_tools: Final = result["tools"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue