mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(bedrock): drop unmappable Responses tools instead of failing the request (LIT-3858) (#31663)
* fix(bedrock): drop unmappable Responses tools instead of failing the request (LIT-3858)
When an OpenAI Responses request is routed to a Bedrock Converse Anthropic model,
litellm translates the tools array into Bedrock toolConfig. Responses built-in tool
types beyond function (web_search, image_generation, namespace, tool_search, custom)
have no Bedrock equivalent, and previously caused two failures.
A web_search tool is derived into a web_search_options param. Bedrock Anthropic
models do not list web_search_options in get_supported_openai_params, so the request
raised UnsupportedParamsError (HTTP 400) even though it never needed web search. The
derived param is now dropped on the Bedrock chat-completion bridge for models that
do not support it, scoped to Bedrock so other providers are untouched and without
requiring drop_params. Nova still keeps it since it maps to a nova_grounding systemTool.
The remaining non-function tools reached _bedrock_tools_pt and were emitted as junk
litellm_unnamed_tool_N toolSpecs with empty schemas, polluting toolConfig with tools
the model could hallucinate calls to. They are now dropped because they carry neither
an OpenAI function nor an Anthropic input_schema, while mappable function and
input_schema tools survive untouched.
* refactor(responses): drop derived web_search_options via provider config
Greptile flagged that the LIT-3858 fix put Bedrock-specific logic in the
generic Responses->Chat Completion bridge: it imported AmazonConverseConfig
and branched on custom_llm_provider.startswith("bedrock").
Read web_search_options support from each provider's own
get_supported_openai_params instead, so the bridge stays provider-agnostic
and Bedrock capability knowledge lives in the Bedrock config that already
owns it. Behavior is unchanged for the cases the PR targeted (Bedrock
Anthropic drops, Bedrock Nova and OpenAI keep) and now generalizes correctly
to any provider whose config does not support the derived param.
Add a Cohere regression test proving the drop is provider-agnostic; it fails
under the old bedrock-only check and passes now.
* fix(responses): drop derived web_search_options for bedrock_converse alias
Greptile/T-Rex caught that the provider-agnostic drop regressed the
bedrock_converse route: get_supported_openai_params did not map the
bedrock_converse alias (only "bedrock"), so it returned None (unmapped) and
the derived web_search_options was forwarded for
model="bedrock/converse/us.anthropic.claude-sonnet-4-6",
custom_llm_provider="bedrock_converse" instead of being dropped. The previous
startswith("bedrock") check happened to match the alias.
Map bedrock_converse through AmazonConverseConfig in get_supported_openai_params,
mirroring the existing ["bedrock", "bedrock_converse"] pairing in
_strip_model_name. Add regression tests at both levels: the alias now drops the
derived param for Anthropic Converse models, still keeps it for Nova, and the
helper resolves identically to "bedrock".
This commit is contained in:
parent
ea7be19225
commit
8a55e9e560
6 changed files with 256 additions and 1 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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": {}})
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue