From 54dd6668d05c7d587df4fa978f4bdeef29d1bd92 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 8 Nov 2026 00:06:38 +0000 Subject: [PATCH] fix(utils): keep a non-string tool_choice type a 400, not a TypeError A list or dict in tool_choice["type"] is unhashable, so the hosted tool lookup needs the string check first. The bridge test that used file_search as an untranslatable choice now uses a type that really is unknown. --- litellm/utils.py | 5 ++++- .../test_litellm_completion_responses.py | 4 ++-- tests/unit/test_utils.py | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index d9ab3626859..23a21cfc856 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -9758,9 +9758,12 @@ def validate_chat_completion_tool_choice( ) # Standard OpenAI format: {"type": "function", "function": {...}} + forces_hosted_tool: Final[bool] = ( + isinstance(tool_choice_type, str) and tool_choice_type in HOSTED_TOOL_CHOICE_TYPES + ) if ( tool_choice_type != "allowed_tools" - and tool_choice_type not in HOSTED_TOOL_CHOICE_TYPES + and not forces_hosted_tool and (tool_choice_type is None or tool_choice.get("function") is None) ): raise BadRequestError( diff --git a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py index d5860b7519b..31ab4c6ac8c 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -5949,12 +5949,12 @@ async def test_bridge_rejects_untranslatable_tool_choice_with_a_400(stream: bool model="anthropic/claude-haiku-4-5", input="Which fruit is red?", tools=[{"type": "function", "name": "lookup_fruit", "parameters": {"type": "object"}}], - tool_choice={"type": "file_search"}, + tool_choice={"type": "web_search_tool"}, stream=stream, api_key="sk-unused", ) assert exc_info.value.status_code == 400 - assert "tool_choice={'type': 'file_search'}" in str(exc_info.value) + assert "tool_choice={'type': 'web_search_tool'}" in str(exc_info.value) def test_cache_control_on_user_message(): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index cf073ad21e5..63cc2aa60d7 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -8810,6 +8810,7 @@ def test_validate_tool_choice_cursor_format(): {"type": "function"}, {"name": "lookup_fruit"}, {"type": "web_search_tool"}, + {"type": ["web_search"]}, ], ) def test_validate_tool_choice_invalid_dict_is_a_400(tool_choice):