From 0091d048dc03bd11e87f71199e10af5de85c2900 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Sat, 21 Mar 2026 11:36:26 -0700 Subject: [PATCH] fix: make reasoning summary opt-in, fix missing injection path, narrow test exceptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Greptile review feedback: 1. Replace opt-out `disable_default_reasoning_summary` with existing opt-in `reasoning_auto_summary` flag — avoids backwards-incompatible change where all users routing thinking-enabled requests would silently get a changed reasoning_effort shape (string -> dict) on upgrade. 2. Add default summary injection to `_translate_thinking_to_openai` — this path was the only one missing it, causing inconsistent behavior for litellm.completion() callers using the Anthropic adapter. 3. Narrow `except Exception` to `except (ValueError, TypeError, AttributeError)` in tests to avoid masking genuine failures. Co-Authored-By: Claude Opus 4.6 --- docs/my-website/docs/proxy/config_settings.md | 3 +- docs/my-website/docs/reasoning_content.md | 16 +- litellm/__init__.py | 1 - .../adapters/handler.py | 14 +- .../adapters/transformation.py | 27 ++- .../responses_adapters/transformation.py | 9 +- .../experimental_pass_through/utils.py | 9 +- ...odel_prices_and_context_window_backup.json | 8 +- ...erimental_pass_through_messages_handler.py | 173 ++++++++++-------- .../test_responses_adapters_transformation.py | 76 +++++--- 10 files changed, 199 insertions(+), 137 deletions(-) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 22fc0d3e91..6f204b2b0e 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -813,8 +813,7 @@ router_settings: | LITELLM_MODE | Operating mode for LiteLLM (e.g., production, development) | LITELLM_NON_ROOT | Flag to run LiteLLM in non-root mode for enhanced security in Docker containers | LITELLM_RATE_LIMIT_WINDOW_SIZE | Rate limit window size for LiteLLM. Default is 60 -| LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY | If set to "true", disables automatic default reasoning summary injection (`summary: "detailed"`) for Anthropic experimental pass-through translations. Default is "false" -| LITELLM_REASONING_AUTO_SUMMARY | If set to "true", automatically enables detailed reasoning summaries for reasoning models (e.g., o1, o3-mini, deepseek-reasoner). When enabled, adds `summary: "detailed"` to reasoning effort configurations. Default is "false" +| LITELLM_REASONING_AUTO_SUMMARY | If set to "true", automatically enables detailed reasoning summaries (`summary: "detailed"`) for reasoning models across all translation paths (Anthropic adapter, Responses API, etc.). Default is "false" | LITELLM_SALT_KEY | Salt key for encryption in LiteLLM | LITELLM_SSL_CIPHERS | SSL/TLS cipher configuration for faster handshakes. Controls cipher suite preferences for OpenSSL connections. | LITELLM_SECRET_AWS_KMS_LITELLM_LICENSE | AWS KMS encrypted license for LiteLLM diff --git a/docs/my-website/docs/reasoning_content.md b/docs/my-website/docs/reasoning_content.md index f112120366..24694063ef 100644 --- a/docs/my-website/docs/reasoning_content.md +++ b/docs/my-website/docs/reasoning_content.md @@ -700,11 +700,11 @@ response = await litellm.anthropic.messages.acreate( # The summary="concise" is preserved when routing to OpenAI's Responses API ``` -### Default Summary Injection for `/v1/messages` Adapter +### Enabling Default Summary Injection for `/v1/messages` Adapter -When the Anthropic `/v1/messages` adapter translates `thinking` parameters to OpenAI `reasoning_effort` for non-Claude models, `summary="detailed"` is automatically injected by default. This ensures that reasoning text is returned in the response (matching the Anthropic thinking behavior). +When the Anthropic `/v1/messages` adapter translates `thinking` parameters to OpenAI `reasoning_effort` for non-Claude models, you can opt-in to automatic `summary="detailed"` injection using the `reasoning_auto_summary` flag. This ensures that reasoning text is returned in the response (matching the Anthropic thinking behavior). -To **disable** this default injection, use the `disable_default_reasoning_summary` flag: +To **enable** this default injection, use the `reasoning_auto_summary` flag: @@ -712,8 +712,8 @@ To **disable** this default injection, use the `disable_default_reasoning_summar ```python import litellm -# Disable default summary="detailed" injection -litellm.disable_default_reasoning_summary = True +# Enable default summary="detailed" injection +litellm.reasoning_auto_summary = True response = await litellm.anthropic.messages.acreate( model="openai/gpt-5.1", @@ -721,7 +721,7 @@ response = await litellm.anthropic.messages.acreate( max_tokens=8096, thinking={"type": "enabled", "budget_tokens": 5000}, ) -# No summary will be injected — only reasoning_effort is forwarded +# summary="detailed" will be automatically added to reasoning_effort ``` @@ -729,7 +729,7 @@ response = await litellm.anthropic.messages.acreate( ```bash -export LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY=true +export LITELLM_REASONING_AUTO_SUMMARY=true ``` @@ -738,7 +738,7 @@ export LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY=true ```yaml litellm_settings: - disable_default_reasoning_summary: true + reasoning_auto_summary: true ``` diff --git a/litellm/__init__.py b/litellm/__init__.py index a55d49690b..7f72e0b0e8 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -305,7 +305,6 @@ llm_guard_mode: Literal["all", "key-specific", "request-specific"] = "all" guardrail_name_config_map: Dict[str, GuardrailItem] = {} include_cost_in_streaming_usage: bool = False reasoning_auto_summary: bool = False -disable_default_reasoning_summary: bool = False ### PROMPTS #### from litellm.types.prompts.init_prompts import PromptSpec diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py index c45d6b1858..897ca3bf89 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py @@ -12,12 +12,12 @@ from typing import ( ) import litellm -from litellm.llms.anthropic.experimental_pass_through.utils import ( - is_default_reasoning_summary_disabled, -) from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( AnthropicAdapter, ) +from litellm.llms.anthropic.experimental_pass_through.utils import ( + is_reasoning_auto_summary_enabled, +) from litellm.types.llms.anthropic_messages.anthropic_response import ( AnthropicMessagesResponse, ) @@ -82,7 +82,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: # Prefix model with "responses/" to route to OpenAI Responses API completion_kwargs["model"] = f"responses/{model}" - summary_disabled = is_default_reasoning_summary_disabled() + auto_summary = is_reasoning_auto_summary_enabled() reasoning_effort = completion_kwargs.get("reasoning_effort") summary = thinking.get("summary") @@ -90,7 +90,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: reasoning_dict: Dict[str, Any] = {"effort": reasoning_effort} if summary: reasoning_dict["summary"] = summary - elif not summary_disabled: + elif auto_summary: reasoning_dict["summary"] = "detailed" completion_kwargs["reasoning_effort"] = reasoning_dict elif isinstance(reasoning_effort, dict): @@ -98,7 +98,9 @@ class LiteLLMMessagesToCompletionTransformationHandler: "summary" not in reasoning_effort and "generate_summary" not in reasoning_effort ): - effective_summary = summary if summary else ("detailed" if not summary_disabled else None) + effective_summary = ( + summary if summary else ("detailed" if auto_summary else None) + ) if effective_summary: updated_reasoning_effort = dict(reasoning_effort) updated_reasoning_effort["summary"] = effective_summary diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 23c8898622..ed49943b7f 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -15,7 +15,7 @@ from typing import ( ) from litellm.llms.anthropic.experimental_pass_through.utils import ( - is_default_reasoning_summary_disabled, + is_reasoning_auto_summary_enabled, ) # OpenAI has a 64-character limit for function/tool names @@ -741,7 +741,7 @@ class LiteLLMAnthropicMessagesAdapter: summary = ( thinking.get("summary") if isinstance(thinking, dict) else None ) - summary_disabled = is_default_reasoning_summary_disabled() + auto_summary = is_reasoning_auto_summary_enabled() if summary: return { "reasoning_effort": { @@ -749,7 +749,7 @@ class LiteLLMAnthropicMessagesAdapter: "summary": summary, } } - elif not summary_disabled: + elif auto_summary: return { "reasoning_effort": { "effort": reasoning_effort, @@ -891,19 +891,25 @@ class LiteLLMAnthropicMessagesAdapter: # Handle array items if "items" in schema: - LiteLLMAnthropicMessagesAdapter._add_additional_properties_false(schema["items"]) + LiteLLMAnthropicMessagesAdapter._add_additional_properties_false( + schema["items"] + ) # Handle anyOf/oneOf/allOf for key in ("anyOf", "oneOf", "allOf"): if key in schema: for sub_schema in schema[key]: - LiteLLMAnthropicMessagesAdapter._add_additional_properties_false(sub_schema) + LiteLLMAnthropicMessagesAdapter._add_additional_properties_false( + sub_schema + ) # Handle $defs / definitions for key in ("$defs", "definitions"): if key in schema: for def_schema in schema[key].values(): - LiteLLMAnthropicMessagesAdapter._add_additional_properties_false(def_schema) + LiteLLMAnthropicMessagesAdapter._add_additional_properties_false( + def_schema + ) def _add_system_message_to_messages( self, @@ -1030,6 +1036,7 @@ class LiteLLMAnthropicMessagesAdapter: 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, @@ -1038,6 +1045,14 @@ class LiteLLMAnthropicMessagesAdapter: "summary": summary, }, ) + elif auto_summary: + new_kwargs["reasoning_effort"] = cast( + Any, + { + "effort": reasoning_effort, + "summary": "detailed", + }, + ) else: new_kwargs["reasoning_effort"] = reasoning_effort diff --git a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py index 2fef5dee46..dae7044a5b 100644 --- a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py @@ -9,9 +9,8 @@ import json from typing import Any, Dict, List, Optional, Union, cast from litellm.llms.anthropic.experimental_pass_through.utils import ( - is_default_reasoning_summary_disabled, + is_reasoning_auto_summary_enabled, ) - from litellm.types.llms.anthropic import ( AllAnthropicToolsValues, AnthopicMessagesAssistantMessageParam, @@ -98,7 +97,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: ) elif btype == "image": url = self._translate_anthropic_image_source_to_url( - block.get("source", {}) + cast(dict, block.get("source", {})) ) if url: user_parts.append( @@ -271,12 +270,12 @@ class LiteLLMAnthropicToResponsesAPIAdapter: effort = "low" else: effort = "minimal" - summary_disabled = is_default_reasoning_summary_disabled() + auto_summary = is_reasoning_auto_summary_enabled() result: Dict[str, Any] = {"effort": effort} summary = thinking.get("summary") if summary: result["summary"] = summary - elif not summary_disabled: + elif auto_summary: result["summary"] = "detailed" return result diff --git a/litellm/llms/anthropic/experimental_pass_through/utils.py b/litellm/llms/anthropic/experimental_pass_through/utils.py index 4a40e4629e..6c1db6017b 100644 --- a/litellm/llms/anthropic/experimental_pass_through/utils.py +++ b/litellm/llms/anthropic/experimental_pass_through/utils.py @@ -3,10 +3,9 @@ import os import litellm -def is_default_reasoning_summary_disabled() -> bool: - """Check whether the default 'summary: detailed' injection should be suppressed.""" +def is_reasoning_auto_summary_enabled() -> bool: + """Check whether the default 'summary: detailed' injection is enabled (opt-in).""" return ( - litellm.disable_default_reasoning_summary - or os.getenv("LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY", "false").lower() - == "true" + litellm.reasoning_auto_summary + or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true" ) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 879dd42be4..bbf9f6d9dc 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -6152,7 +6152,8 @@ "max_query_tokens": 4096, "max_tokens": 32768, "mode": "rerank", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-cohere-rerank-4-0-in-microsoft-foundry/4477076" }, "azure_ai/cohere-rerank-v4.0-fast": { "input_cost_per_query": 0.002, @@ -6163,7 +6164,8 @@ "max_query_tokens": 4096, "max_tokens": 32768, "mode": "rerank", - "output_cost_per_token": 0.0 + "output_cost_per_token": 0.0, + "source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-cohere-rerank-4-0-in-microsoft-foundry/4477076" }, "azure_ai/deepseek-v3.2": { "input_cost_per_token": 5.8e-07, @@ -6173,6 +6175,7 @@ "max_tokens": 163840, "mode": "chat", "output_cost_per_token": 1.68e-06, + "source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-deepseek-v3-2-and-deepseek-v3-2-speciale-in-microsoft-foundry/4477549", "supports_assistant_prefill": true, "supports_function_calling": true, "supports_prompt_caching": true, @@ -6187,6 +6190,7 @@ "max_tokens": 163840, "mode": "chat", "output_cost_per_token": 1.68e-06, + "source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-deepseek-v3-2-and-deepseek-v3-2-speciale-in-microsoft-foundry/4477549", "supports_assistant_prefill": true, "supports_function_calling": true, "supports_prompt_caching": true, 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 203b6dacea..24080ca120 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 @@ -31,7 +31,7 @@ def test_anthropic_experimental_pass_through_messages_handler(): model="openai/claude-3-5-sonnet-20240620", api_key="test-api-key", ) - except Exception as e: + except (ValueError, TypeError, AttributeError) as e: print(f"Error: {e}") mock_responses.assert_called_once() assert mock_responses.call_args.kwargs["api_key"] == "test-api-key" @@ -56,7 +56,7 @@ def test_anthropic_experimental_pass_through_messages_handler_dynamic_api_key_an api_base="test-api-base", custom_key="custom_value", ) - except Exception as e: + except (ValueError, TypeError, AttributeError) as e: print(f"Error: {e}") mock_completion.assert_called_once() assert mock_completion.call_args.kwargs["api_key"] == "test-api-key" @@ -81,7 +81,7 @@ def test_anthropic_experimental_pass_through_messages_handler_custom_llm_provide custom_llm_provider="my-custom-llm", api_key="test-api-key", ) - except Exception as e: + except (ValueError, TypeError, AttributeError) as e: print(f"Error: {e}") # Assert that litellm.completion was called when using a custom LLM provider @@ -125,24 +125,29 @@ async def test_bedrock_converse_budget_tokens_preserved(): max_tokens=1024, messages=[{"role": "user", "content": "What is 2+2?"}], model="bedrock/converse/us.anthropic.claude-sonnet-4-20250514-v1:0", - thinking={ - "budget_tokens": 1024, - "type": "enabled" - }, + thinking={"budget_tokens": 1024, "type": "enabled"}, ) - except Exception: + except (ValueError, TypeError, AttributeError): pass # Expected due to response format conversion mock_acompletion.assert_called_once() call_kwargs = mock_acompletion.call_args.kwargs - print("acompletion call kwargs: ", json.dumps(call_kwargs, indent=4, default=str)) + print( + "acompletion call kwargs: ", json.dumps(call_kwargs, indent=4, default=str) + ) # Verify thinking parameter is passed through with budget_tokens preserved thinking_param = call_kwargs.get("thinking") - assert thinking_param is not None, "thinking parameter should be passed to acompletion" - assert thinking_param.get("type") == "enabled", "thinking.type should be 'enabled'" - assert thinking_param.get("budget_tokens") == 1024, f"thinking.budget_tokens should be 1024, but got {thinking_param.get('budget_tokens')}" + assert ( + thinking_param is not None + ), "thinking parameter should be passed to acompletion" + assert ( + thinking_param.get("type") == "enabled" + ), "thinking.type should be 'enabled'" + assert ( + thinking_param.get("budget_tokens") == 1024 + ), f"thinking.budget_tokens should be 1024, but got {thinking_param.get('budget_tokens')}" def test_openai_model_with_thinking_converts_to_reasoning(): @@ -164,12 +169,9 @@ def test_openai_model_with_thinking_converts_to_reasoning(): messages=[{"role": "user", "content": "What is 2+2?"}], model="openai/gpt-5.2", api_key="test-api-key", - thinking={ - "type": "enabled", - "budget_tokens": 1024 - }, + thinking={"type": "enabled", "budget_tokens": 1024}, ) - except Exception as e: + except (ValueError, TypeError, AttributeError) as e: print(f"Error: {e}") mock_responses.assert_called_once() @@ -177,18 +179,22 @@ def test_openai_model_with_thinking_converts_to_reasoning(): call_kwargs = mock_responses.call_args.kwargs # Verify reasoning is set (converted from thinking) - assert "reasoning" in call_kwargs, "reasoning should be passed to litellm.responses" + assert ( + "reasoning" in call_kwargs + ), "reasoning should be passed to litellm.responses" # budget_tokens=1024 -> effort="minimal" (< 2000 threshold) - # summary="detailed" added by default unless disable_default_reasoning_summary is set - expected_reasoning = {"effort": "minimal", "summary": "detailed"} + # reasoning_auto_summary is False by default, so no summary key + expected_reasoning = {"effort": "minimal"} assert call_kwargs["reasoning"] == expected_reasoning, ( f"reasoning should be {expected_reasoning} for budget_tokens=1024, " f"got {call_kwargs.get('reasoning')}" ) # Verify thinking is NOT passed directly to the Responses API - assert "thinking" not in call_kwargs, "thinking should NOT be passed directly to litellm.responses" + assert ( + "thinking" not in call_kwargs + ), "thinking should NOT be passed directly to litellm.responses" class TestThinkingParameterTransformation: @@ -199,13 +205,13 @@ class TestThinkingParameterTransformation: 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="bedrock/converse/us.anthropic.claude-sonnet-4-20250514-v1:0", ) - + assert result == {"thinking": thinking} assert result["thinking"]["budget_tokens"] == 5000 @@ -221,27 +227,30 @@ class TestThinkingParameterTransformation: model="openai/gpt-5.2", ) - assert result == {"reasoning_effort": {"effort": "minimal", "summary": "detailed"}} + # reasoning_auto_summary is False by default, so no summary key + assert result == {"reasoning_effort": "minimal"} assert "thinking" not in result - def test_translate_thinking_for_model_no_summary_when_disabled(self): - """When disable_default_reasoning_summary is True, no summary is injected.""" + def test_translate_thinking_for_model_summary_when_enabled(self): + """When reasoning_auto_summary is True, summary='detailed' is injected.""" import litellm from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( LiteLLMAnthropicMessagesAdapter, ) - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = True + litellm.reasoning_auto_summary = True thinking = {"type": "enabled", "budget_tokens": 5000} result = LiteLLMAnthropicMessagesAdapter.translate_thinking_for_model( thinking=thinking, model="openai/gpt-5.2", ) - assert result == {"reasoning_effort": "medium"} + assert result == { + "reasoning_effort": {"effort": "medium", "summary": "detailed"} + } finally: - litellm.disable_default_reasoning_summary = original + litellm.reasoning_auto_summary = original def test_translate_thinking_for_model_preserves_user_summary(self): """User-provided summary is always preserved regardless of flag.""" @@ -258,7 +267,7 @@ class TestThinkingParameterTransformation: class TestThinkingSummaryPreservation: - """Tests for thinking.summary preservation and disable_default_reasoning_summary flag.""" + """Tests for thinking.summary preservation and reasoning_auto_summary flag.""" def test_thinking_summary_concise_preserved_for_openai(self): """User-provided summary='concise' should not be replaced with 'detailed'.""" @@ -271,7 +280,10 @@ class TestThinkingSummaryPreservation: LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed( completion_kwargs, thinking=thinking ) - assert completion_kwargs["reasoning_effort"] == {"effort": "medium", "summary": "concise"} + assert completion_kwargs["reasoning_effort"] == { + "effort": "medium", + "summary": "concise", + } def test_thinking_summary_auto_preserved_for_openai(self): """User-provided summary='auto' should be preserved.""" @@ -284,18 +296,21 @@ class TestThinkingSummaryPreservation: LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed( completion_kwargs, thinking=thinking ) - assert completion_kwargs["reasoning_effort"] == {"effort": "high", "summary": "auto"} + assert completion_kwargs["reasoning_effort"] == { + "effort": "high", + "summary": "auto", + } - def test_summary_added_by_default_when_no_user_summary(self): - """When no user summary and flag is off, summary='detailed' is added by default.""" + def test_summary_added_when_auto_summary_enabled(self): + """When reasoning_auto_summary is True, summary='detailed' is added.""" import litellm from litellm.llms.anthropic.experimental_pass_through.adapters.handler import ( LiteLLMMessagesToCompletionTransformationHandler, ) - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = False + litellm.reasoning_auto_summary = True completion_kwargs = { "model": "responses/gpt-5.2", "custom_llm_provider": "openai", @@ -304,20 +319,23 @@ class TestThinkingSummaryPreservation: LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed( completion_kwargs, thinking={"type": "enabled", "budget_tokens": 5000} ) - assert completion_kwargs["reasoning_effort"] == {"effort": "medium", "summary": "detailed"} + assert completion_kwargs["reasoning_effort"] == { + "effort": "medium", + "summary": "detailed", + } finally: - litellm.disable_default_reasoning_summary = original + litellm.reasoning_auto_summary = original - def test_summary_excluded_when_disable_flag_set_string_reasoning(self): - """When disable_default_reasoning_summary is True, summary is not added for string reasoning_effort.""" + def test_no_summary_by_default_string_reasoning(self): + """By default (reasoning_auto_summary=False), summary is not added for string reasoning_effort.""" import litellm from litellm.llms.anthropic.experimental_pass_through.adapters.handler import ( LiteLLMMessagesToCompletionTransformationHandler, ) - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = True + litellm.reasoning_auto_summary = False completion_kwargs = { "model": "responses/gpt-5.2", "custom_llm_provider": "openai", @@ -329,18 +347,18 @@ class TestThinkingSummaryPreservation: assert completion_kwargs["reasoning_effort"] == {"effort": "high"} assert "summary" not in completion_kwargs["reasoning_effort"] finally: - litellm.disable_default_reasoning_summary = original + litellm.reasoning_auto_summary = original - def test_summary_excluded_when_disable_flag_set_dict_reasoning(self): - """When disable_default_reasoning_summary is True, summary is not injected into dict reasoning_effort.""" + def test_no_summary_by_default_dict_reasoning(self): + """By default (reasoning_auto_summary=False), summary is not injected into dict reasoning_effort.""" import litellm from litellm.llms.anthropic.experimental_pass_through.adapters.handler import ( LiteLLMMessagesToCompletionTransformationHandler, ) - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = True + litellm.reasoning_auto_summary = False completion_kwargs = { "model": "responses/gpt-5.2", "custom_llm_provider": "openai", @@ -352,19 +370,19 @@ class TestThinkingSummaryPreservation: assert completion_kwargs["reasoning_effort"] == {"effort": "medium"} assert "summary" not in completion_kwargs["reasoning_effort"] finally: - litellm.disable_default_reasoning_summary = original + litellm.reasoning_auto_summary = original - def test_summary_excluded_when_env_var_set(self): - """When LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY env var is true, summary is not added.""" + def test_summary_added_when_env_var_set(self): + """When LITELLM_REASONING_AUTO_SUMMARY env var is true, summary is added.""" import litellm from litellm.llms.anthropic.experimental_pass_through.adapters.handler import ( LiteLLMMessagesToCompletionTransformationHandler, ) - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = False - os.environ["LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY"] = "true" + litellm.reasoning_auto_summary = False + os.environ["LITELLM_REASONING_AUTO_SUMMARY"] = "true" completion_kwargs = { "model": "responses/gpt-5.2", "custom_llm_provider": "openai", @@ -373,11 +391,13 @@ class TestThinkingSummaryPreservation: LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed( completion_kwargs, thinking={"type": "enabled", "budget_tokens": 10000} ) - assert completion_kwargs["reasoning_effort"] == {"effort": "high"} - assert "summary" not in completion_kwargs["reasoning_effort"] + assert completion_kwargs["reasoning_effort"] == { + "effort": "high", + "summary": "detailed", + } finally: - litellm.disable_default_reasoning_summary = original - os.environ.pop("LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY", None) + litellm.reasoning_auto_summary = original + os.environ.pop("LITELLM_REASONING_AUTO_SUMMARY", None) def test_user_provided_summary_preserved_even_when_flag_off(self): """When user already set summary in dict reasoning_effort, it's preserved regardless of flag.""" @@ -386,9 +406,9 @@ class TestThinkingSummaryPreservation: LiteLLMMessagesToCompletionTransformationHandler, ) - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = False + litellm.reasoning_auto_summary = False completion_kwargs = { "model": "responses/gpt-5.2", "custom_llm_provider": "openai", @@ -399,7 +419,7 @@ class TestThinkingSummaryPreservation: ) assert completion_kwargs["reasoning_effort"]["summary"] == "concise" finally: - litellm.disable_default_reasoning_summary = original + litellm.reasoning_auto_summary = original def test_openai_model_with_thinking_summary_end_to_end(self): """End-to-end: anthropic_messages_handler should preserve thinking.summary for OpenAI models.""" @@ -420,14 +440,15 @@ class TestThinkingSummaryPreservation: "summary": "concise", }, ) - except Exception: + except (ValueError, TypeError, AttributeError): pass mock_responses.assert_called_once() call_kwargs = mock_responses.call_args.kwargs reasoning = call_kwargs["reasoning"] - assert reasoning["summary"] == "concise", \ - f"Expected summary='concise', got summary='{reasoning.get('summary')}'" + assert ( + reasoning["summary"] == "concise" + ), f"Expected summary='concise', got summary='{reasoning.get('summary')}'" def test_responses_adapter_preserves_summary(self): """translate_thinking_to_reasoning should include summary when user provides it.""" @@ -436,25 +457,31 @@ class TestThinkingSummaryPreservation: ) thinking = {"type": "enabled", "budget_tokens": 5000, "summary": "concise"} - result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking) + result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning( + thinking + ) assert result == {"effort": "medium", "summary": "concise"} - def test_responses_adapter_no_summary_when_disabled(self): - """translate_thinking_to_reasoning should not include summary when flag is set and no user summary.""" + def test_responses_adapter_no_summary_by_default(self): + """translate_thinking_to_reasoning should not include summary by default (opt-in).""" import litellm from litellm.llms.anthropic.experimental_pass_through.responses_adapters.transformation import ( LiteLLMAnthropicToResponsesAPIAdapter, ) - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = True + litellm.reasoning_auto_summary = False thinking = {"type": "enabled", "budget_tokens": 5000} - result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking) + result = ( + LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning( + thinking + ) + ) assert result == {"effort": "medium"} assert "summary" not in result finally: - litellm.disable_default_reasoning_summary = original + litellm.reasoning_auto_summary = original def test_translate_thinking_for_model_preserves_summary(self): """translate_thinking_for_model should include summary in reasoning_effort dict when user provides it.""" @@ -467,4 +494,6 @@ class TestThinkingSummaryPreservation: thinking=thinking, model="openai/gpt-5.2", ) - assert result == {"reasoning_effort": {"effort": "medium", "summary": "concise"}} + assert result == { + "reasoning_effort": {"effort": "medium", "summary": "concise"} + } diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py index 7f77394552..6224652d9d 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py @@ -170,6 +170,7 @@ class TestOutputConfigStructuredOutput: # translate_messages_to_responses_input # --------------------------------------------------------------------------- + # Helper: cast plain dicts to the expected type so call sites stay clean. def _translate_messages(messages: List[Any]) -> List[Dict[str, Any]]: return _ADAPTER.translate_messages_to_responses_input(messages) # type: ignore[arg-type] @@ -274,7 +275,11 @@ class TestTranslateMessagesToResponsesInput: "content": [ { "type": "image", - "source": {"type": "base64", "media_type": "image/jpeg", "data": ""}, + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "", + }, } ], } @@ -462,7 +467,10 @@ class TestTranslateMessagesToResponsesInput: ] result = _translate_messages(messages) assert len(result) == 1 - assert result[0]["content"][0] == {"type": "input_text", "text": "Describe this image:"} + assert result[0]["content"][0] == { + "type": "input_text", + "text": "Describe this image:", + } assert result[0]["content"][1] == { "type": "input_image", "image_url": "https://example.com/cat.jpg", @@ -606,7 +614,7 @@ class TestTranslateThinkingToReasoning: result = _ADAPTER.translate_thinking_to_reasoning( {"type": "enabled", "budget_tokens": 10000} ) - assert result == {"effort": "high", "summary": "detailed"} + assert result == {"effort": "high"} def test_budget_above_threshold_high_effort(self): result = _ADAPTER.translate_thinking_to_reasoning( @@ -619,19 +627,19 @@ class TestTranslateThinkingToReasoning: result = _ADAPTER.translate_thinking_to_reasoning( {"type": "enabled", "budget_tokens": 7500} ) - assert result == {"effort": "medium", "summary": "detailed"} + assert result == {"effort": "medium"} def test_budget_low_effort(self): result = _ADAPTER.translate_thinking_to_reasoning( {"type": "enabled", "budget_tokens": 3000} ) - assert result == {"effort": "low", "summary": "detailed"} + assert result == {"effort": "low"} def test_budget_minimal_effort(self): result = _ADAPTER.translate_thinking_to_reasoning( {"type": "enabled", "budget_tokens": 500} ) - assert result == {"effort": "minimal", "summary": "detailed"} + assert result == {"effort": "minimal"} def test_budget_at_exact_thresholds(self): result_medium = _ADAPTER.translate_thinking_to_reasoning( @@ -656,39 +664,37 @@ class TestTranslateThinkingToReasoning: def test_missing_budget_defaults_to_minimal(self): """Missing budget_tokens defaults to 0, which is < 2000 -> minimal.""" result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled"}) - assert result == {"effort": "minimal", "summary": "detailed"} + assert result == {"effort": "minimal"} - def test_summary_excluded_when_disable_flag_set(self): - """When disable_default_reasoning_summary is True, summary is not included.""" + def test_summary_added_when_auto_summary_enabled(self): + """When reasoning_auto_summary is True, summary='detailed' is included.""" import litellm - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = True + litellm.reasoning_auto_summary = True result = _ADAPTER.translate_thinking_to_reasoning( {"type": "enabled", "budget_tokens": 10000} ) - assert result == {"effort": "high"} - assert "summary" not in result + assert result == {"effort": "high", "summary": "detailed"} finally: - litellm.disable_default_reasoning_summary = original + litellm.reasoning_auto_summary = original - def test_summary_excluded_when_env_var_set(self): - """When LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY env var is true, summary is not included.""" + def test_summary_added_when_env_var_set(self): + """When LITELLM_REASONING_AUTO_SUMMARY env var is true, summary is included.""" import litellm - original = litellm.disable_default_reasoning_summary + original = litellm.reasoning_auto_summary try: - litellm.disable_default_reasoning_summary = False - os.environ["LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY"] = "true" + litellm.reasoning_auto_summary = False + os.environ["LITELLM_REASONING_AUTO_SUMMARY"] = "true" result = _ADAPTER.translate_thinking_to_reasoning( {"type": "enabled", "budget_tokens": 5000} ) - assert result == {"effort": "medium"} - assert "summary" not in result + assert result == {"effort": "medium", "summary": "detailed"} finally: - litellm.disable_default_reasoning_summary = original - os.environ.pop("LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY", None) + litellm.reasoning_auto_summary = original + os.environ.pop("LITELLM_REASONING_AUTO_SUMMARY", None) # --------------------------------------------------------------------------- @@ -747,7 +753,9 @@ class TestTranslateRequestBroaderCoverage: def test_tools_translated(self): req = _make_request( - tools=[{"name": "calculator", "description": "Does math.", "input_schema": {}}] + tools=[ + {"name": "calculator", "description": "Does math.", "input_schema": {}} + ] ) kwargs = _ADAPTER.translate_request(req) assert len(kwargs["tools"]) == 1 @@ -764,7 +772,8 @@ class TestTranslateRequestBroaderCoverage: def test_thinking_translated_to_reasoning(self): req = _make_request(thinking={"type": "enabled", "budget_tokens": 12000}) kwargs = _ADAPTER.translate_request(req) - assert kwargs["reasoning"] == {"effort": "high", "summary": "detailed"} + # reasoning_auto_summary is False by default, so no summary key + assert kwargs["reasoning"] == {"effort": "high"} def test_disabled_thinking_not_included_in_kwargs(self): req = _make_request(thinking={"type": "disabled"}) @@ -785,8 +794,17 @@ class TestTranslateRequestBroaderCoverage: def test_no_optional_fields_does_not_add_spurious_keys(self): req = _make_request() kwargs = _ADAPTER.translate_request(req) - for key in ("instructions", "temperature", "top_p", "tools", "tool_choice", - "reasoning", "text", "context_management", "user"): + for key in ( + "instructions", + "temperature", + "top_p", + "tools", + "tool_choice", + "reasoning", + "text", + "context_management", + "user", + ): assert key not in kwargs, f"unexpected key: {key}" @@ -833,9 +851,7 @@ def _make_output_message(texts: List[str]) -> MagicMock: return msg -def _make_function_call_item( - call_id: str, name: str, arguments: str -) -> MagicMock: +def _make_function_call_item(call_id: str, name: str, arguments: str) -> MagicMock: """Build a mock ResponseFunctionToolCall.""" from openai.types.responses import ResponseFunctionToolCall # type: ignore[import]