fix: make reasoning summary opt-in, fix missing injection path, narrow test exceptions

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 <noreply@anthropic.com>
This commit is contained in:
Krrish Dholakia
2026-03-21 11:36:26 -07:00
co-authored by Claude Opus 4.6
parent c350d08d66
commit 0091d048dc
10 changed files with 199 additions and 137 deletions
@@ -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
+8 -8
View File
@@ -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:
<Tabs>
<TabItem value="sdk" label="SDK">
@@ -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
```
</TabItem>
@@ -729,7 +729,7 @@ response = await litellm.anthropic.messages.acreate(
<TabItem value="env" label="Environment Variable">
```bash
export LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY=true
export LITELLM_REASONING_AUTO_SUMMARY=true
```
</TabItem>
@@ -738,7 +738,7 @@ export LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY=true
```yaml
litellm_settings:
disable_default_reasoning_summary: true
reasoning_auto_summary: true
```
</TabItem>
-1
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
)
@@ -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,
@@ -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"}
}
@@ -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]