This commit is contained in:
Mateo Wang 2026-10-29 00:57:12 +00:00 • committed by GitHub
commit 40706c3668
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 570 additions and 55 deletions

View file

@ -24,6 +24,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
GPT5_SERIES_ROUTE = "gpt5_series/"
OPT_IN_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("xhigh", "none"))
LOOKS_UP_BASE_MODEL: ClassVar[bool] = True
@classmethod
def _model_map_lookup_name(cls, model: str) -> str:
@ -88,7 +89,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
drop_params: bool,
api_version: str = "",
*,
declared_reasoning_efforts: Collection[str] = (),
declared_reasoning_efforts: Collection[str] | None = None,
) -> dict:
return OpenAIGPT5Config.map_openai_params(
self,

View file

@ -106,7 +106,7 @@ class AzureAIStudioConfig(OpenAIConfig):
model: str,
drop_params: bool,
*,
declared_reasoning_efforts: Collection[str] = (),
declared_reasoning_efforts: Collection[str] | None = None,
) -> dict[str, object]: # mutable-ok: OpenAIConfig.map_openai_params signature
if not azureAIGPT5Config.is_model_gpt_5_model(model):
return super().map_openai_params(

View file

@ -117,6 +117,7 @@ from litellm.utils import (
)
from ..common_utils import (
DROP_DISABLED_REASONING_EFFORT_WARNING,
BedrockError,
BedrockModelInfo,
bedrock_cache_point_ttl_supported,
@ -1192,11 +1193,7 @@ class AmazonConverseConfig(BaseConfig):
),
status_code=400,
)
verbose_logger.debug(
"Dropping unsupported `reasoning_effort=%s` for Bedrock model=%s.",
value,
model,
)
verbose_logger.warning(DROP_DISABLED_REASONING_EFFORT_WARNING, "reasoning_effort", 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

@ -1314,6 +1314,12 @@ 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)
DROP_DISABLED_REASONING_EFFORT_WARNING: Final = (
"Dropping %s=%r for Bedrock model=%r (drop_params=True) because the model map marks that level unsupported. "
"Send a level the model supports."
)
def bedrock_reasoning_effort_disabled(model: str, effort: str) -> bool:
from litellm.utils import is_explicitly_disabled_factory

View file

