mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
Merge pull request #22904 from Chesars/claude/add-claude-param-default-M4Yic
feat(anthropic): add opt-out flag for default reasoning summary
This commit is contained in:
commit
c9e60d9909
6 changed files with 191 additions and 39 deletions
|
|
@ -293,6 +293,7 @@ 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,6 +12,9 @@ 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,
|
||||
)
|
||||
|
|
@ -77,22 +80,27 @@ 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()
|
||||
|
||||
reasoning_effort = completion_kwargs.get("reasoning_effort")
|
||||
summary = thinking.get("summary")
|
||||
if isinstance(reasoning_effort, str) and reasoning_effort:
|
||||
reasoning_dict: Dict[str, Any] = {"effort": reasoning_effort}
|
||||
if summary:
|
||||
reasoning_dict["summary"] = summary
|
||||
elif not summary_disabled:
|
||||
reasoning_dict["summary"] = "detailed"
|
||||
completion_kwargs["reasoning_effort"] = reasoning_dict
|
||||
elif isinstance(reasoning_effort, dict):
|
||||
if (
|
||||
summary
|
||||
and "summary" not in reasoning_effort
|
||||
"summary" not in reasoning_effort
|
||||
and "generate_summary" not in reasoning_effort
|
||||
):
|
||||
updated_reasoning_effort = dict(reasoning_effort)
|
||||
updated_reasoning_effort["summary"] = summary
|
||||
completion_kwargs["reasoning_effort"] = updated_reasoning_effort
|
||||
effective_summary = summary if summary else ("detailed" if not summary_disabled else None)
|
||||
if effective_summary:
|
||||
updated_reasoning_effort = dict(reasoning_effort)
|
||||
updated_reasoning_effort["summary"] = effective_summary
|
||||
completion_kwargs["reasoning_effort"] = updated_reasoning_effort
|
||||
|
||||
@staticmethod
|
||||
def _prepare_completion_kwargs(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ path used for OpenAI and Azure models.
|
|||
import json
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.utils import (
|
||||
is_default_reasoning_summary_disabled,
|
||||
)
|
||||
|
||||
from litellm.types.llms.anthropic import (
|
||||
AllAnthropicToolsValues,
|
||||
AnthopicMessagesAssistantMessageParam,
|
||||
|
|
@ -241,10 +246,13 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
effort = "low"
|
||||
else:
|
||||
effort = "minimal"
|
||||
summary_disabled = is_default_reasoning_summary_disabled()
|
||||
result: Dict[str, Any] = {"effort": effort}
|
||||
summary = thinking.get("summary")
|
||||
if summary:
|
||||
result["summary"] = summary
|
||||
elif not summary_disabled:
|
||||
result["summary"] = "detailed"
|
||||
return result
|
||||
|
||||
def translate_request(
|
||||
|
|
|
|||
12
litellm/llms/anthropic/experimental_pass_through/utils.py
Normal file
12
litellm/llms/anthropic/experimental_pass_through/utils.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import os
|
||||
|
||||
import litellm
|
||||
|
||||
|
||||
def is_default_reasoning_summary_disabled() -> bool:
|
||||
"""Check whether the default 'summary: detailed' injection should be suppressed."""
|
||||
return (
|
||||
litellm.disable_default_reasoning_summary
|
||||
or os.getenv("LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY", "false").lower()
|
||||
== "true"
|
||||
)
|
||||
|
|
@ -180,7 +180,8 @@ def test_openai_model_with_thinking_converts_to_reasoning():
|
|||
assert "reasoning" in call_kwargs, "reasoning should be passed to litellm.responses"
|
||||
|
||||
# budget_tokens=1024 -> effort="minimal" (< 2000 threshold)
|
||||
expected_reasoning = {"effort": "minimal"}
|
||||
# summary="detailed" added by default unless disable_default_reasoning_summary is set
|
||||
expected_reasoning = {"effort": "minimal", "summary": "detailed"}
|
||||
assert call_kwargs["reasoning"] == expected_reasoning, (
|
||||
f"reasoning should be {expected_reasoning} for budget_tokens=1024, "
|
||||
f"got {call_kwargs.get('reasoning')}"
|
||||
|
|
@ -225,7 +226,7 @@ class TestThinkingParameterTransformation:
|
|||
|
||||
|
||||
class TestThinkingSummaryPreservation:
|
||||
"""Tests for issue #20998: thinking.summary must be preserved when routing to OpenAI Responses API."""
|
||||
"""Tests for thinking.summary preservation and disable_default_reasoning_summary flag."""
|
||||
|
||||
def test_thinking_summary_concise_preserved_for_openai(self):
|
||||
"""User-provided summary='concise' should not be replaced with 'detailed'."""
|
||||
|
|
@ -253,26 +254,123 @@ class TestThinkingSummaryPreservation:
|
|||
)
|
||||
assert completion_kwargs["reasoning_effort"] == {"effort": "high", "summary": "auto"}
|
||||
|
||||
def test_thinking_without_summary_does_not_inject_summary(self):
|
||||
"""When no summary is provided, no summary should be injected (opt-in per OpenAI spec)."""
|
||||
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."""
|
||||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
|
||||
LiteLLMMessagesToCompletionTransformationHandler,
|
||||
)
|
||||
|
||||
thinking = {"type": "enabled", "budget_tokens": 5000}
|
||||
completion_kwargs = {"model": "openai/gpt-5.1", "reasoning_effort": "medium"}
|
||||
LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed(
|
||||
completion_kwargs, thinking=thinking
|
||||
original = litellm.disable_default_reasoning_summary
|
||||
try:
|
||||
litellm.disable_default_reasoning_summary = False
|
||||
completion_kwargs = {
|
||||
"model": "responses/gpt-5.2",
|
||||
"custom_llm_provider": "openai",
|
||||
"reasoning_effort": "medium",
|
||||
}
|
||||
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"}
|
||||
finally:
|
||||
litellm.disable_default_reasoning_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."""
|
||||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
|
||||
LiteLLMMessagesToCompletionTransformationHandler,
|
||||
)
|
||||
assert completion_kwargs["reasoning_effort"] == {"effort": "medium"}
|
||||
assert "summary" not in completion_kwargs["reasoning_effort"]
|
||||
|
||||
original = litellm.disable_default_reasoning_summary
|
||||
try:
|
||||
litellm.disable_default_reasoning_summary = True
|
||||
completion_kwargs = {
|
||||
"model": "responses/gpt-5.2",
|
||||
"custom_llm_provider": "openai",
|
||||
"reasoning_effort": "high",
|
||||
}
|
||||
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"]
|
||||
finally:
|
||||
litellm.disable_default_reasoning_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."""
|
||||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
|
||||
LiteLLMMessagesToCompletionTransformationHandler,
|
||||
)
|
||||
|
||||
original = litellm.disable_default_reasoning_summary
|
||||
try:
|
||||
litellm.disable_default_reasoning_summary = True
|
||||
completion_kwargs = {
|
||||
"model": "responses/gpt-5.2",
|
||||
"custom_llm_provider": "openai",
|
||||
"reasoning_effort": {"effort": "medium"},
|
||||
}
|
||||
LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed(
|
||||
completion_kwargs, thinking={"type": "enabled", "budget_tokens": 5000}
|
||||
)
|
||||
assert completion_kwargs["reasoning_effort"] == {"effort": "medium"}
|
||||
assert "summary" not in completion_kwargs["reasoning_effort"]
|
||||
finally:
|
||||
litellm.disable_default_reasoning_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."""
|
||||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
|
||||
LiteLLMMessagesToCompletionTransformationHandler,
|
||||
)
|
||||
|
||||
original = litellm.disable_default_reasoning_summary
|
||||
try:
|
||||
litellm.disable_default_reasoning_summary = False
|
||||
os.environ["LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY"] = "true"
|
||||
completion_kwargs = {
|
||||
"model": "responses/gpt-5.2",
|
||||
"custom_llm_provider": "openai",
|
||||
"reasoning_effort": "high",
|
||||
}
|
||||
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"]
|
||||
finally:
|
||||
litellm.disable_default_reasoning_summary = original
|
||||
os.environ.pop("LITELLM_DISABLE_DEFAULT_REASONING_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."""
|
||||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
|
||||
LiteLLMMessagesToCompletionTransformationHandler,
|
||||
)
|
||||
|
||||
original = litellm.disable_default_reasoning_summary
|
||||
try:
|
||||
litellm.disable_default_reasoning_summary = False
|
||||
completion_kwargs = {
|
||||
"model": "responses/gpt-5.2",
|
||||
"custom_llm_provider": "openai",
|
||||
"reasoning_effort": {"effort": "high", "summary": "concise"},
|
||||
}
|
||||
LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed(
|
||||
completion_kwargs, thinking={"type": "enabled", "budget_tokens": 10000}
|
||||
)
|
||||
assert completion_kwargs["reasoning_effort"]["summary"] == "concise"
|
||||
finally:
|
||||
litellm.disable_default_reasoning_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.
|
||||
|
||||
OpenAI models are routed to litellm.responses(), so we verify the
|
||||
reasoning dict passed to it contains the user's summary value.
|
||||
"""
|
||||
"""End-to-end: anthropic_messages_handler should preserve thinking.summary for OpenAI models."""
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.handler import (
|
||||
anthropic_messages_handler,
|
||||
)
|
||||
|
|
@ -309,16 +407,22 @@ class TestThinkingSummaryPreservation:
|
|||
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking)
|
||||
assert result == {"effort": "medium", "summary": "concise"}
|
||||
|
||||
def test_responses_adapter_no_summary_when_not_provided(self):
|
||||
"""translate_thinking_to_reasoning should not include summary when not provided."""
|
||||
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."""
|
||||
import litellm
|
||||
from litellm.llms.anthropic.experimental_pass_through.responses_adapters.transformation import (
|
||||
LiteLLMAnthropicToResponsesAPIAdapter,
|
||||
)
|
||||
|
||||
thinking = {"type": "enabled", "budget_tokens": 5000}
|
||||
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking)
|
||||
assert result == {"effort": "medium"}
|
||||
assert "summary" not in result
|
||||
original = litellm.disable_default_reasoning_summary
|
||||
try:
|
||||
litellm.disable_default_reasoning_summary = True
|
||||
thinking = {"type": "enabled", "budget_tokens": 5000}
|
||||
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking)
|
||||
assert result == {"effort": "medium"}
|
||||
assert "summary" not in result
|
||||
finally:
|
||||
litellm.disable_default_reasoning_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."""
|
||||
|
|
@ -332,16 +436,3 @@ class TestThinkingSummaryPreservation:
|
|||
model="openai/gpt-5.2",
|
||||
)
|
||||
assert result == {"reasoning_effort": {"effort": "medium", "summary": "concise"}}
|
||||
|
||||
def test_translate_thinking_for_model_no_summary_when_not_provided(self):
|
||||
"""translate_thinking_for_model should return plain string reasoning_effort when no summary provided."""
|
||||
from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
|
||||
LiteLLMAnthropicMessagesAdapter,
|
||||
)
|
||||
|
||||
thinking = {"type": "enabled", "budget_tokens": 5000}
|
||||
result = LiteLLMAnthropicMessagesAdapter.translate_thinking_for_model(
|
||||
thinking=thinking,
|
||||
model="openai/gpt-5.2",
|
||||
)
|
||||
assert result == {"reasoning_effort": "medium"}
|
||||
|
|
|
|||
|
|
@ -658,6 +658,38 @@ class TestTranslateThinkingToReasoning:
|
|||
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled"})
|
||||
assert result == {"effort": "minimal", "summary": "detailed"}
|
||||
|
||||
def test_summary_excluded_when_disable_flag_set(self):
|
||||
"""When disable_default_reasoning_summary is True, summary is not included."""
|
||||
import litellm
|
||||
|
||||
original = litellm.disable_default_reasoning_summary
|
||||
try:
|
||||
litellm.disable_default_reasoning_summary = True
|
||||
result = _ADAPTER.translate_thinking_to_reasoning(
|
||||
{"type": "enabled", "budget_tokens": 10000}
|
||||
)
|
||||
assert result == {"effort": "high"}
|
||||
assert "summary" not in result
|
||||
finally:
|
||||
litellm.disable_default_reasoning_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."""
|
||||
import litellm
|
||||
|
||||
original = litellm.disable_default_reasoning_summary
|
||||
try:
|
||||
litellm.disable_default_reasoning_summary = False
|
||||
os.environ["LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY"] = "true"
|
||||
result = _ADAPTER.translate_thinking_to_reasoning(
|
||||
{"type": "enabled", "budget_tokens": 5000}
|
||||
)
|
||||
assert result == {"effort": "medium"}
|
||||
assert "summary" not in result
|
||||
finally:
|
||||
litellm.disable_default_reasoning_summary = original
|
||||
os.environ.pop("LITELLM_DISABLE_DEFAULT_REASONING_SUMMARY", None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# translate_request – broader coverage
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue