From 5072590c27aba27212be4d86c2daba03c05ea0cf Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Fri, 24 Jul 2026 19:31:29 -0700 Subject: [PATCH] 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. --- .../adapters/transformation.py | 73 ++++++++----------- ...erimental_pass_through_messages_handler.py | 20 +++++ 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 3d4233e719a..d046ff1eaeb 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -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, diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 8875a75e86f..df3db3d2c57 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -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.