From ece032523498929d7bde4f52368b18ec247a8750 Mon Sep 17 00:00:00 2001 From: Chesars Date: Wed, 4 Mar 2026 10:45:28 -0300 Subject: [PATCH] fix(anthropic): make thinking.summary opt-in, don't hardcode default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove hardcoded summary="detailed" injection — summary is opt-in per OpenAI spec and increases costs. Users opt-in per-request via LiteLLM extension: thinking={"type": "enabled", "budget_tokens": N, "summary": "concise"}. Also preserve summary in translate_thinking_for_model() which previously dropped it when converting thinking → reasoning_effort for non-Claude models. Fixes #20998 --- .../adapters/handler.py | 20 +++++----- .../adapters/transformation.py | 3 ++ ...erimental_pass_through_messages_handler.py | 37 ++++++++++++++++--- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py index c0b77798f2e..01c8f39ee80 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py @@ -44,8 +44,9 @@ class LiteLLMMessagesToCompletionTransformationHandler: For OpenAI models, Chat Completions typically does not return reasoning text (only token accounting). To return a thinking-like content block in the - Anthropic response format, we route the request through OpenAI's Responses API - and request a reasoning summary. + Anthropic response format, we route the request through OpenAI's Responses API. + If the user provides a `summary` field in the thinking dict, it is passed + through to the OpenAI reasoning params (opt-in per OpenAI spec). """ custom_llm_provider = completion_kwargs.get("custom_llm_provider") if custom_llm_provider is None: @@ -77,19 +78,20 @@ class LiteLLMMessagesToCompletionTransformationHandler: completion_kwargs["model"] = f"responses/{model}" reasoning_effort = completion_kwargs.get("reasoning_effort") + summary = thinking.get("summary") if isinstance(thinking, dict) else None if isinstance(reasoning_effort, str) and reasoning_effort: - summary = thinking.get("summary", "detailed") if isinstance(thinking, dict) else "detailed" - completion_kwargs["reasoning_effort"] = { - "effort": reasoning_effort, - "summary": summary, - } + reasoning_dict: Dict[str, Any] = {"effort": reasoning_effort} + if summary: + reasoning_dict["summary"] = summary + completion_kwargs["reasoning_effort"] = reasoning_dict elif isinstance(reasoning_effort, dict): if ( - "summary" not in reasoning_effort + summary + and "summary" not in reasoning_effort and "generate_summary" not in reasoning_effort ): updated_reasoning_effort = dict(reasoning_effort) - updated_reasoning_effort["summary"] = "detailed" + updated_reasoning_effort["summary"] = summary completion_kwargs["reasoning_effort"] = updated_reasoning_effort @staticmethod diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index efbac13735c..2af5e35112c 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -673,6 +673,9 @@ class LiteLLMAnthropicMessagesAdapter: thinking ) if reasoning_effort: + summary = thinking.get("summary") if isinstance(thinking, dict) else None + if summary: + return {"reasoning_effort": {"effort": reasoning_effort, "summary": summary}} return {"reasoning_effort": reasoning_effort} return {} 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 e2639a31263..c6541ccac43 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 @@ -177,8 +177,8 @@ def test_openai_model_with_thinking_converts_to_reasoning_effort(): # Verify reasoning_effort is set (converted from thinking) assert "reasoning_effort" in call_kwargs, "reasoning_effort should be passed to completion" - # reasoning_effort is transformed into a dict with effort and summary fields - expected_reasoning_effort = {"effort": "minimal", "summary": "detailed"} + # reasoning_effort is a dict with effort only (summary is opt-in per OpenAI spec) + expected_reasoning_effort = {"effort": "minimal"} assert call_kwargs["reasoning_effort"] == expected_reasoning_effort, \ f"reasoning_effort should be {expected_reasoning_effort} for budget_tokens=1024, got {call_kwargs.get('reasoning_effort')}" @@ -249,8 +249,8 @@ class TestThinkingSummaryPreservation: ) assert completion_kwargs["reasoning_effort"] == {"effort": "high", "summary": "auto"} - def test_thinking_without_summary_defaults_to_detailed(self): - """When no summary is provided, default 'detailed' should still be used.""" + def test_thinking_without_summary_does_not_inject_summary(self): + """When no summary is provided, no summary should be injected (opt-in per OpenAI spec).""" from litellm.llms.anthropic.experimental_pass_through.adapters.handler import ( LiteLLMMessagesToCompletionTransformationHandler, ) @@ -260,7 +260,8 @@ class TestThinkingSummaryPreservation: LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed( completion_kwargs, thinking=thinking ) - assert completion_kwargs["reasoning_effort"] == {"effort": "medium", "summary": "detailed"} + assert completion_kwargs["reasoning_effort"] == {"effort": "medium"} + assert "summary" not in completion_kwargs["reasoning_effort"] def test_openai_model_with_thinking_summary_end_to_end(self): """End-to-end: anthropic_messages_handler should preserve thinking.summary for OpenAI models.""" @@ -289,3 +290,29 @@ class TestThinkingSummaryPreservation: reasoning_effort = call_kwargs["reasoning_effort"] assert reasoning_effort["summary"] == "concise", \ f"Expected summary='concise', got summary='{reasoning_effort.get('summary')}'" + + def test_translate_thinking_for_model_preserves_summary(self): + """translate_thinking_for_model should include summary in reasoning_effort dict when user provides it.""" + from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( + LiteLLMAnthropicMessagesAdapter, + ) + + thinking = {"type": "enabled", "budget_tokens": 5000, "summary": "concise"} + result = LiteLLMAnthropicMessagesAdapter.translate_thinking_for_model( + thinking=thinking, + model="openai/gpt-5.2", + ) + assert result == {"reasoning_effort": {"effort": "medium", "summary": "concise"}} + + def test_translate_thinking_for_model_no_summary_when_not_provided(self): + """translate_thinking_for_model should return plain string reasoning_effort when no summary provided.""" + from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( + LiteLLMAnthropicMessagesAdapter, + ) + + thinking = {"type": "enabled", "budget_tokens": 5000} + result = LiteLLMAnthropicMessagesAdapter.translate_thinking_for_model( + thinking=thinking, + model="openai/gpt-5.2", + ) + assert result == {"reasoning_effort": "medium"}