diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index d5f0ac32ff4..91adff50a17 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -1213,6 +1213,20 @@ class AmazonConverseConfig(BaseConfig): } return {**additional_request_params, **merged_entries} + @staticmethod + def _drop_tool_choice_type_conflicting_with_tool_config(additional_request_params: dict) -> None: + """Drop ``tool_choice.type`` from the Anthropic passthrough fields. + + Converse rejects a request carrying both ``toolConfig.toolChoice`` and an + ``additionalModelRequestFields.tool_choice.type``, so once the caller asked for a + tool choice the type has to come from ``toolChoice`` alone. Sibling keys such as + ``disable_parallel_tool_use`` have no ``toolConfig`` equivalent and are accepted + alongside ``toolChoice``, so they stay. + """ + tool_choice = additional_request_params.get("tool_choice") + if isinstance(tool_choice, dict): + tool_choice.pop("type", None) + def _prepare_request_params( self, optional_params: dict, model: str, drop_params: bool = False ) -> tuple[dict, dict, dict, OutputConfigBlock | None]: @@ -1569,6 +1583,7 @@ class AmazonConverseConfig(BaseConfig): ) if tool_choice_values is not None: bedrock_tool_config["toolChoice"] = tool_choice_values + self._drop_tool_choice_type_conflicting_with_tool_config(additional_request_params) data: Final[CommonRequestObject] = { "inferenceConfig": self._transform_inference_params(inference_params=inference_params), 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 cca4f4232f2..6d318bb8729 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -4263,6 +4263,78 @@ def test_parallel_tool_calls_emits_typed_auto_tool_choice(parallel_tool_calls, e } +@pytest.mark.parametrize( + "tool_choice, expected_tool_config_choice", + [ + ("auto", {"auto": {}}), + ("required", {"any": {}}), + ({"type": "function", "function": {"name": "get_current_weather"}}, {"tool": {"name": "get_current_weather"}}), + ], +) +def test_parallel_tool_calls_with_explicit_tool_choice_omits_conflicting_type(tool_choice, expected_tool_config_choice): + config = AmazonConverseConfig() + model = "us.anthropic.claude-opus-4-8" + messages = [{"role": "user", "content": "What's the weather in SF and NYC?"}] + + optional_params = config.map_openai_params( + non_default_params={"parallel_tool_calls": False, "tool_choice": tool_choice, "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 request_data["toolConfig"]["toolChoice"] == expected_tool_config_choice + assert request_data["additionalModelRequestFields"]["tool_choice"] == {"disable_parallel_tool_use": True} + + +def test_tool_choice_type_kept_when_no_tool_config_choice_conflicts(): + config = AmazonConverseConfig() + model = "us.anthropic.claude-opus-4-8" + + optional_params = config.map_openai_params( + non_default_params={"parallel_tool_calls": False, "tools": _TOOL_PARAM}, + optional_params={}, + model=model, + drop_params=False, + ) + + request_data = config.transform_request( + model=model, + messages=[{"role": "user", "content": "What's the weather in SF and NYC?"}], + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert "toolChoice" not in request_data["toolConfig"] + assert request_data["additionalModelRequestFields"]["tool_choice"] == { + "type": "auto", + "disable_parallel_tool_use": True, + } + + +def test_drop_tool_choice_type_leaves_other_passthrough_fields_untouched(): + additional_request_params = { + "tool_choice": {"type": "tool", "name": "get_weather", "disable_parallel_tool_use": True}, + "anthropic_beta": ["some-beta"], + } + + AmazonConverseConfig._drop_tool_choice_type_conflicting_with_tool_config(additional_request_params) + + assert additional_request_params == { + "tool_choice": {"name": "get_weather", "disable_parallel_tool_use": True}, + "anthropic_beta": ["some-beta"], + } + + def test_parallel_tool_use_merge_preserves_user_tool_choice_type(): merged = AmazonConverseConfig._merge_parallel_tool_use_config( {"tool_choice": {"type": "tool", "name": "get_weather", "disable_parallel_tool_use": False}},