diff --git a/litellm/llms/azure/chat/gpt_reasoning_transformation.py b/litellm/llms/azure/chat/gpt_reasoning_transformation.py index 5bd03f511c5..72a0500e0af 100644 --- a/litellm/llms/azure/chat/gpt_reasoning_transformation.py +++ b/litellm/llms/azure/chat/gpt_reasoning_transformation.py @@ -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, diff --git a/litellm/llms/azure_ai/chat/transformation.py b/litellm/llms/azure_ai/chat/transformation.py index 6aed7eb3c54..8bb6072d0a9 100644 --- a/litellm/llms/azure_ai/chat/transformation.py +++ b/litellm/llms/azure_ai/chat/transformation.py @@ -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( diff --git a/litellm/llms/azure_ai/responses/transformation.py b/litellm/llms/azure_ai/responses/transformation.py index da734cf8189..b19915648ab 100644 --- a/litellm/llms/azure_ai/responses/transformation.py +++ b/litellm/llms/azure_ai/responses/transformation.py @@ -232,7 +232,7 @@ class AzureAIAgentResponsesAPIConfig(AzureAIResponsesAPIConfig): model: str, drop_params: bool, *, - declared_reasoning_efforts: Collection[str], + declared_reasoning_efforts: Collection[str] | None, ) -> Mapping[str, object]: mapped: Final = super().map_openai_params_for_deployment( response_api_optional_params, model, drop_params, declared_reasoning_efforts=declared_reasoning_efforts diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index f54c352fd6e..05440d70017 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -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 diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index dd721e059a9..4175479f068 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -1331,6 +1331,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 diff --git a/litellm/llms/bedrock/responses/transformation.py b/litellm/llms/bedrock/responses/transformation.py index 7cdd7b08f99..ff44d973d97 100644 --- a/litellm/llms/bedrock/responses/transformation.py +++ b/litellm/llms/bedrock/responses/transformation.py @@ -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 diff --git a/litellm/llms/openai/chat/gpt_reasoning_transformation.py b/litellm/llms/openai/chat/gpt_reasoning_transformation.py index 0311f6c6590..8c9551d01c0 100644 --- a/litellm/llms/openai/chat/gpt_reasoning_transformation.py +++ b/litellm/llms/openai/chat/gpt_reasoning_transformation.py @@ -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: diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 903e6177e7b..9b8515a9ecd 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -32,6 +32,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 @@ -55,6 +56,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: @@ -417,12 +421,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": @@ -464,17 +475,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. @@ -494,7 +508,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=( @@ -512,7 +528,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): @@ -520,6 +536,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: diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 05d6a36c1f6..b0db0516658 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -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. diff --git a/litellm/router_utils/reasoning_effort_capability.py b/litellm/router_utils/reasoning_effort_capability.py index 16b8a22a40f..3c3fad93579 100644 --- a/litellm/router_utils/reasoning_effort_capability.py +++ b/litellm/router_utils/reasoning_effort_capability.py @@ -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, ) diff --git a/litellm/utils.py b/litellm/utils.py index adf195668ee..a656d8ebca3 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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 @@ -3852,7 +3852,7 @@ def _resolve_builtin_model_cost_entry(key: str, provider: str) -> _BuiltinCostMa return None -def is_generalized_model_info(model_info: ModelInfo) -> bool: +def is_generalized_model_info(model_info: ModelInfoBase) -> bool: """Whether ``model_info`` came from a fallback-generalization capability rule. Detected as the resolved key missing ``litellm.model_cost`` while matching a @@ -3868,10 +3868,12 @@ def is_generalized_model_info(model_info: ModelInfo) -> bool: _REGISTRATION_ATTEMPTS: Final = 3 +_ModelInfoT: Final = TypeVar("_ModelInfoT", bound=ModelInfoBase) -def lookup_model_info_for_registration(model: str, custom_llm_provider: str | None) -> ModelInfo | ModelInfoLookupMiss: + +def _lookup_mapped_model_info(lookup: Callable[[], _ModelInfoT]) -> _ModelInfoT | ModelInfoLookupMiss: try: - info: Final = get_model_info(model=model, custom_llm_provider=custom_llm_provider) + info: Final = lookup() except ModelNotMappedError: return ModelInfoLookupMiss(tripped=False) except Exception: @@ -3879,6 +3881,16 @@ def lookup_model_info_for_registration(model: str, custom_llm_provider: str | No return ModelInfoLookupMiss(tripped=False) if is_generalized_model_info(info) else info +def lookup_model_info_for_registration(model: str, custom_llm_provider: str | None) -> ModelInfo | ModelInfoLookupMiss: + return _lookup_mapped_model_info(lambda: get_model_info(model=model, custom_llm_provider=custom_llm_provider)) + + +def lookup_catalog_model_info(model: str, custom_llm_provider: str | None) -> ModelInfoBase | ModelInfoLookupMiss: + return _lookup_mapped_model_info( + lambda: get_model_info_helper(model=model, custom_llm_provider=custom_llm_provider, catalog_only=True) + ) + + def _get_builtin_model_info_for_registration( model: str, custom_llm_provider: str | None ) -> ModelInfo | ModelInfoLookupMiss: @@ -5144,6 +5156,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_catalog_model_info(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_catalog_model_info(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 @@ -5233,19 +5285,30 @@ def get_optional_params( verbose_logger.debug( "\nLiteLLM: Non-Default params passed to completion() %s", _debug_params_repr(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, @@ -6761,6 +6824,8 @@ def get_model_info_helper( custom_llm_provider: str | None = None, api_base: str | None = None, api_key: str | None = None, + *, + catalog_only: bool = False, ) -> ModelInfoBase: """ Helper for 'get_model_info'. Separated out to avoid infinite loop caused by returning 'supported_openai_param's @@ -6794,7 +6859,7 @@ def get_model_info_helper( model_cost_custom_llm_provider: Final = custom_llm_provider ######################### provider_config: BaseLLMModelInfo | None = None - if custom_llm_provider and custom_llm_provider in LlmProvidersSet: + if not catalog_only and custom_llm_provider and custom_llm_provider in LlmProvidersSet: provider_config = ProviderConfigManager.get_provider_model_info( model=model, provider=LlmProviders(custom_llm_provider) ) @@ -6818,7 +6883,7 @@ def get_model_info_helper( e, ) - if custom_llm_provider == "huggingface": + if not catalog_only and custom_llm_provider == "huggingface": max_tokens: Final = _get_max_position_embeddings(model_name=model) return ModelInfoBase( key=model, diff --git a/tests/unit/llms/azure/chat/test_azure_gpt_reasoning_transformation.py b/tests/unit/llms/azure/chat/test_azure_gpt_reasoning_transformation.py index a82dd98e31d..2d9381ec16a 100644 --- a/tests/unit/llms/azure/chat/test_azure_gpt_reasoning_transformation.py +++ b/tests/unit/llms/azure/chat/test_azure_gpt_reasoning_transformation.py @@ -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. diff --git a/tests/unit/llms/bedrock/chat/test_converse_transformation.py b/tests/unit/llms/bedrock/chat/test_converse_transformation.py index 154797bab5a..2d75ce1f22f 100644 --- a/tests/unit/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/unit/llms/bedrock/chat/test_converse_transformation.py @@ -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( diff --git a/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py b/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py index 6da131f38cc..a1466cfdae4 100644 --- a/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py +++ b/tests/unit/llms/bedrock/responses/test_bedrock_openai_responses.py @@ -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( diff --git a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py index a2289167ad4..0ae6a73968d 100644 --- a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py +++ b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py @@ -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): diff --git a/tests/unit/llms/openai/responses/test_openai_responses_transformation.py b/tests/unit/llms/openai/responses/test_openai_responses_transformation.py index a185cdaf718..f5a33f87fd0 100644 --- a/tests/unit/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/unit/llms/openai/responses/test_openai_responses_transformation.py @@ -1,4 +1,5 @@ import json +import logging from collections.abc import Callable, Mapping from types import SimpleNamespace from typing import Final, cast @@ -19,6 +20,7 @@ from litellm.llms.hosted_vllm.responses.transformation import HostedVLLMResponse from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.llms.xai.responses.transformation import XAIResponsesAPIConfig from litellm.responses.litellm_completion_transformation.transformation import LiteLLMCompletionResponsesConfig +from litellm.responses.utils import ResponsesAPIRequestUtils from litellm.types.llms.openai import ( ImageGenerationPartialImageEvent, IncompleteDetails, @@ -2479,6 +2481,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 diff --git a/tests/unit/llms/openai/test_gpt_reasoning_transformation.py b/tests/unit/llms/openai/test_gpt_reasoning_transformation.py index 4cdc674c774..807a43c5d9d 100644 --- a/tests/unit/llms/openai/test_gpt_reasoning_transformation.py +++ b/tests/unit/llms/openai/test_gpt_reasoning_transformation.py @@ -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()] diff --git a/tests/unit/router_utils/test_reasoning_effort_capability.py b/tests/unit/router_utils/test_reasoning_effort_capability.py index d0564812c48..1d8ee338cbb 100644 --- a/tests/unit/router_utils/test_reasoning_effort_capability.py +++ b/tests/unit/router_utils/test_reasoning_effort_capability.py @@ -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( diff --git a/tests/unit/test_utils_get_optional_params.py b/tests/unit/test_utils_get_optional_params.py index 13a64577efb..05b776cb873 100644 --- a/tests/unit/test_utils_get_optional_params.py +++ b/tests/unit/test_utils_get_optional_params.py @@ -1,4 +1,5 @@ import json +import logging from typing import Final from unittest.mock import MagicMock, patch @@ -2097,3 +2098,146 @@ 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")) + + +@pytest.mark.parametrize("provider", ["ollama", "ollama_chat", "lemonade", "huggingface"]) +def test_dropping_thinking_for_an_unmapped_local_model_sends_no_request( + local_model_cost_map: None, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch, provider: str +) -> None: + outbound: Final = respx.MockRouter(assert_all_mocked=False) + monkeypatch.setattr( + litellm, "module_level_client", HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(outbound.handler))) + ) + + with caplog.at_level(logging.WARNING, logger="LiteLLM"): + optional_params: Final = get_optional_params( + model="unmapped-local-model", custom_llm_provider=provider, thinking={"type": "disabled"}, drop_params=True + ) + + assert "thinking" not in optional_params + assert len([record for record in caplog.records if "Dropping thinking" in record.getMessage()]) == 1 + assert not outbound.calls + + +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()]