@ -52,6 +52,7 @@ from litellm.llms.base_llm.responses.codex_compat import drop_unsupported_tools,
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
from litellm.llms.bedrock.common_utils import (
BEDROCK_CHAT_COMPLETIONS_ROUTE_PREFIX,
DROP_DISABLED_REASONING_EFFORT_WARNING,
BedrockError,
bedrock_reasoning_effort_disabled,
bedrock_supports_openai_responses,
@ -173,7 +174,7 @@ def _without_disabled_reasoning_effort(
),
status_code=400,
)
verbose_logger.debug("Dropping unsupported `reasoning.effort=%s` for Bedrock model=%s.", effort, model)
verbose_logger.warning(DROP_DISABLED_REASONING_EFFORT_WARNING, "reasoning.effort", 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

View file

@ -5,8 +5,10 @@ from collections.abc import Collection
from typing import ClassVar, Final
import litellm
from litellm._logging import verbose_logger
from litellm.utils import (
declared_value_factory,
has_catalog_entry,
is_explicitly_disabled_factory,
supports_factory,
)
@ -16,6 +18,25 @@ from .gpt_transformation import OpenAIGPTConfig
_GPT_SERIES_VERSION: Final = re.compile(r"^gpt-(\d+)(?:\.(\d+))?(?=[.-]|$)")
def refused_reasoning_effort_warning(*, suggest_declared_levels: bool, suggest_base_model: bool) -> str:
return "".join(
(
"Dropping %s=%r for model=%r (drop_params=True) because the model map does not mark that level "
"supported for %r.",
(
" If the deployment supports it, list every level it supports in model_info.reasoning_effort_levels."
if suggest_declared_levels
else ""
),
(
" If the deployment name is not its underlying model, set model_info.base_model to that model."
if suggest_base_model
else ""
),
)
)
def _catalogue_declares_default_effort() -> bool:
"""Whether the loaded cost map carries default_reasoning_effort for ANY entry.
@ -93,6 +114,7 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
OPT_IN_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("xhigh",))
ROUTE_SPECIFIC_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("max",))
LOOKS_UP_BASE_MODEL: ClassVar[bool] = False
@classmethod
def is_model_gpt_5_model(cls, model: str) -> bool:
@ -307,7 +329,7 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
model: str,
drop_params: bool,
*,
declared_reasoning_efforts: Collection[str] = (),
declared_reasoning_efforts: Collection[str] | None = None,
) -> dict:
if self.is_model_gpt_5_search_model(model):
if "max_tokens" in non_default_params:
@ -338,10 +360,21 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
if (
requested_effort is not None
and requested_effort not in declared_reasoning_efforts
and requested_effort not in (declared_reasoning_efforts or ())
and self.refuses_reasoning_effort_level(model, requested_effort)
):
if litellm.drop_params or drop_params:
verbose_logger.warning(
refused_reasoning_effort_warning(
suggest_declared_levels=declared_reasoning_efforts is not None,
suggest_base_model=self.LOOKS_UP_BASE_MODEL
and not has_catalog_entry(self._model_map_lookup_name(model), None),
),
"reasoning_effort",
requested_effort,
model,
self._model_map_lookup_name(model),
)
non_default_params.pop("reasoning_effort", None)
optional_params.pop("reasoning_effort", None)
else:

View file

@ -27,6 +27,7 @@ from litellm.litellm_core_utils.url_utils import encode_url_path_segment, join_a
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
from litellm.llms.openai.chat.gpt_reasoning_transformation import (
is_gpt_reasoning_series_name,
refused_reasoning_effort_warning,
unsupported_reasoning_effort_message,
)
from litellm.responses.litellm_completion_transformation.custom_tools import TOOL_CALL_ITEM_ID_PREFIX_BY_TYPE
@ -50,6 +51,9 @@ from ..common_utils import OpenAIError
from ..workload_identity import get_workload_identity_bearer_token, resolve_openai_workload_identity_config
OPENAI_RESPONSES_API_MIN_MAX_OUTPUT_TOKENS: Final = 16
DROP_UNSUPPORTED_REASONING_WARNING: Final = (
"Dropping reasoning=%r for model=%r (drop_params=True) because its model cost map entry lacks supports_reasoning."
)
_RAW_RESPONSE_JSON: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True))
if TYPE_CHECKING:
@ -324,12 +328,19 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
return declared
return info["key"] in _bundled_openai_reasoning_models()
@staticmethod
def _is_known_non_reasoning_model(model: str) -> bool:
from litellm.utils import is_known_non_reasoning_model
return is_known_non_reasoning_model(model.split("/")[-1], LlmProviders.OPENAI.value)
@staticmethod
def _requests_reasoning_effort(reasoning: object) -> bool:
effort: Final = (
reasoning.get("effort") if isinstance(reasoning, Mapping) else getattr(reasoning, "effort", None)
)
return effort is not None
return OpenAIResponsesAPIConfig._requested_effort(reasoning) is not None
@staticmethod
def _requested_effort(reasoning: object) -> object:
return reasoning.get("effort") if isinstance(reasoning, Mapping) else getattr(reasoning, "effort", None)
@staticmethod
def _enforce_min_max_output_tokens(max_output_tokens: "int | None") -> "int | None":
@ -371,17 +382,20 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
) -> dict:
return dict(
self.map_openai_params_for_deployment(
response_api_optional_params, model, drop_params, declared_reasoning_efforts=()
response_api_optional_params, model, drop_params, declared_reasoning_efforts=None
)
)
def honors_declared_reasoning_efforts(self) -> bool:
return type(self).map_openai_params is OpenAIResponsesAPIConfig.map_openai_params
def map_openai_params_for_deployment(
self,
response_api_optional_params: ResponsesAPIOptionalRequestParams,
model: str,
drop_params: bool,
*,
declared_reasoning_efforts: Collection[str],
declared_reasoning_efforts: Collection[str] | None,
) -> Mapping[str, object]:
"""No mapping applied since inputs are in OpenAI spec already.
@ -401,7 +415,9 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
and not self._supports_reasoning_param(model=model)
):
if drop_params or litellm.drop_params:
params.pop("reasoning", None)
dropped_reasoning: Final = params.pop("reasoning", None)
if self._requested_effort(dropped_reasoning) != "none" or not self._is_known_non_reasoning_model(model):
verbose_logger.warning(DROP_UNSUPPORTED_REASONING_WARNING, dropped_reasoning, model)
else:
raise litellm.UnsupportedParamsError(
message=(
@ -419,7 +435,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
if (
isinstance(reasoning, dict)
and isinstance(requested_effort, str)
and requested_effort not in declared_reasoning_efforts
and requested_effort not in (declared_reasoning_efforts or ())
and self._effort_level_is_disabled(lookup_name, requested_effort)
):
if not (drop_params or litellm.drop_params):
@ -427,6 +443,15 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
message=unsupported_reasoning_effort_message("reasoning.effort", requested_effort, model),
status_code=400,
)
verbose_logger.warning(
refused_reasoning_effort_warning(
suggest_declared_levels=declared_reasoning_efforts is not None, suggest_base_model=False
),
"reasoning.effort",
requested_effort,
model,
lookup_name,
)
remaining: Final = reasoning.copy()
remaining.pop("effort", None)
if remaining:

View file

@ -131,14 +131,14 @@ def _provider_mapped_params(
params: ResponsesAPIOptionalRequestParams,
model: str,
drop_params: bool,
declared_reasoning_efforts: Collection[str],
declared_reasoning_efforts: Collection[str] | None,
) -> Mapping[str, object]:
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
if (
declared_reasoning_efforts
declared_reasoning_efforts is not None
and isinstance(config, OpenAIResponsesAPIConfig)
and type(config).map_openai_params is OpenAIResponsesAPIConfig.map_openai_params
and config.honors_declared_reasoning_efforts()
):
return config.map_openai_params_for_deployment(
params, model, drop_params, declared_reasoning_efforts=declared_reasoning_efforts
@ -287,7 +287,7 @@ class ResponsesAPIRequestUtils:
response_api_optional_params: ResponsesAPIOptionalRequestParams,
allowed_openai_params: list[str] | None = None,
drop_params: bool | None = None,
declared_reasoning_efforts: Collection[str] = (),
declared_reasoning_efforts: Collection[str] | None = None,
) -> dict:
"""
Get optional parameters for the responses API.

View file

@ -426,6 +426,27 @@ def _log_remap(
logged_deployment_id: Final = _single_line(deployment_id)
logged_model_group: Final = _single_line(model_group)
logged_deployment_model: Final = _single_line(deployment_model)
extra: Final = MappingProxyType(
{
"reasoning_effort_deployment_id": logged_deployment_id,
"reasoning_effort_model_group": logged_model_group,
"reasoning_effort_deployment_model": logged_deployment_model,
"reasoning_effort_requested": logged_requested_level,
"reasoning_effort_sent": logged_sent_level,
"reasoning_effort_rounding": rounding,
}
)
if sent_level is None and requested_level != _DEFAULT_EFFORT:
verbose_logger.warning(
"Dropping reasoning level %s (drop_params=True) for deployment %s, model group %s, model %s because its "
"model_info.reasoning_effort_levels does not list that level. List every level the deployment supports.",
logged_requested_level,
logged_deployment_id,
logged_model_group,
logged_deployment_model,
extra=extra,
)
return
verbose_logger.info(
"reasoning_effort %s sent as %s (rounding %s) to deployment %s, model group %s, model %s",
logged_requested_level,
@ -434,14 +455,7 @@ def _log_remap(
logged_deployment_id,
logged_model_group,
logged_deployment_model,
extra={
"reasoning_effort_deployment_id": logged_deployment_id,
"reasoning_effort_model_group": logged_model_group,
"reasoning_effort_deployment_model": logged_deployment_model,
"reasoning_effort_requested": logged_requested_level,
"reasoning_effort_sent": logged_sent_level,
"reasoning_effort_rounding": rounding,
},
extra=extra,
)

View file

@ -91,7 +91,7 @@ from litellm.litellm_core_utils.fallback_generalizations import (
match_capability_generalizations,
match_fill_missing_generalizations,
)
from litellm.litellm_core_utils.get_model_cost_map import AliasedCostEntry
from litellm.litellm_core_utils.get_model_cost_map import AliasedCostEntry, GetModelCostMap
from litellm.litellm_core_utils.model_param_helper import peek_reasoning_summary_aliases
from litellm.litellm_core_utils.offline_mode import is_offline_mode
from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload
@ -4974,6 +4974,46 @@ def _declared_reasoning_efforts(model_info: Mapping[str, object] | None) -> tupl
return deployment_reasoning_effort_levels(model_info) or ()
_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_known_non_reasoning_model(model: str, custom_llm_provider: str | None) -> bool:
info: Final = lookup_model_info_for_registration(model, custom_llm_provider)
if isinstance(info, ModelInfoLookupMiss) or info.get("supports_reasoning") is True:
return False
return info.get("supports_reasoning") is False or info["key"] in GetModelCostMap.loaded_model_cost_map()
def has_catalog_entry(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["key"] in GetModelCostMap.loaded_model_cost_map()
def _dropped_reasoning_warning(model: str, custom_llm_provider: str | None) -> str:
return "".join(
(
"Dropping %s=%r for model=%r (drop_params=True) because %s does not support %s for it.",
(
""
if has_catalog_entry(model, custom_llm_provider)
else " If the deployment is a reasoning model, set model_info.base_model to its underlying model."
),
)
)
def _warns_on_dropped_reasoning(value: object, model: str, custom_llm_provider: str | None) -> bool:
return not (_turns_reasoning_off(value) and is_known_non_reasoning_model(model, custom_llm_provider))
def get_optional_params(
# use the openai defaults
# https://platform.openai.com/docs/api-reference/chat/create
@ -5061,19 +5101,30 @@ 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 = {}
for k in non_default_params:
if k not in supported_params:
if k in PROVIDER_UNVALIDATED_PARAMS:
continue
if k == "n" and n == 1: # langchain sends n=1 as a default value
continue # skip this param
unsupported_params[k] = non_default_params[k]
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 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(
_dropped_reasoning_warning(base_model or model, custom_llm_provider),
reasoning_param,
unsupported_params[reasoning_param],
model,
custom_llm_provider,
reasoning_param,
)
else:
raise UnsupportedParamsError(
status_code=500,

View file

@ -1,3 +1,4 @@
import logging
from collections.abc import Mapping
from typing import Final
@ -279,6 +280,29 @@ def test_azure_gpt5_reasoning_effort_none_dropped(config: AzureOpenAIGPT5Config)
assert "reasoning_effort" not in params or params.get("reasoning_effort") != "none"
@pytest.mark.parametrize(("model", "names_base_model"), [("azure/litellm-qa-sol-alias", True), ("azure/gpt-5", False)])
def test_azure_dropped_effort_warning_suggests_base_model_only_for_a_name_the_map_lacks(
config: AzureOpenAIGPT5Config, caplog: pytest.LogCaptureFixture, model: str, names_base_model: bool
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
params: Final = config.map_openai_params(
non_default_params={"reasoning_effort": "none"},
optional_params={},
model=model,
drop_params=True,
api_version="2025-04-01-preview",
declared_reasoning_efforts=(),
)
assert "reasoning_effort" not in params
drop_warnings: Final = [
record.getMessage() for record in caplog.records if "Dropping reasoning_effort='none'" in record.getMessage()
]
assert len(drop_warnings) == 1
assert "model_info.reasoning_effort_levels" in drop_warnings[0]
assert ("model_info.base_model" in drop_warnings[0]) is names_base_model
# Logprobs support tests for Azure GPT-5.2
def test_azure_gpt5_2_supports_logprobs(config: AzureOpenAIGPT5Config):
"""Test that Azure GPT-5.2 models support logprobs parameters.

View file

@ -1,6 +1,7 @@
import base64
import copy
import json
import logging
import os
import re
from collections.abc import Iterator, Mapping
@ -966,7 +967,9 @@ def test_reasoning_effort_maps_to_reasoning_effort_for_openai_gpt5_converse(mode
"us.openai.gpt-6-astra",
],
)
def test_openai_gpt5_converse_rejects_effort_level_disabled_in_model_map(model, local_model_cost_map):
def test_openai_gpt5_converse_rejects_effort_level_disabled_in_model_map(
model: str, local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
config = AmazonConverseConfig()
assert litellm.utils.is_explicitly_disabled_factory(
model=model, custom_llm_provider="bedrock_converse", key="supports_minimal_reasoning_effort"
@ -980,15 +983,19 @@ def test_openai_gpt5_converse_rejects_effort_level_disabled_in_model_map(model,
drop_params=False,
)
optional_params = config.map_openai_params(
non_default_params={"reasoning_effort": "minimal"},
optional_params={},
model=model,
drop_params=True,
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
optional_params: Final = 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
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]
assert "model_info" not in dropped[0]
@pytest.mark.parametrize(

View file

@ -7,13 +7,17 @@ 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
import litellm
from litellm.llms.bedrock.common_utils import bedrock_supports_openai_responses
from litellm.llms.bedrock.responses.transformation import BedrockOpenAIResponsesConfig
from litellm.llms.bedrock.responses.transformation import (
BedrockOpenAIResponsesConfig,
_without_disabled_reasoning_effort,
)
from litellm.responses.file_search.emulated_handler import should_use_emulated_file_search
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
@ -337,13 +341,37 @@ class TestDisabledReasoningEffort:
)
@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,
)
def test_effort_level_disabled_in_model_map_is_dropped_with_a_warning(
self, model: str, local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
params: Final = _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}
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]
assert "model_info" not in dropped[0]
def test_bedrock_disabled_effort_drop_warns_on_its_own(
self, local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
params: Final = _without_disabled_reasoning_effort(
{"reasoning": {"effort": "minimal"}, "max_output_tokens": 64}, MODEL, True
)
assert params == {"max_output_tokens": 64}
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):
params = _cfg().map_openai_params(

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 (
@ -572,6 +573,32 @@ class TestBedrockMantleServiceTier:
assert "priority" in str(mock_warning.call_args)
class TestBedrockMantleRefusedEffort:
def test_dropping_a_refused_effort_does_not_suggest_declaring_levels_mantle_ignores(
self, local_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
refused: Final = next(
level
for level in ("none", "minimal", "low")
if litellm.model_cost["gpt-6.1-sol"].get(f"supports_{level}_reasoning_effort") is False
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
params: Final = BedrockMantleResponsesAPIConfig().map_openai_params(
response_api_optional_params={"reasoning": {"effort": refused}},
model="openai.gpt-6.1-sol",
drop_params=True,
)
assert "reasoning" not in params
drop_warnings: Final = [
record.getMessage()
for record in caplog.records
if f"Dropping reasoning.effort={refused!r}" in record.getMessage()
]
assert len(drop_warnings) == 1
assert "model_info" not in drop_warnings[0]
class TestBedrockMantleReasoningSummary:
@pytest.mark.parametrize("summary", ["concise", "detailed"])
def test_unsupported_reasoning_summary_dropped_when_drop_params_true(self, summary):

View file

@ -1,4 +1,5 @@
import json
import logging
from collections.abc import Callable, Mapping
from types import SimpleNamespace
from typing import Final, cast
@ -15,6 +16,7 @@ from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIC
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.responses.litellm_completion_transformation.transformation import LiteLLMCompletionResponsesConfig
from litellm.responses.utils import ResponsesAPIRequestUtils
from litellm.types.llms.openai import (
ImageGenerationPartialImageEvent,
IncompleteDetails,
@ -2387,6 +2389,105 @@ class TestResponsesSurfaceSharesTheEffortRule:
)
assert mapped["reasoning"] == {"summary": "auto"}
@pytest.mark.parametrize(
("declared_reasoning_efforts", "suggests_declared_levels"),
[((), True), (None, False)],
ids=["responses route reads model_info levels", "compact route reads none"],
)
def test_dropping_a_refused_effort_logs_one_warning_naming_only_the_fix_its_route_honors(
self,
local_model_cost_map: None,
caplog: pytest.LogCaptureFixture,
declared_reasoning_efforts: tuple[str, ...] | None,
suggests_declared_levels: bool,
) -> None:
refused: Final = next(
level
for level in ("none", "minimal", "low")
if litellm.model_cost["gpt-6.1-sol"].get(f"supports_{level}_reasoning_effort") is False
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
mapped: Final = ResponsesAPIRequestUtils.get_optional_params_responses_api(
model="gpt-6.1-sol",
responses_api_provider_config=OpenAIResponsesAPIConfig(),
response_api_optional_params={"reasoning": {"effort": refused}},
drop_params=True,
declared_reasoning_efforts=declared_reasoning_efforts,
)
assert "reasoning" not in mapped
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert len(drop_warnings) == 1
assert repr(refused) in drop_warnings[0] and "gpt-6.1-sol" in drop_warnings[0]
assert ("model_info.reasoning_effort_levels" in drop_warnings[0]) is suggests_declared_levels
assert "model_info.base_model" not in drop_warnings[0]
def test_dropping_reasoning_for_a_model_without_supports_reasoning_logs_one_warning(
self, local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
mapped: Final = OpenAIResponsesAPIConfig().map_openai_params(
response_api_optional_params={"reasoning": {"effort": "medium"}},
model="gpt-4o",
drop_params=True,
)
assert "reasoning" not in mapped
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("'medium'", "gpt-4o", "supports_reasoning"))
def test_turning_reasoning_off_for_a_model_without_supports_reasoning_drops_it_silently(
self, local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
mapped: Final = OpenAIResponsesAPIConfig().map_openai_params(
response_api_optional_params={"reasoning": {"effort": "none", "summary": "auto"}},
model="gpt-4o",
drop_params=True,
)
assert "reasoning" not in mapped
assert not [record for record in caplog.records if "Dropping" in record.getMessage()]
def test_turning_reasoning_off_warns_when_only_custom_pricing_describes_the_model(
self, local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
litellm.register_model(
{
"pricing-only-test-model": {
"litellm_provider": "openai",
"mode": "responses",
"input_cost_per_token": 0.000001,
"output_cost_per_token": 0.000002,
}
}
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
mapped: Final = OpenAIResponsesAPIConfig().map_openai_params(
response_api_optional_params={"reasoning": {"effort": "none"}},
model="pricing-only-test-model",
drop_params=True,
)
assert "reasoning" not in mapped
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("'none'", "pricing-only-test-model"))
def test_a_forwarded_effort_logs_no_drop_warning(
self, local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
mapped: Final = OpenAIResponsesAPIConfig().map_openai_params(
response_api_optional_params={"reasoning": {"effort": "medium"}},
model="gpt-6.1-sol",
drop_params=True,
)
assert mapped["reasoning"] == {"effort": "medium"}
assert not [record for record in caplog.records if "Dropping" in record.getMessage()]
@pytest.mark.parametrize("level", ["none", "minimal", "low", "medium", "high", "xhigh", "max"])
def test_gpt5_3_codex_forwards_exactly_the_effort_levels_its_map_row_allows(self, local_model_cost_map, level):
"""The gate turns a stale false into a local refusal, so the Responses-only codex row has to

View file

@ -1,3 +1,4 @@
import logging
import re
from collections.abc import Mapping
from typing import Final
@ -1589,3 +1590,55 @@ def test_gpt_6_astra_takes_the_reasoning_series_request_shape():
assert "max_tokens" not in params
assert params["reasoning_effort"] == "max"
assert params["verbosity"] == "low"
@pytest.mark.parametrize(
("custom_llm_provider", "level", "advice"),
[
("azure", "none", frozenset(("model_info.reasoning_effort_levels", "model_info.base_model"))),
("azure", "xhigh", frozenset(("model_info.reasoning_effort_levels", "model_info.base_model"))),
("openai", "xhigh", frozenset(("model_info.reasoning_effort_levels",))),
("azure_ai", "xhigh", frozenset(("model_info.reasoning_effort_levels",))),
("github_copilot", "xhigh", frozenset()),
],
)
def test_a_reasoning_effort_the_level_gate_drops_logs_a_warning_naming_only_the_fixes_its_route_honors(
caplog: pytest.LogCaptureFixture, custom_llm_provider: str, level: str, advice: frozenset[str]
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
params: Final = litellm.get_optional_params(
model="gpt-5.6-terra-ptu",
custom_llm_provider=custom_llm_provider,
reasoning_effort=level,
drop_params=True,
)
assert "reasoning_effort" not in params
drop_warnings: Final = [
record.getMessage()
for record in caplog.records
if record.levelno == logging.WARNING and "reasoning_effort" in record.getMessage()
]
assert len(drop_warnings) == 1
assert repr(level) in drop_warnings[0] and "gpt-5.6-terra-ptu" in drop_warnings[0]
assert (
frozenset(
fragment
for fragment in ("model_info.reasoning_effort_levels", "model_info.base_model")
if fragment in drop_warnings[0]
)
== advice
)
def test_a_reasoning_effort_the_level_gate_forwards_logs_no_drop_warning(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
params: Final = litellm.get_optional_params(
model="gpt-5.6-terra-ptu",
custom_llm_provider="azure",
reasoning_effort="medium",
drop_params=True,
)
assert params["reasoning_effort"] == "medium"
assert not [record for record in caplog.records if "reasoning_effort" in record.getMessage()]

View file

@ -626,8 +626,9 @@ class TestResolveDeploymentReasoningEffort:
_sent("turbo\nforged line", drop_params=True)
assert [record.getMessage() for record in caplog.records] == [
"reasoning_effort unrecognized sent as None (rounding up) to deployment deployment-1, "
"model group kimi-k3, model hosted_vllm/my-kimi"
"Dropping reasoning level unrecognized (drop_params=True) for deployment deployment-1, model group "
"kimi-k3, model hosted_vllm/my-kimi because its model_info.reasoning_effort_levels does not list that "
"level. List every level the deployment supports."
]
def test_line_breaks_in_logged_names_cannot_forge_a_log_line(self, caplog: pytest.LogCaptureFixture) -> None:
@ -669,6 +670,27 @@ class TestResolveDeploymentReasoningEffort:
"reasoning_effort_rounding": "up",
}
def test_only_a_dropped_level_logs_a_warning(self, caplog: pytest.LogCaptureFixture) -> None:
verbose_logger.addHandler(caplog.handler)
verbose_logger.propagate = False
verbose_logger.setLevel(logging.INFO)
declared: Final = {"reasoning_effort_levels": ["low", "high"]}
_sent("none", declared, drop_params=True)
_sent("medium")
_sent("default", declared, drop_params=True)
_sent("default", declared)
assert [record.levelno for record in caplog.records] == [
logging.WARNING,
logging.INFO,
logging.INFO,
logging.INFO,
]
dropped: Final = caplog.records[0].getMessage()
assert "Dropping reasoning level none" in dropped
assert "model_info.reasoning_effort_levels" in dropped
class TestAzureGpt6SolAndLunaAdvertiseTheOpenAiLevels:
@pytest.mark.parametrize(

View file

@ -1,4 +1,5 @@
import json
import logging
from typing import Final
from unittest.mock import MagicMock, patch
@ -2097,3 +2098,127 @@ def test_supports_system_message_merges_system_prompt_into_user_message() -> Non
]
assert map_system_message_pt(messages=messages) == [{"role": "user", "content": "Listen here! Hello there!"}]
def test_dropping_an_unsupported_reasoning_effort_logs_one_warning(
local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
optional_params: Final = get_optional_params(
model="terra-ptu", custom_llm_provider="azure", reasoning_effort="none", drop_params=True
)
assert "reasoning_effort" not in optional_params
drop_warnings: Final = [
record.getMessage() for record in caplog.records if "Dropping reasoning_effort" in record.getMessage()
]
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("'none'", "terra-ptu", "model_info.base_model"))
def test_dropping_an_unsupported_thinking_logs_one_warning(
local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
optional_params: Final = get_optional_params(
model="terra-ptu", custom_llm_provider="azure", thinking={"type": "disabled"}, drop_params=True
)
assert "thinking" not in optional_params
drop_warnings: Final = [
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", "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
@pytest.mark.parametrize("reasoning_kwargs", [{"reasoning_effort": "none"}, {"thinking": {"type": "disabled"}}])
def test_turning_reasoning_off_warns_when_only_custom_pricing_describes_the_model(
local_model_cost_map: None, caplog: pytest.LogCaptureFixture, reasoning_kwargs: dict[str, object]
) -> None:
litellm.register_model(
{
"azure/pricing-only-test-deployment": {
"litellm_provider": "azure",
"mode": "chat",
"input_cost_per_token": 0.000001,
"output_cost_per_token": 0.000002,
}
}
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
optional_params: Final = get_optional_params(
model="pricing-only-test-deployment", custom_llm_provider="azure", drop_params=True, **reasoning_kwargs
)
assert not set(reasoning_kwargs) & set(optional_params)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("pricing-only-test-deployment", "model_info.base_model"))
def test_dropping_reasoning_for_a_catalog_reasoning_model_does_not_suggest_base_model(
local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
assert litellm.model_cost["azure/gpt-5.6-sol"].get("supports_reasoning") is True
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
optional_params: Final = get_optional_params(
model="gpt-5.6-sol", custom_llm_provider="azure", thinking={"type": "disabled"}, drop_params=True
)
assert "thinking" not in optional_params
drop_warnings: Final = [
record.getMessage() for record in caplog.records if "Dropping thinking" in record.getMessage()
]
assert len(drop_warnings) == 1
assert "model_info.base_model" not in drop_warnings[0]
def test_turning_reasoning_off_for_a_catalog_non_reasoning_model_drops_it_silently(
local_model_cost_map: None, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
optional_params: Final = get_optional_params(
model="gpt-4o", custom_llm_provider="azure", reasoning_effort="none", drop_params=True
)
assert "reasoning_effort" not in optional_params
assert not [record for record in caplog.records if "Dropping" in record.getMessage()]