mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-29 22:21:21 +00:00
fix(bedrock): align toolUse/toolSpec names and allow hyphens (#28874)
* fix(bedrock): align toolUse/toolSpec names and allow hyphens in tool names Sanitize tool names consistently in tool history and tool_choice, and preserve hyphens per current Bedrock [a-zA-Z0-9_-]+ constraint. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(bedrock): docstring matches alpha-first tool name normalization Greptile: pattern is [a-zA-Z][a-zA-Z0-9_-]* to reflect prepend-'a' behavior. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3997,7 +3997,7 @@ def _convert_to_bedrock_tool_call_invoke(
|
||||
for tool in tool_calls:
|
||||
if "function" in tool:
|
||||
tool_id = tool["id"]
|
||||
name = tool["function"].get("name", "")
|
||||
name = make_valid_bedrock_tool_name(tool["function"].get("name", ""))
|
||||
arguments = tool["function"].get("arguments", "")
|
||||
|
||||
if not arguments or not arguments.strip():
|
||||
@@ -5323,16 +5323,10 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915
|
||||
|
||||
|
||||
def make_valid_bedrock_tool_name(input_tool_name: str) -> str:
|
||||
"""
|
||||
Replaces any invalid characters in the input tool name with underscores
|
||||
and ensures the resulting string is a valid identifier for Bedrock tools
|
||||
"""
|
||||
"""Normalize tool names to Bedrock pattern [a-zA-Z][a-zA-Z0-9_-]*."""
|
||||
|
||||
def replace_invalid(char):
|
||||
"""
|
||||
Bedrock tool names only supports alpha-numeric characters and underscores
|
||||
"""
|
||||
if char.isalnum() or char == "_":
|
||||
if char.isalnum() or char in ("_", "-"):
|
||||
return char
|
||||
return "_"
|
||||
|
||||
@@ -5492,7 +5486,7 @@ def _bedrock_tools_pt(
|
||||
raw_name = f"litellm_unnamed_tool_{tool_idx}"
|
||||
|
||||
# related issue: https://github.com/BerriAI/litellm/issues/5007
|
||||
# Bedrock tool names must satisfy regular expression pattern: [a-zA-Z][a-zA-Z0-9_]* ensure this is true
|
||||
# Bedrock tool names must satisfy pattern: [a-zA-Z][a-zA-Z0-9_-]*
|
||||
name = make_valid_bedrock_tool_name(input_tool_name=raw_name)
|
||||
if _tool_description: # bedrock doesn't accept empty "" or None descriptions
|
||||
description = _tool_description
|
||||
|
||||
@@ -30,6 +30,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
BedrockConverseMessagesProcessor,
|
||||
_bedrock_converse_messages_pt,
|
||||
_bedrock_tools_pt,
|
||||
make_valid_bedrock_tool_name,
|
||||
)
|
||||
from litellm.llms.anthropic.chat.transformation import (
|
||||
DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING,
|
||||
@@ -595,7 +596,9 @@ class AmazonConverseConfig(BaseConfig):
|
||||
elif isinstance(tool_choice, dict):
|
||||
# only supported for anthropic + mistral models - https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolChoice.html
|
||||
specific_tool = SpecificToolChoiceBlock(
|
||||
name=tool_choice.get("function", {}).get("name", "")
|
||||
name=make_valid_bedrock_tool_name(
|
||||
tool_choice.get("function", {}).get("name", "")
|
||||
)
|
||||
)
|
||||
return ToolChoiceValuesBlock(tool=specific_tool)
|
||||
else:
|
||||
|
||||
@@ -1062,7 +1062,7 @@ def test_bedrock_tools_pt_invalid_names():
|
||||
print("bedrock tools after prompt formatting=", result)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["toolSpec"]["name"] == "a123_invalid_name"
|
||||
assert result[0]["toolSpec"]["name"] == "a123-invalid_name"
|
||||
assert result[1]["toolSpec"]["name"] == "another_invalid_name"
|
||||
|
||||
|
||||
@@ -1171,7 +1171,7 @@ def test_bedrock_tools_transformation_valid_params():
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert "toolSpec" in result[0]
|
||||
assert result[0]["toolSpec"]["name"] == "a123_invalid_name"
|
||||
assert result[0]["toolSpec"]["name"] == "a123-invalid_name"
|
||||
assert result[0]["toolSpec"]["description"] == "Invalid name test"
|
||||
assert "inputSchema" in result[0]["toolSpec"]
|
||||
assert "json" in result[0]["toolSpec"]["inputSchema"]
|
||||
|
||||
+86
@@ -10,10 +10,12 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
BedrockConverseMessagesProcessor,
|
||||
BedrockImageProcessor,
|
||||
_bedrock_converse_messages_pt,
|
||||
_bedrock_tools_pt,
|
||||
_convert_to_bedrock_tool_call_invoke,
|
||||
_convert_to_bedrock_tool_call_result,
|
||||
anthropic_messages_pt,
|
||||
convert_to_gemini_tool_call_result,
|
||||
make_valid_bedrock_tool_name,
|
||||
ollama_pt,
|
||||
sanitize_messages_for_tool_calling,
|
||||
)
|
||||
@@ -2082,6 +2084,90 @@ def test_bedrock_tool_call_invoke_non_dict_arguments():
|
||||
assert result[0]["toolUse"]["input"] == {}
|
||||
|
||||
|
||||
def test_make_valid_bedrock_tool_name_preserves_hyphens():
|
||||
assert make_valid_bedrock_tool_name("my-tool") == "my-tool"
|
||||
assert (
|
||||
make_valid_bedrock_tool_name(
|
||||
"CreateCaseKnowledgeArticle_foTWsqR6yDt-OnSsvR5e6Q"
|
||||
)
|
||||
== "CreateCaseKnowledgeArticle_foTWsqR6yDt-OnSsvR5e6Q"
|
||||
)
|
||||
|
||||
|
||||
def test_bedrock_tool_name_sanitized_consistently_in_tools_and_tool_use():
|
||||
"""toolSpec and toolUse names must match after sanitization (issue #5007)."""
|
||||
raw_name = "foo@bar"
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": raw_name,
|
||||
"description": "test",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
]
|
||||
tool_spec_name = _bedrock_tools_pt(tools)[0]["toolSpec"]["name"]
|
||||
|
||||
tool_calls = [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": raw_name, "arguments": "{}"},
|
||||
}
|
||||
]
|
||||
tool_use_name = _convert_to_bedrock_tool_call_invoke(tool_calls)[0]["toolUse"][
|
||||
"name"
|
||||
]
|
||||
|
||||
assert tool_spec_name == "foo_bar"
|
||||
assert tool_use_name == tool_spec_name
|
||||
|
||||
|
||||
def test_bedrock_converse_messages_pt_tool_use_matches_tool_spec_hyphen_name():
|
||||
"""Hyphenated tool names are preserved and consistent in multi-turn history."""
|
||||
tool_name = "my-tool"
|
||||
messages = [
|
||||
{"role": "user", "content": "call the tool"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_hyphen",
|
||||
"type": "function",
|
||||
"function": {"name": tool_name, "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
translated = _bedrock_converse_messages_pt(
|
||||
messages=messages, model="", llm_provider=""
|
||||
)
|
||||
tool_use_blocks = [
|
||||
block
|
||||
for msg in translated
|
||||
for block in msg.get("content", [])
|
||||
if "toolUse" in block
|
||||
]
|
||||
assert len(tool_use_blocks) == 1
|
||||
assert tool_use_blocks[0]["toolUse"]["name"] == tool_name
|
||||
|
||||
tool_spec_name = _bedrock_tools_pt(
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"description": "test",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
]
|
||||
)[0]["toolSpec"]["name"]
|
||||
assert tool_spec_name == tool_name
|
||||
|
||||
|
||||
def test_bedrock_tool_call_invoke_multiple_normal_tools():
|
||||
"""Multiple separate tool calls (normal parallel calling) work correctly."""
|
||||
tool_calls = [
|
||||
|
||||
Reference in New Issue
Block a user