mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
fix(reasoning): warn when Claude drops a non-string reasoning_effort
Claude chat and native /v1/messages dropped a reasoning_effort like 5 or
{"effort": 5} with no log line. Both now log one warn-only WARNING. A
Magistral thinking.type sent as a list no longer raises TypeError.
This commit is contained in:
parent
6043cb9c4f
commit
5bbb992569
8 changed files with 134 additions and 2 deletions
|
|
@ -20,6 +20,9 @@ OpenAIStyleReasoningEffort = Literal["minimal", "low", "medium", "high"]
|
|||
_DROP_WARNING: Final = "Dropping %s=%s for model=%r because %s"
|
||||
_DROP_PARAMS_DROP_WARNING: Final = "Dropping %s=%s for model=%r (drop_params=True) because %s"
|
||||
_NON_NUMERIC_BUDGET_REASON: Final = "it is not a number. Send budget_tokens as an integer to set a reasoning level."
|
||||
_NON_STRING_EFFORT_REASON: Final = (
|
||||
"it is not a string. Send reasoning_effort as a level name such as 'high' to set a reasoning level."
|
||||
)
|
||||
_REASONING_PARAMS: Final = ("reasoning", "reasoning_effort", "thinking")
|
||||
OPAQUE_VALUE: Final = TypeAdapter(object)
|
||||
|
||||
|
|
@ -32,6 +35,14 @@ def warn_dropped_reasoning(
|
|||
)
|
||||
|
||||
|
||||
def warn_non_string_reasoning_effort(value: object, model: str | None) -> None:
|
||||
effort: Final = as_str_mapping(value)
|
||||
requested_level: Final = effort.get("effort") if effort is not None else value
|
||||
if requested_level is None or isinstance(value, str):
|
||||
return
|
||||
warn_dropped_reasoning("reasoning_effort", value, model, _NON_STRING_EFFORT_REASON, drop_params_gated=False)
|
||||
|
||||
|
||||
def warn_dropped_reasoning_params(
|
||||
dropped: Collection[object],
|
||||
request: Mapping[str, object],
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
|
|||
place_mid_conversation_system,
|
||||
split_leading_system_run,
|
||||
)
|
||||
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_non_string_reasoning_effort
|
||||
from litellm.llms.base_llm.base_utils import (
|
||||
ResponseFormatSchema,
|
||||
as_json_object,
|
||||
|
|
@ -1951,6 +1952,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
if isinstance(effort_value, dict):
|
||||
effort_value = effort_value.get("effort")
|
||||
if not isinstance(effort_value, str):
|
||||
warn_non_string_reasoning_effort(OPAQUE_VALUE.validate_python(value), model)
|
||||
continue
|
||||
mapped_thinking = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort=effort_value,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from litellm.constants import ANTHROPIC_BILLING_METADATA_PREFIX
|
|||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_non_string_reasoning_effort
|
||||
from litellm.litellm_core_utils.url_utils import ensure_api_route
|
||||
from litellm.llms.anthropic.messages.convertible_blocks import drop_non_string_tool_result_texts
|
||||
from litellm.llms.anthropic.messages.safeguards import proxy_serves_safeguards
|
||||
|
|
@ -695,12 +696,17 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
):
|
||||
anthropic_messages_optional_request_params.pop("output_format") # pyright: ignore[reportUnknownMemberType] # the request params arrive as a bare dict
|
||||
|
||||
requested_reasoning_effort: Final = OPAQUE_VALUE.validate_python(
|
||||
anthropic_messages_optional_request_params.get("reasoning_effort") # pyright: ignore[reportUnknownMemberType] # the request params arrive as a bare dict
|
||||
)
|
||||
self._translate_reasoning_effort_to_anthropic(
|
||||
model=model,
|
||||
optional_params=anthropic_messages_optional_request_params,
|
||||
max_tokens=max_tokens,
|
||||
custom_llm_provider=self._resolved_provider,
|
||||
)
|
||||
if self._report_reasoning_drops:
|
||||
warn_non_string_reasoning_effort(requested_reasoning_effort, model)
|
||||
|
||||
AnthropicModelInfo.maybe_drop_disabled_thinking(
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ def _warn_unless_reasoning_prompt_honors(param: str, value: object, model: str)
|
|||
if param == "reasoning_effort" and isinstance(value, str) and value in _DOCUMENTED_MAGISTRAL_EFFORTS:
|
||||
return
|
||||
thinking: Final = as_str_mapping(value)
|
||||
if param == "thinking" and thinking is not None and thinking.get("type") in _REASONING_ON_THINKING_TYPES:
|
||||
thinking_type: Final = thinking.get("type") if thinking is not None else None
|
||||
if param == "thinking" and isinstance(thinking_type, str) and thinking_type in _REASONING_ON_THINKING_TYPES:
|
||||
return
|
||||
warn_dropped_reasoning(param, value, model, _REASONING_PROMPT_REASON, drop_params_gated=False)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import copy
|
||||
import json
|
||||
import logging
|
||||
from itertools import chain
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
|
@ -3189,6 +3190,47 @@ def test_reasoning_effort_accepts_dict_shape_for_non_adaptive_model(
|
|||
)
|
||||
|
||||
|
||||
def _reasoning_effort_drop_warnings(caplog: pytest.LogCaptureFixture) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
record.getMessage() for record in caplog.records if "Dropping reasoning_effort=" in record.getMessage()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reasoning_effort_value", [5, {"effort": 5, "summary": "concise"}])
|
||||
def test_reasoning_effort_that_is_not_a_level_name_is_dropped_with_one_warning(
|
||||
reasoning_effort_value: object, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
result: Final = AnthropicConfig().map_openai_params(
|
||||
non_default_params={"reasoning_effort": reasoning_effort_value},
|
||||
optional_params={},
|
||||
model="claude-sonnet-4-6-20260219",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
warnings: Final = _reasoning_effort_drop_warnings(caplog)
|
||||
assert "thinking" not in result
|
||||
assert "output_config" not in result
|
||||
assert len(warnings) == 1
|
||||
assert f"reasoning_effort={reasoning_effort_value!r}" in warnings[0]
|
||||
assert "claude-sonnet-4-6-20260219" in warnings[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reasoning_effort_value", [{"summary": "concise"}, {"effort": "low", "summary": "concise"}])
|
||||
def test_reasoning_effort_with_a_level_name_or_no_level_logs_no_drop(
|
||||
reasoning_effort_value: object, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
AnthropicConfig().map_openai_params(
|
||||
non_default_params={"reasoning_effort": reasoning_effort_value},
|
||||
optional_params={},
|
||||
model="claude-sonnet-4-6-20260219",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert _reasoning_effort_drop_warnings(caplog) == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,budget_tokens,expected",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
"""Tests for ``reasoning_effort`` translation on the Anthropic /v1/messages route."""
|
||||
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.constants import (
|
||||
|
|
@ -46,6 +49,49 @@ def test_reasoning_effort_maps_to_output_config_for_adaptive_model(
|
|||
assert result.get("output_config") == {"effort": expected_effort}
|
||||
|
||||
|
||||
def _reasoning_effort_drop_warnings(caplog: pytest.LogCaptureFixture) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
record.getMessage() for record in caplog.records if "Dropping reasoning_effort=" in record.getMessage()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reasoning_effort", [5, {"effort": "high"}])
|
||||
def test_reasoning_effort_that_is_not_a_string_is_dropped_with_one_warning(
|
||||
reasoning_effort: object, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
result: Final = AnthropicMessagesConfig().transform_anthropic_messages_request(
|
||||
model="claude-opus-4-7",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
anthropic_messages_optional_request_params={"max_tokens": 1024, "reasoning_effort": reasoning_effort},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
warnings: Final = _reasoning_effort_drop_warnings(caplog)
|
||||
assert "reasoning_effort" not in result
|
||||
assert "output_config" not in result
|
||||
assert len(warnings) == 1
|
||||
assert f"reasoning_effort={reasoning_effort!r}" in warnings[0]
|
||||
assert "claude-opus-4-7" in warnings[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reasoning_effort", ["high", {"summary": "concise"}])
|
||||
def test_reasoning_effort_that_is_a_string_or_names_no_level_logs_no_drop(
|
||||
reasoning_effort: object, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
AnthropicMessagesConfig().transform_anthropic_messages_request(
|
||||
model="claude-opus-4-7",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
anthropic_messages_optional_request_params={"max_tokens": 1024, "reasoning_effort": reasoning_effort},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert _reasoning_effort_drop_warnings(caplog) == ()
|
||||
|
||||
|
||||
def test_reasoning_effort_none_clears_thinking_and_output_config():
|
||||
config = AnthropicMessagesConfig()
|
||||
optional_params = {
|
||||
|
|
|
|||
|
|
@ -618,6 +618,25 @@ def test_native_baseline_body_drops_a_non_numeric_budget_without_logging_a_drop(
|
|||
assert [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()] == []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map")
|
||||
def test_native_baseline_body_drops_a_non_string_reasoning_effort_without_logging_a_drop(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
request: Final = {
|
||||
"model": "claude-opus-5",
|
||||
"max_tokens": 64,
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"reasoning_effort": 5,
|
||||
}
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
body: Final = prepare_native_baseline_body(request, "anthropic/claude-opus-5")
|
||||
|
||||
assert body is not None
|
||||
assert "reasoning_effort" not in body
|
||||
assert [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()] == []
|
||||
|
||||
|
||||
def test_native_baseline_body_matches_the_sent_body_when_drop_params_meets_an_auto_added_beta(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -1677,7 +1677,12 @@ def _warnings_naming(caplog: pytest.LogCaptureFixture, param: str) -> tuple[str,
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
"param, value",
|
||||
(("reasoning_effort", "none"), ("reasoning_effort", "xhigh"), ("thinking", {"type": "disabled"})),
|
||||
(
|
||||
("reasoning_effort", "none"),
|
||||
("reasoning_effort", "xhigh"),
|
||||
("thinking", {"type": "disabled"}),
|
||||
("thinking", {"type": ["enabled"]}),
|
||||
),
|
||||
)
|
||||
def test_magistral_reasoning_request_replaced_by_prompt_warns_once(
|
||||
param: str, value: object, caplog: pytest.LogCaptureFixture
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue