Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3759,6 +3759,61 @@ def pre_process_optional_params(passed_params: dict, non_default_params: dict, c
return optional_params


# Warn-once bookkeeping for `drop_params`. Keyed by
# (provider, model, sorted dropped param names) so a route that drops the same
# params on every request warns once, not once per call. Bounded so a
# long-lived proxy serving many models cannot grow it without limit; past the
# cap we stop recording (and therefore may repeat a warning), which is the safe
# direction to fail.
_MAX_DROPPED_PARAM_WARNINGS = 1000
_DROPPED_PARAM_WARNINGS: set[tuple[str, str, tuple[str, ...]]] = set()


def _warn_dropped_params(
unsupported_params: dict,
model: str | None,
custom_llm_provider: str | None,
) -> None:
"""Log once when `drop_params` discards caller-specified parameters.

`drop_params` exists so an unsupported parameter does not fail the whole
request, and that tradeoff is right. But dropping a parameter changes
generation behaviour, and today it happens with no signal at all: a
`reasoning_effort`, `temperature` or penalty set in a proxy config simply
never reaches the provider, and nothing in the logs or the response says
so. The config and the wire disagree, silently and indefinitely.

This is easiest to hit on a provider whose supported-param set is derived
from the model-cost map: a model absent from the map is treated as
supporting nothing beyond the base set, so the gate fails closed for any
slug newer than the map — which is a routine state, not an exotic one.

Warns rather than debugs because the user asked for something and did not
get it; deduped so a per-request drop does not flood the log.
"""
if not unsupported_params:
return
dropped = tuple(sorted(unsupported_params.keys()))
key = (custom_llm_provider or "", model or "", dropped)
if key in _DROPPED_PARAM_WARNINGS:
return
if len(_DROPPED_PARAM_WARNINGS) < _MAX_DROPPED_PARAM_WARNINGS:
_DROPPED_PARAM_WARNINGS.add(key)
verbose_logger.warning(
"litellm.drop_params: dropping unsupported params %s for model=%s, "
"provider=%s. They will NOT reach the provider, so whatever behaviour "
"they were meant to control is unchanged. To send them anyway, pass "
"allowed_openai_params=%s in the request, or on the proxy add "
"`allowed_openai_params: %s` to that model's litellm_params in "
"config.yaml.",
list(dropped),
model,
custom_llm_provider,
list(dropped),
list(dropped),
)


def get_optional_params(
# use the openai defaults
# https://platform.openai.com/docs/api-reference/chat/create
Expand Down Expand Up @@ -3857,6 +3912,11 @@ def _check_valid_arg(supported_params: List[str]):

if unsupported_params:
if litellm.drop_params is True or (drop_params is not None and drop_params is True):
_warn_dropped_params(
unsupported_params=unsupported_params,
model=model,
custom_llm_provider=custom_llm_provider,
)
for k in unsupported_params.keys():
non_default_params.pop(k, None)
else:
Expand Down
94 changes: 94 additions & 0 deletions tests/test_litellm/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4863,3 +4863,97 @@ def test_is_prompt_caching_valid_prompt_explicit_min_token_count_overrides_model
is_prompt_caching_valid_prompt(model="claude-opus-4-8", messages=PROMPT_CACHE_MESSAGES, min_token_count=8192)
is False
)


class TestDropParamsVisibility:
"""`drop_params` must not discard caller-specified params in silence.

Dropping a param changes generation behaviour, so a `reasoning_effort` or
`temperature` set in a proxy config that never reaches the provider is a
real behavioural difference with no signal attached. The warning is
deduped because the same params are dropped on every request for a given
route.
"""

def setup_method(self):
litellm.utils._DROPPED_PARAM_WARNINGS.clear()

def test_warns_when_an_unsupported_param_is_dropped(self):
# openrouter advertises reasoning_effort only for models flagged
# supports_reasoning in the model-cost map; an absent slug is not.
with patch.object(litellm.utils.verbose_logger, "warning") as mock_warn:
result = litellm.utils.get_optional_params(
model="qwen/qwen3-max",
custom_llm_provider="openrouter",
reasoning_effort="high",
drop_params=True,
)
assert "reasoning_effort" not in result
mock_warn.assert_called_once()
assert "reasoning_effort" in str(mock_warn.call_args)

def test_repeat_calls_warn_once(self):
with patch.object(litellm.utils.verbose_logger, "warning") as mock_warn:
for _ in range(5):
litellm.utils.get_optional_params(
model="qwen/qwen3-max",
custom_llm_provider="openrouter",
reasoning_effort="high",
drop_params=True,
)
assert mock_warn.call_count == 1

def test_a_different_model_warns_separately(self):
with patch.object(litellm.utils.verbose_logger, "warning") as mock_warn:
for model in ("qwen/qwen3-max", "vendor/some-other-slug"):
litellm.utils.get_optional_params(
model=model,
custom_llm_provider="openrouter",
reasoning_effort="high",
drop_params=True,
)
assert mock_warn.call_count == 2

def test_supported_params_do_not_warn(self):
with patch.object(litellm.utils.verbose_logger, "warning") as mock_warn:
result = litellm.utils.get_optional_params(
model="qwen/qwen3-max",
custom_llm_provider="openrouter",
temperature=0.3,
top_p=0.9,
drop_params=True,
)
assert result["temperature"] == 0.3
mock_warn.assert_not_called()

def test_warning_names_both_remedies(self):
# Most people hit this on the proxy, reading a config.yaml rather than
# writing request kwargs, so a message naming only the per-request form
# reads as "not applicable to me". `allowed_openai_params` is settable
# in a model_list entry's litellm_params (LiteLLM_Params is
# ConfigDict(extra="allow")) and does reach get_optional_params from
# there, so both routes are worth naming.
with patch.object(litellm.utils.verbose_logger, "warning") as mock_warn:
litellm.utils.get_optional_params(
model="qwen/qwen3-max",
custom_llm_provider="openrouter",
reasoning_effort="high",
drop_params=True,
)
message = mock_warn.call_args[0][0]
assert "allowed_openai_params" in message
assert "litellm_params" in message
assert "config.yaml" in message

def test_no_warning_when_drop_params_is_off(self):
# Without drop_params the caller gets a loud exception instead; the
# failure is already visible, so no warning is needed.
with patch.object(litellm.utils.verbose_logger, "warning") as mock_warn:
with pytest.raises(litellm.utils.UnsupportedParamsError):
litellm.utils.get_optional_params(
model="qwen/qwen3-max",
custom_llm_provider="openrouter",
reasoning_effort="high",
drop_params=False,
)
mock_warn.assert_not_called()
Loading