mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
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:
parent
00d74f7196
commit
d32087cd8e
7 changed files with 123 additions and 35 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue