mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix(anthropic): guard against empty tool_use names in messages adapter
When translating Anthropic messages to OpenAI format, a tool_use block whose name is missing or blank was passed through verbatim, producing an empty function name. OpenAI-compatible backends such as Mistral via vLLM reject those with InvalidFunctionCallException, and a None name crashed outright in truncate_tool_name with a TypeError. The tool definitions path already guarded this case by substituting a placeholder; this applies the same guard to the tool_use path via a shared resolve_tool_name helper Fixes #30515
This commit is contained in:
parent
20dabb781a
commit
b0bf29170b
2 changed files with 78 additions and 7 deletions
|
|
@ -47,6 +47,19 @@ def truncate_tool_name(name: str) -> str:
|
|||
return f"{name[:TOOL_NAME_PREFIX_LENGTH]}_{name_hash}"
|
||||
|
||||
|
||||
def resolve_tool_name(raw_name: Optional[str], fallback: str) -> str:
|
||||
"""
|
||||
Return a usable tool name, substituting ``fallback`` when the name is missing or blank.
|
||||
|
||||
OpenAI-compatible backends (e.g. Mistral via vLLM) reject tool calls whose function name
|
||||
is empty, so an Anthropic tool_use block or tool definition without a usable name must be
|
||||
given a non-empty placeholder rather than passed through as an empty string.
|
||||
"""
|
||||
if raw_name is not None and str(raw_name).strip():
|
||||
return str(raw_name)
|
||||
return fallback
|
||||
|
||||
|
||||
def create_tool_name_mapping(
|
||||
tools: List[Dict[str, Any]],
|
||||
) -> Dict[str, str]:
|
||||
|
|
@ -529,8 +542,8 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
has_cache_control_in_text = True
|
||||
assistant_content_list.append(text_block)
|
||||
elif content.get("type") == "tool_use":
|
||||
# Truncate tool name for OpenAI's 64-char limit
|
||||
tool_name = truncate_tool_name(content.get("name", ""))
|
||||
original_name = resolve_tool_name(content.get("name"), "litellm_unnamed_tool")
|
||||
tool_name = truncate_tool_name(original_name)
|
||||
function_chunk: ChatCompletionToolCallFunctionChunk = {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(content.get("input", {})),
|
||||
|
|
@ -750,11 +763,7 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
new_tools.append(tool) # type: ignore[arg-type]
|
||||
continue
|
||||
|
||||
raw_name = tool.get("name")
|
||||
if raw_name is None or (isinstance(raw_name, str) and not str(raw_name).strip()):
|
||||
original_name = f"litellm_unnamed_tool_{idx}"
|
||||
else:
|
||||
original_name = str(raw_name)
|
||||
original_name = resolve_tool_name(tool.get("name"), f"litellm_unnamed_tool_{idx}")
|
||||
truncated_name = truncate_tool_name(original_name)
|
||||
|
||||
# Store mapping if name was truncated
|
||||
|
|
|
|||
|
|
@ -1075,6 +1075,68 @@ def test_translate_anthropic_messages_to_openai_tool_use_with_signature():
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool_use_name", ["", " ", None])
|
||||
def test_translate_anthropic_messages_to_openai_tool_use_without_name(tool_use_name):
|
||||
"""Regression test for https://github.com/BerriAI/litellm/issues/30515.
|
||||
|
||||
A tool_use block whose name is missing or blank must not produce an empty OpenAI
|
||||
function name; OpenAI-compatible backends such as Mistral via vLLM reject those.
|
||||
Before the fix an empty name was passed through verbatim and a None name crashed
|
||||
with a TypeError inside truncate_tool_name.
|
||||
"""
|
||||
messages = cast(
|
||||
Any,
|
||||
[
|
||||
{"role": "user", "content": [{"type": "text", "text": "hi"}]},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_1",
|
||||
"name": tool_use_name,
|
||||
"input": {"q": "x"},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
result = adapter.translate_anthropic_messages_to_openai(messages=messages)
|
||||
|
||||
tool_calls = result[1]["tool_calls"]
|
||||
assert len(tool_calls) == 1
|
||||
function_name = tool_calls[0]["function"]["name"]
|
||||
assert function_name and function_name.strip()
|
||||
|
||||
|
||||
def test_translate_anthropic_messages_to_openai_tool_use_preserves_name():
|
||||
"""A valid tool_use name must be passed through unchanged."""
|
||||
messages = cast(
|
||||
Any,
|
||||
[
|
||||
{"role": "user", "content": [{"type": "text", "text": "hi"}]},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_1",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "London"},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
result = adapter.translate_anthropic_messages_to_openai(messages=messages)
|
||||
|
||||
assert result[1]["tool_calls"][0]["function"]["name"] == "get_weather"
|
||||
|
||||
|
||||
def test_translate_anthropic_messages_to_openai_tool_result_with_multiple_content_items():
|
||||
"""
|
||||
Test that tool_result with multiple content items creates a single tool message
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue