diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index 84f6445846b..c4ddb4b7ee0 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -58,7 +58,7 @@ def get_supported_openai_params( supported_params = list(dict.fromkeys([*supported_params, *base_model_params])) return supported_params - if custom_llm_provider == "bedrock": + if custom_llm_provider == "bedrock" or custom_llm_provider == "bedrock_converse": return litellm.AmazonConverseConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "meta_llama": provider_config = litellm.ProviderConfigManager.get_provider_chat_config( diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index e54218cb8db..55900133de8 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -5027,6 +5027,12 @@ def _bedrock_tools_pt(tools: List, model: Optional[str] = None) -> List[BedrockT tool_block_list.append(tool) # type: ignore continue + # Responses built-in tools (web_search, image_generation, namespace, tool_search, + # custom) carry neither an OpenAI "function" nor an Anthropic "input_schema" and have + # no Bedrock toolSpec equivalent; drop them instead of emitting an empty junk toolSpec. + if isinstance(tool, dict) and "function" not in tool and "input_schema" not in tool: + continue + # OpenAI function tools, or Anthropic Messages / Claude Code ({name, input_schema, type, ...}) if isinstance(tool, dict) and "input_schema" in tool and "function" not in tool: parameters = copy.deepcopy(tool.get("input_schema") or {"type": "object", "properties": {}}) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index d2a7edd21ef..866698b1f96 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -12,6 +12,9 @@ from openai.types.responses.tool_param import FunctionToolParam from typing_extensions import TypedDict from litellm.caching import InMemoryCache +from litellm.litellm_core_utils.get_supported_openai_params import ( + get_supported_openai_params, +) from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.responses.litellm_completion_transformation.session_handler import ( ResponsesSessionHandler, @@ -155,6 +158,22 @@ class LiteLLMCompletionResponsesConfig: # Return as-is for unknown formats return tool_choice + @staticmethod + def _should_drop_derived_web_search_options(model: str, custom_llm_provider: Optional[str]) -> bool: + """ + A Responses ``web_search`` built-in tool is derived into a ``web_search_options`` param. + When the resolved provider/model does not support it (e.g. Bedrock Anthropic, where only + Nova maps it to a nova_grounding systemTool), the derived param is dropped here instead of + raising UnsupportedParamsError downstream. Providers that support it keep it untouched. + + Support is read from each provider's own ``get_supported_openai_params`` so this bridge + stays provider-agnostic; an unmapped provider (``None``) is treated as "keep". + """ + supported_params: Optional[List[str]] = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) + return supported_params is not None and "web_search_options" not in supported_params + @staticmethod def transform_responses_api_request_to_chat_completion_request( model: str, @@ -175,6 +194,11 @@ class LiteLLMCompletionResponsesConfig: responses_api_request.get("tools") or [] # type: ignore ) + if web_search_options is not None and LiteLLMCompletionResponsesConfig._should_drop_derived_web_search_options( + model=model, custom_llm_provider=custom_llm_provider + ): + web_search_options = None + response_format = None text_param = responses_api_request.get("text") if text_param: 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 ed2dfc9440e..c022303e9d6 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 @@ -1553,6 +1553,69 @@ def test_bedrock_tools_pt_does_not_handle_system_tool(): assert tool_spec["name"] == "get_weather" +def test_bedrock_tools_pt_drops_unmappable_responses_builtin_tools(): + """ + Regression for LIT-3858: Responses built-in tools (image_generation, namespace, + tool_search, custom) have no Bedrock toolSpec equivalent. They must be dropped, not + emitted as junk ``litellm_unnamed_tool_N`` toolSpecs the model can hallucinate calls to. + Mappable ``function`` and Anthropic ``input_schema`` tools must survive untouched. + """ + from litellm.litellm_core_utils.prompt_templates.factory import _bedrock_tools_pt + + tools = [ + { + "type": "function", + "function": { + "name": "noop", + "description": "x", + "parameters": {"type": "object", "properties": {}}, + }, + }, + {"type": "image_generation", "output_format": "png"}, + {"type": "namespace", "name": "grp", "description": "g", "tools": []}, + {"type": "custom", "name": "free_form"}, + ] + + result = _bedrock_tools_pt( + tools=tools, model="anthropic.claude-sonnet-4-5-20250929-v1:0" + ) + + names = [block["toolSpec"]["name"] for block in result if "toolSpec" in block] + assert names == ["noop"] + assert not any(name.startswith("litellm_unnamed_tool_") for name in names) + + +def test_bedrock_tools_pt_keeps_anthropic_input_schema_tools(): + """ + The drop guard for unmappable tools must not regress Anthropic Messages format tools, + which carry an ``input_schema`` instead of an OpenAI ``function`` key. + """ + from litellm.litellm_core_utils.prompt_templates.factory import _bedrock_tools_pt + + tools = [ + { + "type": "image_generation", + "output_format": "png", + }, + { + "name": "lookup", + "description": "look something up", + "input_schema": { + "type": "object", + "properties": {"q": {"type": "string"}}, + "required": ["q"], + }, + }, + ] + + result = _bedrock_tools_pt( + tools=tools, model="anthropic.claude-sonnet-4-5-20250929-v1:0" + ) + + names = [block["toolSpec"]["name"] for block in result if "toolSpec" in block] + assert names == ["lookup"] + + def test_convert_to_anthropic_tool_result_image_with_cache_control(): """ Test that cache_control is properly applied to image content in tool results. diff --git a/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py b/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py index 84900e3f2ed..8587ad1ab01 100644 --- a/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py +++ b/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py @@ -146,3 +146,33 @@ def test_sambanova_embeddings_request_returns_list_not_none(): ) assert embedding_params == [] + + +def test_bedrock_converse_alias_resolves_like_bedrock(): + """The ``bedrock_converse`` invocation alias must resolve through AmazonConverseConfig + just like ``bedrock`` (the codebase already pairs them, e.g. ``_strip_model_name``). + Before this mapping it returned ``None`` (unmapped), so callers gating on supported + params saw no Bedrock capabilities for a Converse model invoked via the alias.""" + anthropic_model = "bedrock/converse/us.anthropic.claude-sonnet-4-6" + + via_alias = get_supported_openai_params( + model=anthropic_model, custom_llm_provider="bedrock_converse" + ) + + assert via_alias is not None + assert via_alias == get_supported_openai_params( + model=anthropic_model, custom_llm_provider="bedrock" + ) + assert "web_search_options" not in via_alias + assert "tools" in via_alias + + +def test_bedrock_converse_alias_keeps_nova_web_search_options(): + """Nova on the ``bedrock_converse`` alias still advertises web_search_options, proving the + alias routes through the model-aware config rather than a blanket Bedrock default.""" + nova_params = get_supported_openai_params( + model="amazon.nova-pro-v1:0", custom_llm_provider="bedrock_converse" + ) + + assert nova_params is not None + assert "web_search_options" in nova_params diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index a0b1676068b..426c73645c1 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -1407,6 +1407,138 @@ class TestToolTransformation: == "string" ) + def test_bedrock_anthropic_drops_derived_web_search_options(self): + """ + Regression for LIT-3858: a Responses web_search tool becomes a derived + web_search_options param. Bedrock Anthropic models do not support it, so on the + chat-completion bridge it must be dropped (so litellm doesn't raise + UnsupportedParamsError) without the caller setting drop_params. + """ + responses_api_request = { + "tools": [{"type": "web_search", "external_web_access": False}], + } + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="anthropic.claude-sonnet-4-5-20250929-v1:0", + input="hi", + responses_api_request=responses_api_request, + custom_llm_provider="bedrock", + ) + + assert "web_search_options" not in result + + def test_bedrock_converse_provider_drops_derived_web_search_options(self): + """ + Regression for the ``bedrock_converse`` alias: routing a Bedrock Converse model resolves + to custom_llm_provider='bedrock_converse' with model='bedrock/converse/...'. The derived + web_search_options must still be dropped on this route, not just the bare 'bedrock' one. + """ + responses_api_request = { + "tools": [{"type": "web_search", "external_web_access": False}], + } + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="bedrock/converse/us.anthropic.claude-sonnet-4-6", + input="hi", + responses_api_request=responses_api_request, + custom_llm_provider="bedrock_converse", + ) + + assert "web_search_options" not in result + + def test_bedrock_nova_keeps_derived_web_search_options(self): + """Nova models map web_search_options to a nova_grounding systemTool, so keep it.""" + responses_api_request = { + "tools": [{"type": "web_search", "search_context_size": "high"}], + } + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="amazon.nova-pro-v1:0", + input="hi", + responses_api_request=responses_api_request, + custom_llm_provider="bedrock", + ) + + assert result.get("web_search_options") is not None + + def test_supported_provider_keeps_derived_web_search_options(self): + """A provider whose config lists web_search_options (e.g. OpenAI) keeps it untouched.""" + responses_api_request = { + "tools": [{"type": "web_search", "search_context_size": "high"}], + } + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="gpt-4o", + input="hi", + responses_api_request=responses_api_request, + custom_llm_provider="openai", + ) + + assert result.get("web_search_options") is not None + + def test_unsupported_non_bedrock_provider_drops_derived_web_search_options(self): + """ + The drop is provider-agnostic, not hardcoded to Bedrock: any provider whose config + does not list web_search_options (e.g. Cohere) drops the derived param. This fails if + the drop is ever re-scoped to a single provider. + """ + responses_api_request = { + "tools": [{"type": "web_search", "search_context_size": "high"}], + } + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="command-r", + input="hi", + responses_api_request=responses_api_request, + custom_llm_provider="cohere", + ) + + assert "web_search_options" not in result + + def test_bedrock_anthropic_responses_tools_yield_only_function_toolspec(self): + """ + End-to-end (no network) of the LIT-3858 acceptance criterion: the mixed tools array + is transformed for a Bedrock Anthropic model, then fed through the Bedrock tool layer. + The derived web_search_options is dropped, and toolConfig contains only the function + tool, never the web_search/image_generation/namespace built-ins as junk toolSpecs. + """ + from litellm.litellm_core_utils.prompt_templates.factory import ( + _bedrock_tools_pt, + ) + + model = "anthropic.claude-sonnet-4-5-20250929-v1:0" + responses_api_request = { + "tools": [ + { + "type": "function", + "name": "noop", + "description": "x", + "parameters": {"type": "object", "properties": {}}, + }, + {"type": "web_search", "external_web_access": False}, + {"type": "image_generation", "output_format": "png"}, + {"type": "namespace", "name": "grp", "description": "g", "tools": []}, + ], + } + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model=model, + input="hi", + responses_api_request=responses_api_request, + custom_llm_provider="bedrock", + ) + + assert "web_search_options" not in result + + bedrock_tool_blocks = _bedrock_tools_pt(tools=result["tools"], model=model) + names = [ + block["toolSpec"]["name"] + for block in bedrock_tool_blocks + if "toolSpec" in block + ] + assert names == ["noop"] + assert not any(name.startswith("litellm_unnamed_tool_") for name in names) + class TestUsageTransformation: """Test cases for usage transformation from Chat Completion to Responses API format"""