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:
Sameer Kankute
2026-05-26 11:58:21 -07:00
committed by GitHub
co-authored by Cursor
parent d52fbfb458
commit f17dfcd869
4 changed files with 96 additions and 13 deletions
@@ -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"]
@@ -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 = [