fix: skip the drop warning when a non-reasoning model is asked for no reasoning, and honor declared levels on Bedrock Responses

This commit is contained in:
mateo-berri 2026-10-28 11:02:20 -07:00
parent 00d74f7196
commit d32087cd8e
7 changed files with 123 additions and 35 deletions

View file

@ -34,7 +34,7 @@ inference profile, so the model is named ``us.openai.gpt-5.6-sol`` or
"""
import asyncio
from collections.abc import Awaitable, Callable, Mapping
from collections.abc import Awaitable, Callable, Collection, Mapping
from types import MappingProxyType
from typing import Final
@ -157,13 +157,13 @@ def inline_remote_image_urls(
def _without_disabled_reasoning_effort(
params: Mapping[str, object], model: str, drop_params: bool
params: Mapping[str, object], model: str, drop_params: bool, declared_reasoning_efforts: Collection[str]
) -> 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):
if effort in declared_reasoning_efforts or not bedrock_reasoning_effort_disabled(model=model, effort=effort):
return dict(params)
if not (drop_params or litellm.drop_params):
raise litellm.UnsupportedParamsError(
@ -281,14 +281,16 @@ class BedrockOpenAIResponsesConfig(BaseAWSLLM, OpenAIResponsesAPIConfig):
fake_stream=fake_stream,
)
def map_openai_params(
def map_openai_params_for_deployment(
self,
response_api_optional_params: ResponsesAPIOptionalRequestParams,
model: str,
drop_params: bool,
) -> dict: # mutable-ok: signature fixed by the override contract
mapped: Final = super().map_openai_params(
response_api_optional_params=response_api_optional_params, model=model, drop_params=drop_params
*,
declared_reasoning_efforts: Collection[str],
) -> Mapping[str, object]:
mapped: Final = super().map_openai_params_for_deployment(
response_api_optional_params, model, drop_params, declared_reasoning_efforts=declared_reasoning_efforts
)
unsupported: Final = tuple(sorted(BEDROCK_RUNTIME_UNSUPPORTED_RESPONSE_PARAMS & mapped.keys()))
if unsupported:
@ -297,7 +299,7 @@ class BedrockOpenAIResponsesConfig(BaseAWSLLM, OpenAIResponsesAPIConfig):
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)
params: Final = _without_disabled_reasoning_effort(supported, model, drop_params, declared_reasoning_efforts)
tools: Final = params.get("tools")
if not isinstance(tools, list):
return params

View file

@ -15,7 +15,7 @@ role / access key / profile / web identity), signed via the shared
BaseAWSLLM._sign_request after the request body is finalized.
"""
from collections.abc import Mapping, Sequence
from collections.abc import Collection, Mapping, Sequence
from typing import Final, cast # noqa: TID251 # map_openai_params returns the filtered params as a bare dict
import httpx
@ -229,18 +229,23 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI
def _model_map_lookup_name(model: str) -> str:
return model.split("/")[-1].removeprefix("openai.")
def map_openai_params(
def map_openai_params_for_deployment(
self,
response_api_optional_params: ResponsesAPIOptionalRequestParams,
model: str,
drop_params: bool,
) -> dict:
*,
declared_reasoning_efforts: Collection[str],
) -> Mapping[str, object]:
params: Final = self._handle_unsupported_reasoning_summary(
self._handle_unsupported_service_tier(
super().map_openai_params(
response_api_optional_params=response_api_optional_params,
model=model,
drop_params=drop_params,
dict(
super().map_openai_params_for_deployment(
response_api_optional_params,
model,
drop_params,
declared_reasoning_efforts=declared_reasoning_efforts,
)
),
drop_params=drop_params,
),

View file

@ -4974,12 +4974,24 @@ def _declared_reasoning_efforts(model_info: Mapping[str, object] | None) -> tupl
return deployment_reasoning_effort_levels(model_info) or ()
_DROPPED_REASONING_PARAM_FIXES: Final = MappingProxyType(
{
"reasoning_effort": "If the deployment is a reasoning model, set model_info.base_model to its underlying model.",
"thinking": "If the deployment is a reasoning model, send reasoning_effort instead.",
}
)
_DROPPED_REASONING_PARAMS: Final = ("reasoning_effort", "thinking")
def _turns_reasoning_off(value: object) -> bool:
match value:
case "none" | {"effort": "none"} | {"type": "disabled"}:
return True
case _:
return False
def _is_mapped_non_reasoning_model(model: str, custom_llm_provider: str | None) -> bool:
info: Final = lookup_model_info_for_registration(model, custom_llm_provider)
return not isinstance(info, ModelInfoLookupMiss) and info.get("supports_reasoning") is not True
def _warns_on_dropped_reasoning(value: object, model: str, custom_llm_provider: str | None) -> bool:
return not (_turns_reasoning_off(value) and _is_mapped_non_reasoning_model(model, custom_llm_provider))
def get_optional_params(
@ -5069,26 +5081,31 @@ def get_optional_params(
verbose_logger.info("\nLiteLLM completion() model= %s; provider = %s", model, custom_llm_provider)
verbose_logger.debug("\nLiteLLM: Params passed to completion() %s", passed_params)
verbose_logger.debug("\nLiteLLM: Non-Default params passed to completion() %s", non_default_params)
unsupported_params: Final[Mapping[str, object]] = {
k: non_default_params[k]
for k in non_default_params
if k not in supported_params and k not in PROVIDER_UNVALIDATED_PARAMS and not (k == "n" and n == 1)
}
unsupported_params: Final[MappingProxyType[str, object]] = MappingProxyType(
{
k: non_default_params[k]
for k in non_default_params
if k not in supported_params and k not in PROVIDER_UNVALIDATED_PARAMS and not (k == "n" and n == 1)
}
)
if unsupported_params:
if litellm.drop_params is True or (drop_params is not None and drop_params is True):
for k in unsupported_params:
non_default_params.pop(k, None)
for reasoning_param, fix in _DROPPED_REASONING_PARAM_FIXES.items():
if reasoning_param in unsupported_params:
for reasoning_param in _DROPPED_REASONING_PARAMS:
if reasoning_param in unsupported_params and _warns_on_dropped_reasoning(
unsupported_params[reasoning_param], base_model or model, custom_llm_provider
):
verbose_logger.warning(
"Dropping %s=%r for model=%r (drop_params=True) because %s does not support %s for it. %s",
"Dropping %s=%r for model=%r (drop_params=True) because %s does not support %s for it. "
"If the deployment is a reasoning model, set model_info.base_model to its underlying "
"model.",
reasoning_param,
unsupported_params[reasoning_param],
model,
custom_llm_provider,
reasoning_param,
fix,
)
else:
raise UnsupportedParamsError(

View file

@ -984,7 +984,7 @@ def test_openai_gpt5_converse_rejects_effort_level_disabled_in_model_map(
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
optional_params = config.map_openai_params(
optional_params: Final = config.map_openai_params(
non_default_params={"reasoning_effort": "minimal"},
optional_params={},
model=model,
@ -993,7 +993,7 @@ def test_openai_gpt5_converse_rejects_effort_level_disabled_in_model_map(
_, additional_request_params, _, _ = config._prepare_request_params(optional_params, model)
assert "reasoning" not in additional_request_params
assert "thinking" not in additional_request_params
dropped = [r.getMessage() for r in caplog.records if "Dropping reasoning_effort='minimal'" in r.getMessage()]
dropped: Final = [r.getMessage() for r in caplog.records if "Dropping reasoning_effort='minimal'" in r.getMessage()]
assert len(dropped) == 1 and model in dropped[0]

View file

@ -7,6 +7,7 @@ falls back to the Chat Completions bridge and rides Converse.
import json
import logging
from importlib.resources import files
from typing import Final
from unittest.mock import patch
import pytest
@ -15,6 +16,7 @@ import litellm
from litellm.llms.bedrock.common_utils import bedrock_supports_openai_responses
from litellm.llms.bedrock.responses.transformation import BedrockOpenAIResponsesConfig
from litellm.responses.file_search.emulated_handler import should_use_emulated_file_search
from litellm.responses.utils import ResponsesAPIRequestUtils
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
@ -341,7 +343,7 @@ class TestDisabledReasoningEffort:
self, model: str, local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
params = _cfg().map_openai_params(
params: Final = _cfg().map_openai_params(
response_api_optional_params={
"reasoning": {"effort": "minimal", "summary": "auto"},
"max_output_tokens": 64,
@ -350,7 +352,9 @@ class TestDisabledReasoningEffort:
drop_params=True,
)
assert params == {"reasoning": {"summary": "auto"}, "max_output_tokens": 64}
dropped = [r.getMessage() for r in caplog.records if "Dropping reasoning.effort='minimal'" in r.getMessage()]
dropped: Final = [
r.getMessage() for r in caplog.records if "Dropping reasoning.effort='minimal'" in r.getMessage()
]
assert len(dropped) == 1 and model in dropped[0]
def test_effort_only_reasoning_is_removed_when_dropped(self, local_model_cost_map):
@ -365,6 +369,17 @@ class TestDisabledReasoningEffort:
)
assert params == {"reasoning": {"effort": "low"}}
@pytest.mark.parametrize("model", ["us.openai.gpt-5.6-luna", MODEL])
def test_effort_level_the_deployment_declares_is_forwarded(self, model: str, local_model_cost_map: None) -> None:
params: Final = ResponsesAPIRequestUtils.get_optional_params_responses_api(
model=model,
responses_api_provider_config=_cfg(),
response_api_optional_params={"reasoning": {"effort": "minimal"}},
drop_params=False,
declared_reasoning_efforts=("minimal", "high"),
)
assert params["reasoning"] == {"effort": "minimal"}
def _never_fetch(url: str) -> str:
raise AssertionError(f"unexpected sync fetch of {url}")

View file

@ -9,6 +9,7 @@ gate, the URL construction for both paths, and the shared Bearer auth.
import copy
import logging
from typing import Final
import pytest
from botocore.exceptions import (
@ -21,6 +22,7 @@ import litellm
from litellm.llms.bedrock_mantle.responses.transformation import (
BedrockMantleResponsesAPIConfig,
)
from litellm.responses.utils import ResponsesAPIRequestUtils
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
@ -399,6 +401,16 @@ class TestBedrockMantleSamplingParams:
assert ("top_p" in mapped) is survives
assert ("temperature" in mapped) is survives
def test_effort_level_the_deployment_declares_is_forwarded(self, local_cost_map) -> None:
params: Final = ResponsesAPIRequestUtils.get_optional_params_responses_api(
model="openai.gpt-5.6-luna",
responses_api_provider_config=BedrockMantleResponsesAPIConfig(),
response_api_optional_params={"reasoning": {"effort": "minimal"}},
drop_params=False,
declared_reasoning_efforts=("minimal", "high"),
)
assert params["reasoning"] == {"effort": "minimal"}
def test_top_p_without_drop_params_raises_only_while_reasoning_is_active(self, local_cost_map):
with pytest.raises(litellm.UnsupportedParamsError):
BedrockMantleResponsesAPIConfig().map_openai_params(

View file

@ -2129,4 +2129,41 @@ def test_dropping_an_unsupported_thinking_logs_one_warning(
record.getMessage() for record in caplog.records if "Dropping thinking" in record.getMessage()
]
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("'disabled'", "terra-ptu", "send reasoning_effort"))
assert all(fragment in drop_warnings[0] for fragment in ("'disabled'", "terra-ptu", "model_info.base_model"))
@pytest.mark.parametrize(
("reasoning_kwargs", "warns"),
[
({"reasoning_effort": "none"}, False),
({"thinking": {"type": "disabled"}}, False),
({"reasoning_effort": "high"}, True),
({"thinking": {"type": "enabled", "budget_tokens": 1024}}, True),
],
)
def test_reasoning_drop_warns_unless_a_mapped_non_reasoning_model_was_asked_for_none(
local_model_cost_map: None,
caplog: pytest.LogCaptureFixture,
reasoning_kwargs: dict[str, object],
warns: bool,
) -> None:
litellm.register_model(
{
"azure/plain-chat-test-deployment": {
"litellm_provider": "azure",
"mode": "chat",
"max_tokens": 1024,
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_reasoning": False,
}
}
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
optional_params: Final = get_optional_params(
model="plain-chat-test-deployment", custom_llm_provider="azure", drop_params=True, **reasoning_kwargs
)
assert not set(reasoning_kwargs) & set(optional_params)
assert any("Dropping" in record.getMessage() for record in caplog.records) is warns