mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-30 06:43:27 +00:00
fix(openai): warn when the GPT-5 reasoning effort gate drops a level under drop_params (#47761)
* fix(openai): warn when the GPT-5 reasoning effort gate drops a level under drop_params A GPT-5 request whose reasoning_effort the model map does not enable for that model lost the effort with no log line under drop_params. An Azure deployment name missing from the cost map lost reasoning_effort none this way and ran at the model's default effort. The gate now logs a warning naming the model, the level, the map entry it checked, and the model_info.base_model and model_info.reasoning_effort_levels fixes. * fix(openai): warn when the Responses route or a deployment's declared levels drop a reasoning effort The /v1/responses gate dropped a row-disabled GPT-5 effort, and reasoning for an OpenAI model without supports_reasoning, with no log line, and the router logged a declared-level drop only at INFO. Both Responses drops now log a WARNING and the router logs its drop at WARNING. The warning now leads with model_info.reasoning_effort_levels, since base_model only helps a deployment named differently from its underlying model. * fix: warn only when a reasoning level is actually dropped, and on generic reasoning_effort drops * fix: warn when thinking or a Bedrock Converse reasoning level is dropped, and name the router drop a reasoning level * fix: skip the drop warning when a non-reasoning model is asked for no reasoning, and honor declared levels on Bedrock Responses * fix: stay silent when reasoning is turned off on /v1/responses and warn on the Bedrock Responses fallback drop * fix(reasoning): warn when any reasoning-off request is dropped and give each route only the advice it honors A request to turn reasoning off on a model name the catalog lacks now warns on every route, since it may be a reasoning model. Chat suggests model_info.reasoning_effort_levels and, for a name with no catalog entry, model_info.base_model. OpenAI Responses suggests declared levels only where the config honors them, and Bedrock suggests neither. The Bedrock and Mantle Responses forwarding of declared levels is reverted until it can be proven live. * fix(reasoning): suggest reasoning_effort_levels and base_model only where the route reads them The chat gate suggested model_info.reasoning_effort_levels on routes that never pass declared levels (github_copilot, chatgpt and other OpenAIConfig delegates) and model_info.base_model on OpenAI and Azure AI chat, whose gate looks up the deployment name rather than base_model. /v1/responses/compact suggested levels it never reads either. Declared levels are now None when a caller passes none, so the advice appears only when a caller fed them, and the base_model advice is limited to the Azure chat gate, the one that looks up base_model. The openai row of the chat warning test now expects no base_model advice, the Responses warning test goes through get_optional_params_responses_api the way /v1/responses and compact call it, and the Azure direct-config test passes declared levels the way get_optional_params does. Long test lines are wrapped. * fix: read only the built-in catalog when choosing the reasoning drop warning
This commit is contained in:
parent
71babd931b
commit
0cf2bb7296
19 changed files with 609 additions and 61 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue