Merge pull request #38222 from BerriAI/litellm_fix_mantle_reasoning_effort_max

fix(completion_extras): forward reasoning_effort=max through the Responses API bridge
This commit is contained in:
Mateo Wang 2026-08-25 10:06:08 -07:00 committed by GitHub
commit 92fe35854b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 46 additions and 22 deletions

View file

@ -5,7 +5,7 @@ Handler for transforming /chat/completions api requests to litellm.responses req
import json
import os
from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, Union, cast
from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, Union, cast, get_args
from openai.types.responses.custom_tool_param import CustomToolParam
from openai.types.responses.response_input_param import (
@ -35,6 +35,7 @@ from litellm.responses.sse_output_recovery import (
)
from litellm.responses.utils import normalize_responses_api_stream_options
from litellm.types.llms.openai import (
REASONING_EFFORT,
ChatCompletionAnnotation,
ChatCompletionReasoningItem,
ChatCompletionToolCallChunk,
@ -1113,22 +1114,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
litellm.reasoning_auto_summary or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"
)
# If string is passed, map with optional summary based on flag/env var
if reasoning_effort == "none":
return Reasoning(effort="none", summary="detailed") if auto_summary_enabled else Reasoning(effort="none")
elif reasoning_effort == "high":
return Reasoning(effort="high", summary="detailed") if auto_summary_enabled else Reasoning(effort="high")
elif reasoning_effort == "xhigh":
return Reasoning(effort="xhigh", summary="detailed") if auto_summary_enabled else Reasoning(effort="xhigh")
elif reasoning_effort == "medium":
if reasoning_effort in get_args(REASONING_EFFORT):
return (
Reasoning(effort="medium", summary="detailed") if auto_summary_enabled else Reasoning(effort="medium")
)
elif reasoning_effort == "low":
return Reasoning(effort="low", summary="detailed") if auto_summary_enabled else Reasoning(effort="low")
elif reasoning_effort == "minimal":
return (
Reasoning(effort="minimal", summary="detailed") if auto_summary_enabled else Reasoning(effort="minimal")
Reasoning(effort=reasoning_effort, summary="detailed")
if auto_summary_enabled
else Reasoning(effort=reasoning_effort)
)
return None

View file

@ -1840,7 +1840,7 @@ ResponsesAPIStreamingResponse = Annotated[
]
REASONING_EFFORT = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
REASONING_EFFORT = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"]
class OpenAIRealtimeStreamSession(TypedDict, total=False):

View file

@ -2,7 +2,7 @@ import datetime
import json
import os
import unittest
from typing import TYPE_CHECKING, List, Literal, Optional, Tuple
from typing import TYPE_CHECKING, Final, List, Literal, Optional, Tuple
from unittest.mock import ANY, MagicMock, Mock, patch
import httpx
@ -1585,10 +1585,16 @@ def test_map_reasoning_effort_adds_summary_detailed(monkeypatch):
assert result_dict["summary"] == "custom_summary"
print("✓ Dict input is passed through without modification")
# Test 5: None/unknown values return None
result_unknown = handler._map_reasoning_effort("unknown_value")
assert result_unknown is None
print("✓ Unknown reasoning_effort values return None")
# Test 5: every REASONING_EFFORT level reaches the provider, and anything else (a typo, an
# unshipped level, "default") is dropped so the request still succeeds at the provider default
from litellm.types.llms.openai import Reasoning
for effort in ("max", "xhigh", "none"):
result_passthrough = handler._map_reasoning_effort(effort)
assert result_passthrough == Reasoning(effort=effort)
for dropped in ("ultra", "hgih", "unknown_value", "", "default"):
assert handler._map_reasoning_effort(dropped) is None
print("✓ Enumerated levels pass through and unknown ones are dropped")
print(
"✓ All reasoning_effort behaviors work correctly with flag/env var control"
@ -2438,6 +2444,32 @@ def test_map_optional_params_preserves_reasoning_summary():
assert responses_api_request["reasoning"]["summary"] == "detailed"
@pytest.mark.parametrize("reasoning_effort", ["max", "high"])
def test_transform_request_bedrock_mantle_tools_keeps_reasoning_effort(monkeypatch, reasoning_effort):
"""Regression for reasoning_effort=max being dropped on the chat -> Responses bridge (issue #38084)."""
from litellm.completion_extras.litellm_responses_transformation.transformation import (
LiteLLMResponsesTransformationHandler,
)
monkeypatch.setattr(litellm, "reasoning_auto_summary", False)
monkeypatch.delenv("LITELLM_REASONING_AUTO_SUMMARY", raising=False)
handler: Final = LiteLLMResponsesTransformationHandler()
result: Final = handler.transform_request(
model="openai.gpt-5.6-sol",
messages=[{"role": "user", "content": "Say pong"}],
optional_params={
"reasoning_effort": reasoning_effort,
"tools": [{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}}],
},
litellm_params={"custom_llm_provider": "bedrock_mantle"},
headers={},
litellm_logging_obj=Mock(),
)
assert result["reasoning"] == {"effort": reasoning_effort}
def test_map_optional_params_tool_choice_chat_nested_to_responses_api():
"""Chat tool_choice must become Responses ToolChoiceFunction (top-level name)."""
from litellm.completion_extras.litellm_responses_transformation.transformation import (

View file

@ -1353,6 +1353,8 @@ class TestParseCursorModelVariant:
("claude-opus-5-fast", "claude-opus-5", None),
("gpt-5.6-sol", "gpt-5.6-sol", None),
("foo-thinking-ultra-fast", "foo-thinking-ultra", None),
("gpt-5.6-thinking-max", "gpt-5.6", "max"),
("foo-thinking-mega-fast", "foo-thinking-mega", None),
("-thinking-high", "-thinking-high", None),
],
)