fix(anthropic-adapter): dedupe reasoning_effort wrapping to close sibling gap

translate_thinking_for_model duplicated the same summary/auto_summary
wrapping logic as _translate_thinking_to_openai without the
disabled-thinking guard, so it could still wrap "none" into an
{effort, summary} dict when reasoning_auto_summary is enabled (caught
by Cursor Bugbot). Extract the wrapping rule into one shared
_apply_reasoning_summary_wrapping helper used by both call sites so
this invariant can't drift apart again.
This commit is contained in:
Tin Chi Lo 2026-07-24 19:31:29 -07:00
parent fed03a41d1
commit 5072590c27
2 changed files with 52 additions and 41 deletions

View file

@ -684,25 +684,37 @@ class LiteLLMAnthropicMessagesAdapter:
thinking
)
if reasoning_effort:
summary = thinking.get("summary") if isinstance(thinking, dict) else None
auto_summary = is_reasoning_auto_summary_enabled()
if summary:
return {
"reasoning_effort": {
"effort": reasoning_effort,
"summary": summary,
}
}
elif auto_summary:
return {
"reasoning_effort": {
"effort": reasoning_effort,
"summary": "detailed",
}
}
return {"reasoning_effort": reasoning_effort}
return {
"reasoning_effort": LiteLLMAnthropicMessagesAdapter._apply_reasoning_summary_wrapping(
reasoning_effort, thinking
)
}
return {}
@staticmethod
def _apply_reasoning_summary_wrapping(
reasoning_effort: str,
thinking: Dict[str, Any],
) -> Any:
"""
Apply the reasoning_effort/summary wrapping rules shared by every
thinking->reasoning_effort translation path.
Disabled thinking always stays a plain string - there's no reasoning
trace to summarize, and non-Claude providers (e.g. Fireworks) expect
reasoning_effort as a plain string, not a summary dict.
"""
thinking_type = thinking.get("type") if isinstance(thinking, dict) else None
if thinking_type == "disabled":
return reasoning_effort
summary = thinking.get("summary") if isinstance(thinking, dict) else None
if summary:
return cast(Any, {"effort": reasoning_effort, "summary": summary})
if is_reasoning_auto_summary_enabled():
return cast(Any, {"effort": reasoning_effort, "summary": "detailed"})
return reasoning_effort
def translate_anthropic_tool_choice_to_openai(
self, tool_choice: AnthropicMessagesToolChoice
) -> ChatCompletionToolChoiceValues:
@ -997,30 +1009,9 @@ class LiteLLMAnthropicMessagesAdapter:
if isinstance(output_config, dict) and output_config.get("effort"):
reasoning_effort = output_config["effort"]
if thinking_type == "disabled":
new_kwargs["reasoning_effort"] = reasoning_effort
return
summary = thinking.get("summary") if isinstance(thinking, dict) else None
auto_summary = is_reasoning_auto_summary_enabled()
if summary:
new_kwargs["reasoning_effort"] = cast(
Any,
{
"effort": reasoning_effort,
"summary": summary,
},
)
elif auto_summary:
new_kwargs["reasoning_effort"] = cast(
Any,
{
"effort": reasoning_effort,
"summary": "detailed",
},
)
else:
new_kwargs["reasoning_effort"] = reasoning_effort
new_kwargs["reasoning_effort"] = self._apply_reasoning_summary_wrapping(
reasoning_effort, cast(Dict[str, Any], thinking)
)
def _translate_output_format_to_openai(
self,

View file

@ -651,6 +651,26 @@ class TestThinkingSummaryPreservation:
"reasoning_effort": {"effort": "high", "summary": "concise"}
}
def test_translate_thinking_for_model_disabled_stays_plain_string_when_auto_summary_enabled(self):
"""Disabled thinking must stay a plain string even when reasoning_auto_summary is on."""
import litellm
from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
LiteLLMAnthropicMessagesAdapter,
)
original = litellm.reasoning_auto_summary
try:
litellm.reasoning_auto_summary = True
thinking = {"type": "disabled"}
result = LiteLLMAnthropicMessagesAdapter.translate_thinking_for_model(
thinking=thinking,
model="openai/gpt-5.2",
)
finally:
litellm.reasoning_auto_summary = original
assert result == {"reasoning_effort": "none"}
# ---------------------------------------------------------------------------
# Parity tests: redundant empty-text-block sanitization scan removal.