mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 08:23:17 +00:00
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:
co-authored by
Claude Opus 4.6
parent
c350d08d66
commit
0091d048dc
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+4
-5
@@ -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,
|
||||
|
||||
+101
-72
@@ -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"}
|
||||
}
|
||||
|
||||
+46
-30
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user