fix(bedrock): honor unsupported reasoning effort levels for OpenAI GPT models (#44183)

Bedrock GPT-5.6 rejects reasoning effort minimal with 400 unsupported_value. The model map already
sets supports_minimal_reasoning_effort=false for these models, but neither the Converse path nor the
native Responses path read it, so the value was forwarded. Drop it under drop_params and raise
UnsupportedParamsError otherwise, matching how the OpenAI GPT-5 path treats explicitly disabled
effort levels

Co-authored-by: Aasif-Multani <20943280+Aasif-Multani@users.noreply.github.com>
This commit is contained in:
Aasif Multani 2026-10-03 09:33:58 +05:30 • committed by GitHub
parent 6b6222578e
commit 54260bec88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 117 additions and 1 deletions

View file

@ -104,6 +104,7 @@ from ..common_utils import (
BedrockModelInfo,
bedrock_converse_supports_parallel_tool_use_config,
bedrock_model_accepts_cache_points,
bedrock_reasoning_effort_disabled,
get_anthropic_beta_from_headers,
get_bedrock_tool_name,
is_bedrock_application_inference_profile_arn,
@ -1135,6 +1136,25 @@ class AmazonConverseConfig(BaseConfig):
"Dropping unsupported `reasoning_effort` param for Bedrock model=%s; it always reasons and rejects it.",
model,
)
elif (
param == "reasoning_effort"
and isinstance(value, str)
and self._is_openai_gpt_reasoning_model(model)
and bedrock_reasoning_effort_disabled(model=model, effort=value)
):
if not (litellm.drop_params or drop_params):
raise litellm.utils.UnsupportedParamsError(
message=(
f"{model} does not support reasoning_effort={value}. "
"To drop unsupported params, set `litellm.drop_params = True`."
),
status_code=400,
)
verbose_logger.debug(
"Dropping unsupported `reasoning_effort=%s` for Bedrock model=%s.",
value,
model,
)
elif param == "reasoning_effort" and isinstance(value, str):
self._handle_reasoning_effort_parameter(
model=model, reasoning_effort=value, optional_params=optional_params

View file

@ -1017,6 +1017,14 @@ def _mantle_api_base_from_env() -> str | None:
return next((base[: -len(suffix)] for suffix in _MANTLE_OPENAI_BASE_SUFFIXES if base.endswith(suffix)), base)
def bedrock_reasoning_effort_disabled(model: str, effort: str) -> bool:
from litellm.utils import is_explicitly_disabled_factory
return is_explicitly_disabled_factory(
model=model, custom_llm_provider="bedrock_converse", key=f"supports_{effort}_reasoning_effort"
)
def bedrock_supports_openai_responses(model: str | None, model_cost: Mapping[str, object]) -> bool:
"""Whether a Bedrock model is served by bedrock-runtime's OpenAI Responses surface.

View file

@ -52,6 +52,7 @@ from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
from litellm.llms.bedrock.common_utils import (
BEDROCK_CHAT_COMPLETIONS_ROUTE_PREFIX,
BedrockError,
bedrock_reasoning_effort_disabled,
bedrock_supports_openai_responses,
)
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
@ -154,6 +155,29 @@ def inline_remote_image_urls(
return items # pyright: ignore[reportReturnType] # items keep the caller's input union
def _without_disabled_reasoning_effort(
params: Mapping[str, object], model: str, drop_params: bool
) -> dict[str, object]: # mutable-ok: becomes the map_openai_params return value
reasoning: Final = params.get("reasoning")
effort: Final = reasoning.get("effort") if isinstance(reasoning, Mapping) else None
if not isinstance(reasoning, Mapping) or not isinstance(effort, str):
return dict(params)
if not bedrock_reasoning_effort_disabled(model=model, effort=effort):
return dict(params)
if not (drop_params or litellm.drop_params):
raise litellm.UnsupportedParamsError(
message=(
f"{model} does not support reasoning.effort={effort}. "
"To drop unsupported params, set `litellm.drop_params = True`."
),
status_code=400,
)
verbose_logger.debug("Dropping unsupported `reasoning.effort=%s` for Bedrock model=%s.", effort, model)
rest: Final = {key: value for key, value in reasoning.items() if key != "effort"}
without_reasoning: Final = {key: value for key, value in params.items() if key != "reasoning"}
return {**without_reasoning, "reasoning": rest} if rest else without_reasoning
class BedrockOpenAIResponsesConfig(BaseAWSLLM, OpenAIResponsesAPIConfig):
"""Responses API config for the OpenAI models on the bedrock-runtime endpoint."""
@ -270,7 +294,8 @@ class BedrockOpenAIResponsesConfig(BaseAWSLLM, OpenAIResponsesAPIConfig):
"Bedrock Runtime Responses API: dropping unsupported parameter(s) %s that the endpoint rejects.",
unsupported,
)
params: Final = {key: value for key, value in mapped.items() if key not in unsupported}
supported: Final[dict[str, object]] = {key: value for key, value in mapped.items() if key not in unsupported}
params: Final = _without_disabled_reasoning_effort(supported, model, drop_params)
tools: Final = params.get("tools")
if not isinstance(tools, list):
return params

View file

@ -520,6 +520,39 @@ def test_reasoning_effort_maps_to_reasoning_effort_for_openai_gpt5_converse(mode
assert "thinking" not in additional_request_params
@pytest.mark.parametrize(
"model",
[
"us.openai.gpt-5.6-luna",
"bedrock/converse/global.openai.gpt-5.6-terra",
"us.openai.gpt-6-astra",
],
)
def test_openai_gpt5_converse_rejects_effort_level_disabled_in_model_map(model, local_model_cost_map):
config = AmazonConverseConfig()
assert litellm.utils.is_explicitly_disabled_factory(
model=model, custom_llm_provider="bedrock_converse", key="supports_minimal_reasoning_effort"
)
with pytest.raises(litellm.utils.UnsupportedParamsError, match="minimal"):
config.map_openai_params(
non_default_params={"reasoning_effort": "minimal"},
optional_params={},
model=model,
drop_params=False,
)
optional_params = config.map_openai_params(
non_default_params={"reasoning_effort": "minimal"},
optional_params={},
model=model,
drop_params=True,
)
_, additional_request_params, _, _ = config._prepare_request_params(optional_params, model)
assert "reasoning" not in additional_request_params
assert "thinking" not in additional_request_params
@pytest.mark.parametrize(
"model",
[

View file

@ -328,6 +328,36 @@ class TestBackgroundDrop:
assert not [r for r in caplog.records if "dropping unsupported parameter" in r.getMessage()]
class TestDisabledReasoningEffort:
@pytest.mark.parametrize("model", ["us.openai.gpt-5.6-luna", MODEL])
def test_effort_level_disabled_in_model_map_is_rejected(self, model, local_model_cost_map):
with pytest.raises(litellm.UnsupportedParamsError, match="minimal"):
_cfg().map_openai_params(
response_api_optional_params={"reasoning": {"effort": "minimal"}}, model=model, drop_params=False
)
@pytest.mark.parametrize("model", ["us.openai.gpt-5.6-luna", MODEL])
def test_effort_level_disabled_in_model_map_is_dropped_with_drop_params(self, model, local_model_cost_map):
params = _cfg().map_openai_params(
response_api_optional_params={"reasoning": {"effort": "minimal", "summary": "auto"}, "max_output_tokens": 64},
model=model,
drop_params=True,
)
assert params == {"reasoning": {"summary": "auto"}, "max_output_tokens": 64}
def test_effort_only_reasoning_is_removed_when_dropped(self, local_model_cost_map):
params = _cfg().map_openai_params(
response_api_optional_params={"reasoning": {"effort": "minimal"}}, model=MODEL, drop_params=True
)
assert params == {}
def test_supported_effort_level_is_forwarded(self, local_model_cost_map):
params = _cfg().map_openai_params(
response_api_optional_params={"reasoning": {"effort": "low"}}, model=MODEL, drop_params=False
)
assert params == {"reasoning": {"effort": "low"}}
def _never_fetch(url: str) -> str:
raise AssertionError(f"unexpected sync fetch of {url}")