diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index efc890d9ee2..25acf952891 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -606,6 +606,24 @@ class AmazonConverseConfig(BaseConfig): status_code=400, ) + @staticmethod + def _map_bedrock_tool_choice_to_anthropic_tool_choice( + tool_choice: Optional[ToolChoiceValuesBlock], + ) -> dict: + if tool_choice is None or "auto" in tool_choice: + return {"type": "auto"} + if "any" in tool_choice: + return {"type": "any"} + + specific_tool_choice = tool_choice.get("tool") + if specific_tool_choice is not None: + name = specific_tool_choice.get("name") + if name: + return {"type": "tool", "name": name} + return {"type": "tool"} + + return {"type": "auto"} + def get_supported_image_types(self) -> List[str]: return ["png", "jpeg", "gif", "webp"] @@ -1241,7 +1259,15 @@ class AmazonConverseConfig(BaseConfig): "_parallel_tool_use_config", None ) if parallel_tool_use_config is not None and is_claude_4_5_on_bedrock(model): + bedrock_tool_choice = inference_params.pop("tool_choice", None) + anthropic_tool_choice = ( + self._map_bedrock_tool_choice_to_anthropic_tool_choice( + tool_choice=bedrock_tool_choice, + ) + ) for key, value in parallel_tool_use_config.items(): + if key == "tool_choice" and isinstance(value, dict): + value = {**anthropic_tool_choice, **value} if ( key in additional_request_params and isinstance(additional_request_params[key], dict) 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 5f2ed3dc00f..ce8e4f09c3f 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -3756,6 +3756,7 @@ def test_parallel_tool_calls_newer_model_adds_disable_flag(): assert "additionalModelRequestFields" in request_data assert "tool_choice" in request_data["additionalModelRequestFields"] + assert request_data["additionalModelRequestFields"]["tool_choice"]["type"] == "auto" assert ( request_data["additionalModelRequestFields"]["tool_choice"][ "disable_parallel_tool_use" @@ -3765,6 +3766,91 @@ def test_parallel_tool_calls_newer_model_adds_disable_flag(): assert "parallel_tool_calls" not in request_data["additionalModelRequestFields"] +def test_parallel_tool_calls_true_includes_type_field(): + """parallel_tool_calls=True should include Anthropic's required tool_choice type.""" + config = AmazonConverseConfig() + model = "anthropic.claude-sonnet-4-5-20250929-v1:0" + messages = [{"role": "user", "content": "What's the weather in SF and NYC?"}] + + optional_params = config.map_openai_params( + non_default_params={"parallel_tool_calls": True, "tools": _TOOL_PARAM}, + optional_params={}, + model=model, + drop_params=False, + ) + + request_data = config.transform_request( + model=model, + messages=messages, + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert "additionalModelRequestFields" in request_data + tool_choice = request_data["additionalModelRequestFields"]["tool_choice"] + assert tool_choice["type"] == "auto" + assert tool_choice["disable_parallel_tool_use"] is False + assert "parallel_tool_calls" not in request_data["additionalModelRequestFields"] + + +@pytest.mark.parametrize( + ("openai_tool_choice", "expected_anthropic_tool_choice"), + [ + ( + "auto", + {"type": "auto", "disable_parallel_tool_use": True}, + ), + ( + "required", + {"type": "any", "disable_parallel_tool_use": True}, + ), + ( + {"type": "function", "function": {"name": "get_weather"}}, + { + "type": "tool", + "name": "get_weather", + "disable_parallel_tool_use": True, + }, + ), + ], + ids=["auto", "required", "named-tool"], +) +def test_parallel_tool_calls_moves_explicit_tool_choice_into_anthropic_extension( + openai_tool_choice, + expected_anthropic_tool_choice, +): + """Explicit tool_choice should not conflict with Anthropic's parallel tool extension.""" + config = AmazonConverseConfig() + model = "anthropic.claude-sonnet-4-5-20250929-v1:0" + messages = [{"role": "user", "content": "What's the weather in SF and NYC?"}] + + optional_params = config.map_openai_params( + non_default_params={ + "tools": _TOOL_PARAM, + "tool_choice": openai_tool_choice, + "parallel_tool_calls": False, + }, + optional_params={}, + model=model, + drop_params=False, + ) + + request_data = config.transform_request( + model=model, + messages=messages, + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert ( + request_data["additionalModelRequestFields"]["tool_choice"] + == expected_anthropic_tool_choice + ) + assert "toolChoice" not in request_data["toolConfig"] + + def test_parallel_tool_calls_older_model_drops_disable_flag(): """Older Claude models (pre-4.5) must NOT receive disable_parallel_tool_use — Bedrock rejects it.""" config = AmazonConverseConfig()