diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 2bb379655b4..0e71788e021 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -5,6 +5,7 @@ Translating between OpenAI's `/chat/completion` format and Amazon's `/converse` import copy import json import re +import ast import time import types from typing import Any, List, Literal, Optional, Tuple, Union, cast, overload @@ -1863,6 +1864,78 @@ class AmazonConverseConfig(BaseConfig): ) ) + tool_call_match = re.search( + r"\s*(.*?)\s*", + content, + flags=re.DOTALL | re.IGNORECASE, + ) + if tool_call_match is not None: + parsed_tool_call = self._parse_json_string_if_possible( + tool_call_match.group(1) + ) + if not isinstance(parsed_tool_call, dict): + return None + + resolved_tool_name = self._resolve_nexus_tool_name( + cast(Optional[str], parsed_tool_call.get("name")), tool_call_names + ) + if resolved_tool_name is None: + return None + + parsed_tool_input: Any = parsed_tool_call.get( + "arguments", parsed_tool_call.get("input", {}) + ) + if isinstance(parsed_tool_input, str): + parsed_tool_input = self._parse_json_string_if_possible( + parsed_tool_input + ) + if not isinstance(parsed_tool_input, dict): + return None + + return ChatCompletionMessageToolCall( + function=Function( + name=resolved_tool_name, + arguments=json.dumps(parsed_tool_input), + ) + ) + + direct_function_call_match = re.match( + r"^\s*([A-Za-z0-9_.-]+)\s*\((.*)\)\s*$", + content, + flags=re.DOTALL, + ) + if direct_function_call_match is not None: + resolved_tool_name = self._resolve_nexus_tool_name( + direct_function_call_match.group(1), tool_call_names + ) + if resolved_tool_name is None: + return None + + raw_tool_input = direct_function_call_match.group(2).strip() + parsed_tool_input: Any = {} + if raw_tool_input: + if raw_tool_input.startswith("{") and raw_tool_input.endswith("}"): + parsed_tool_input = self._parse_json_string_if_possible( + raw_tool_input + ) + else: + parsed_tool_input = {} + for key, value in re.findall( + r"([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\"(?:[^\"\\]|\\.)*\"|'(?:[^'\\]|\\.)*')", + raw_tool_input, + ): + parsed_tool_input[key] = ast.literal_eval(value) + + if not isinstance(parsed_tool_input, dict): + return None + + return ChatCompletionMessageToolCall( + function=Function( + name=resolved_tool_name, + arguments=json.dumps(parsed_tool_input), + ) + ) + tool_use_match = re.search( r"\s*(.*?)\s*", content, diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index f467cd9f80f..a38cf4892bb 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -329,6 +329,88 @@ def test_apply_tool_call_transformation_if_needed_with_nexus_tool_use_xml_format ) +def test_apply_tool_call_transformation_if_needed_with_nexus_tool_call_json_format(): + from litellm.types.utils import Message + + config = AmazonConverseConfig() + tools = [ + { + "type": "function", + "function": { + "name": "read_file", + "parameters": { + "type": "object", + "properties": { + "path": {"type": "string"}, + }, + "required": ["path"], + }, + }, + } + ] + message = Message( + role="assistant", + content='{"name":"read_file","arguments":{"path":"README.md"}}', + ) + + transformed_message, transformed_finish_reason = ( + config.apply_tool_call_transformation_if_needed( + message=message, + tools=tools, + initial_finish_reason="stop", + ) + ) + + assert transformed_message.content is None + assert transformed_finish_reason == "tool_calls" + assert transformed_message.tool_calls is not None + assert transformed_message.tool_calls[0].function.name == "read_file" + assert transformed_message.tool_calls[0].function.arguments == json.dumps( + {"path": "README.md"} + ) + + +def test_apply_tool_call_transformation_if_needed_with_direct_function_call_format(): + from litellm.types.utils import Message + + config = AmazonConverseConfig() + tools = [ + { + "type": "function", + "function": { + "name": "read_file", + "parameters": { + "type": "object", + "properties": { + "path": {"type": "string"}, + }, + "required": ["path"], + }, + }, + } + ] + message = Message( + role="assistant", + content='read_file(path="README.md")', + ) + + transformed_message, transformed_finish_reason = ( + config.apply_tool_call_transformation_if_needed( + message=message, + tools=tools, + initial_finish_reason="stop", + ) + ) + + assert transformed_message.content is None + assert transformed_finish_reason == "tool_calls" + assert transformed_message.tool_calls is not None + assert transformed_message.tool_calls[0].function.name == "read_file" + assert transformed_message.tool_calls[0].function.arguments == json.dumps( + {"path": "README.md"} + ) + + def test_apply_tool_call_transformation_if_needed_ignores_unknown_nexus_tool_name(): from litellm.types.utils import Message