From f17dfcd8699ac9be80530993e73d7d0d76bb5ec8 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 27 May 2026 00:28:21 +0530 Subject: [PATCH] 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 * 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 --------- Co-authored-by: Cursor --- .../prompt_templates/factory.py | 14 +-- .../bedrock/chat/converse_transformation.py | 5 +- .../test_bedrock_completion.py | 4 +- ...llm_core_utils_prompt_templates_factory.py | 86 +++++++++++++++++++ 4 files changed, 96 insertions(+), 13 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index f169f86079..03341ce0c9 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -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 diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index efc890d9ee..ae100eda8d 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -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: diff --git a/tests/llm_translation/test_bedrock_completion.py b/tests/llm_translation/test_bedrock_completion.py index 69c87d1d23..a3b4a010f6 100644 --- a/tests/llm_translation/test_bedrock_completion.py +++ b/tests/llm_translation/test_bedrock_completion.py @@ -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"] diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index 6c8f7585ab..129ea237ef 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -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 = [