mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(anthropic): accept dict-shape reasoning_effort from Responses bridge (#28201)
* fix(anthropic): accept dict-shape reasoning_effort from Responses bridge Issue #28196 — the Responses->Chat parser (transformation.py:184-200) keeps the full dict as reasoning_effort when summary is set; that branch was added in #25359. But the Anthropic transformation here still guarded on isinstance(value, str), silently dropping the param. Result: callers using the standard Reasoning(effort, summary) OpenAI-shaped object on Anthropic lose thinking entirely (0 reasoning_tokens, no thinking_blocks). Coerce dict -> string before mapping. Same shape tolerance that gpt_5_transformation._normalize_reasoning_effort_for_chat_completion already implements. summary is irrelevant for Anthropic's thinking_blocks. Adds two regression tests: one parametrized over string + dict shapes (with and without summary), one covering unparseable dict inputs (drops silently, no crash). * test(anthropic): add non-adaptive model coverage for dict-shape reasoning_effort Per Greptile feedback on PR #28198: the original regression test only exercised the adaptive (4.6+) path. Add a parametrized test for the non-adaptive branch (claude-sonnet-4-5) verifying that dict-shape reasoning_effort still maps to thinking.type='enabled' + budget_tokens, and that output_config is NOT set on pre-4.6 models. * test(anthropic): convert unparseable-dict test to @pytest.mark.parametrize Per @greptile-apps inline review on PR #28201 — matches the parametrize style of the two adjacent dict-shape tests and produces clearer failure messages (test ID per case instead of one collapsing for-loop).
This commit is contained in:
parent
249ec01f4a
commit
d9af172d27
2 changed files with 130 additions and 4 deletions
|
|
@ -1506,9 +1506,21 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
optional_params["metadata"] = {"user_id": value}
|
||||
elif param == "thinking":
|
||||
optional_params["thinking"] = value
|
||||
elif param == "reasoning_effort" and isinstance(value, str):
|
||||
elif param == "reasoning_effort":
|
||||
# Accept both string ("low") and dict ({"effort": "low",
|
||||
# "summary": "concise"}). The Responses->Chat parser keeps the
|
||||
# full dict when `summary` is set (see #25359), so a dict here
|
||||
# is the standard shape Otto/OpenAI-Responses-Bridge callers
|
||||
# send. Coerce to the effort string before mapping — same
|
||||
# shape-tolerance the GPT-5 path already implements in
|
||||
# `_normalize_reasoning_effort_for_chat_completion`.
|
||||
effort_value = value
|
||||
if isinstance(effort_value, dict):
|
||||
effort_value = effort_value.get("effort")
|
||||
if not isinstance(effort_value, str):
|
||||
continue
|
||||
mapped_thinking = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort=value,
|
||||
reasoning_effort=effort_value,
|
||||
model=model,
|
||||
llm_provider=self.custom_llm_provider or "anthropic",
|
||||
)
|
||||
|
|
@ -1519,12 +1531,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
optional_params["thinking"] = mapped_thinking
|
||||
if AnthropicConfig._is_adaptive_thinking_model(model):
|
||||
mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(
|
||||
value
|
||||
effort_value
|
||||
)
|
||||
if mapped_effort is None:
|
||||
AnthropicConfig._raise_invalid_reasoning_effort(
|
||||
model=model,
|
||||
value=value,
|
||||
value=effort_value,
|
||||
llm_provider=self.custom_llm_provider or "anthropic",
|
||||
)
|
||||
optional_params["output_config"] = {"effort": mapped_effort}
|
||||
|
|
|
|||
|
|
@ -2476,6 +2476,120 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models():
|
|||
), f"output_config should not be set for {model}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reasoning_effort_value",
|
||||
[
|
||||
# String shape — what callers send when using `reasoning_effort="low"` directly.
|
||||
"low",
|
||||
# Dict shape with `effort` only — what the Responses->Chat parser produces
|
||||
# when `reasoning={"effort": "low"}` is set without `summary`.
|
||||
{"effort": "low"},
|
||||
# Dict shape with `effort` AND `summary` — what the Responses->Chat parser
|
||||
# produces when callers send `Reasoning(effort="low", summary="concise")`.
|
||||
# PR #25359 added the dict-keeping branch for this case, but the Anthropic
|
||||
# transformation must coerce the dict back to a string before mapping.
|
||||
{"effort": "low", "summary": "concise"},
|
||||
{"effort": "low", "summary": "detailed"},
|
||||
],
|
||||
)
|
||||
def test_reasoning_effort_accepts_dict_shape_for_adaptive_model(reasoning_effort_value):
|
||||
"""
|
||||
Adaptive-thinking (Claude 4.6+) branch: dict-shape reasoning_effort must
|
||||
map to ``thinking.type='adaptive'`` + ``output_config.effort``.
|
||||
|
||||
Regression test for the dict-shape ``reasoning_effort`` produced by the
|
||||
Responses->Chat parser when ``summary`` is set on the request's
|
||||
``reasoning`` field. Before this fix, the Anthropic transformation guarded
|
||||
on ``isinstance(value, str)`` and silently dropped the param — disabling
|
||||
extended thinking entirely.
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": reasoning_effort_value},
|
||||
optional_params={},
|
||||
model="claude-sonnet-4-6-20260219",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# thinking must be set (adaptive for 4.6+)
|
||||
assert "thinking" in result, (
|
||||
f"thinking missing for reasoning_effort={reasoning_effort_value!r}"
|
||||
)
|
||||
assert result["thinking"]["type"] == "adaptive"
|
||||
# output_config must carry the mapped effort
|
||||
assert "output_config" in result, (
|
||||
f"output_config missing for reasoning_effort={reasoning_effort_value!r}"
|
||||
)
|
||||
assert result["output_config"]["effort"] == "low"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reasoning_effort_value",
|
||||
[
|
||||
"low",
|
||||
{"effort": "low"},
|
||||
{"effort": "low", "summary": "concise"},
|
||||
],
|
||||
)
|
||||
def test_reasoning_effort_accepts_dict_shape_for_non_adaptive_model(reasoning_effort_value):
|
||||
"""
|
||||
Non-adaptive (pre-4.6) branch: dict-shape reasoning_effort must still map
|
||||
to ``thinking.type='enabled'`` + ``budget_tokens``. ``output_config`` must
|
||||
NOT be set on these models.
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": reasoning_effort_value},
|
||||
optional_params={},
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert "thinking" in result, (
|
||||
f"thinking missing for reasoning_effort={reasoning_effort_value!r}"
|
||||
)
|
||||
assert result["thinking"]["type"] == "enabled"
|
||||
assert "budget_tokens" in result["thinking"]
|
||||
assert result["thinking"]["budget_tokens"] > 0
|
||||
# Older models must not get adaptive-thinking output_config
|
||||
assert "output_config" not in result, (
|
||||
f"output_config should not be set for non-adaptive model "
|
||||
f"(reasoning_effort={reasoning_effort_value!r})"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad_value",
|
||||
[
|
||||
{"summary": "concise"}, # missing effort
|
||||
{"effort": None}, # explicit None effort
|
||||
{"effort": 123}, # non-string effort
|
||||
],
|
||||
)
|
||||
def test_reasoning_effort_unparseable_dict_is_dropped(bad_value):
|
||||
"""
|
||||
A dict shape that doesn't carry a usable ``effort`` key (e.g. only
|
||||
``summary`` is set, or the value is some other unexpected type) should be
|
||||
silently dropped — not crash, not partially apply.
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": bad_value},
|
||||
optional_params={},
|
||||
model="claude-sonnet-4-6-20260219",
|
||||
drop_params=False,
|
||||
)
|
||||
assert "thinking" not in result, (
|
||||
f"thinking should not be set for bad value {bad_value!r}"
|
||||
)
|
||||
assert "output_config" not in result, (
|
||||
f"output_config should not be set for bad value {bad_value!r}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue