mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(responses): translate the reasoning object into a chat-completion reasoning effort
The Responses API takes reasoning as an object, {effort, summary}. Chat
Completions takes reasoning_effort as a string enum and has no equivalent of
summary, but the completion bridge forwarded the whole object whenever summary
was set, which agentic clients set on every request.
Bedrock Converse guards its mapping with isinstance(value, str) and has no else
branch, so the object fell through, thinking was never enabled, and the caller
was billed for a non-thinking turn with nothing in the response to explain it.
The object is still forwarded for the one caller that can consume it: a model
whose cost-map mode is responses, which litellm.completion bridges back onto the
Responses API and reassembles {effort, summary} there. That decision is delegated
to responses_api_bridge_check, the same check litellm.completion runs, rather
than a second copy of the rule that could drift from it. An object carrying no
effort now yields no reasoning_effort at all.
This commit is contained in:
parent
d4a72e7372
commit
1d5ed79931
2 changed files with 172 additions and 17 deletions
|
|
@ -309,6 +309,67 @@ class LiteLLMCompletionResponsesConfig:
|
|||
)
|
||||
return supported_params is not None and "web_search_options" not in supported_params
|
||||
|
||||
@staticmethod
|
||||
def _completion_bridges_back_to_responses_api(
|
||||
model: str,
|
||||
custom_llm_provider: str | None,
|
||||
tools: Sequence[ChatCompletionToolParam | OpenAIMcpServerTool] | None,
|
||||
web_search_options: OpenAIWebSearchOptions | None,
|
||||
reasoning_param: Reasoning,
|
||||
) -> bool:
|
||||
"""
|
||||
Whether ``litellm.completion`` will route this model back onto the Responses API.
|
||||
|
||||
Delegates to the same check ``litellm.completion`` itself runs, so the two cannot
|
||||
disagree about which models take the Responses-shaped params.
|
||||
"""
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
||||
try:
|
||||
model_info, _ = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider or "",
|
||||
web_search_options=web_search_options,
|
||||
tools=tools,
|
||||
reasoning_effort=reasoning_param,
|
||||
reasoning_summary=reasoning_param.get("summary"),
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 # a capability probe must never fail the request it probes for
|
||||
verbose_logger.debug(f"responses bridge: reasoning effort mode check failed: {e}")
|
||||
return False
|
||||
return model_info.get("mode") == "responses"
|
||||
|
||||
@staticmethod
|
||||
def _transform_reasoning_to_reasoning_effort(
|
||||
reasoning_param: Reasoning | str | None,
|
||||
model: str,
|
||||
custom_llm_provider: str | None,
|
||||
tools: Sequence[ChatCompletionToolParam | OpenAIMcpServerTool] | None = None,
|
||||
web_search_options: OpenAIWebSearchOptions | None = None,
|
||||
) -> Reasoning | str | None:
|
||||
"""
|
||||
Map the Responses ``reasoning`` param onto Chat Completions ``reasoning_effort``.
|
||||
|
||||
Chat Completions defines ``reasoning_effort`` as a string enum, and ``summary`` is a
|
||||
Responses-only field with no Chat Completions equivalent. Sending the whole object to a
|
||||
chat provider is rejected or silently discarded, which turns reasoning off. The object is
|
||||
kept only when ``litellm.completion`` will bridge this model back onto the Responses API,
|
||||
the one caller that can consume it.
|
||||
"""
|
||||
if not reasoning_param:
|
||||
return None
|
||||
if isinstance(reasoning_param, str):
|
||||
return reasoning_param
|
||||
if LiteLLMCompletionResponsesConfig._completion_bridges_back_to_responses_api(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
tools=tools,
|
||||
web_search_options=web_search_options,
|
||||
reasoning_param=reasoning_param,
|
||||
):
|
||||
return reasoning_param
|
||||
return reasoning_param.get("effort")
|
||||
|
||||
@staticmethod
|
||||
def transform_responses_api_request_to_chat_completion_request(
|
||||
model: str,
|
||||
|
|
@ -339,23 +400,15 @@ class LiteLLMCompletionResponsesConfig:
|
|||
if text_param:
|
||||
response_format = LiteLLMCompletionResponsesConfig._transform_text_format_to_response_format(text_param)
|
||||
|
||||
# Extract reasoning_effort from reasoning parameter
|
||||
reasoning_effort: Reasoning | str | None = None
|
||||
reasoning_param: Final = responses_api_request.get("reasoning")
|
||||
if reasoning_param:
|
||||
if isinstance(reasoning_param, dict):
|
||||
# reasoning can be {"effort": "low|medium|high", "summary": "detailed"}
|
||||
# Keep the full dict when summary is set so the responses API bridge can
|
||||
# forward it; otherwise use the effort string for chat completion (e.g. Gemini).
|
||||
if "summary" in reasoning_param:
|
||||
reasoning_effort = reasoning_param
|
||||
elif "effort" in reasoning_param:
|
||||
reasoning_effort = reasoning_param.get("effort")
|
||||
else:
|
||||
reasoning_effort = reasoning_param
|
||||
elif isinstance(reasoning_param, str):
|
||||
# reasoning could be a string directly
|
||||
reasoning_effort = reasoning_param
|
||||
reasoning_effort: Final[Reasoning | str | None] = (
|
||||
LiteLLMCompletionResponsesConfig._transform_reasoning_to_reasoning_effort(
|
||||
reasoning_param=responses_api_request.get("reasoning"),
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
tools=tools,
|
||||
web_search_options=web_search_options,
|
||||
)
|
||||
)
|
||||
|
||||
litellm_completion_request: dict = {
|
||||
"messages": LiteLLMCompletionResponsesConfig.transform_responses_api_input_to_messages(
|
||||
|
|
|
|||
|
|
@ -2527,6 +2527,108 @@ class TestToolTransformation:
|
|||
"type": "object",
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, custom_llm_provider",
|
||||
[
|
||||
("bedrock/converse/global.anthropic.claude-sonnet-5", "bedrock_converse"),
|
||||
("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
|
||||
("claude-sonnet-5", "vertex_ai"),
|
||||
("gemini-3.1-pro-preview", "vertex_ai"),
|
||||
("moonshotai.kimi-k2-thinking", "bedrock_mantle"),
|
||||
],
|
||||
)
|
||||
def test_reasoning_summary_still_yields_a_string_reasoning_effort(self, model, custom_llm_provider):
|
||||
"""
|
||||
A Responses request carrying ``reasoning.summary`` must still reach a chat provider as a
|
||||
plain ``reasoning_effort`` string. ``summary`` is Responses-only, and forwarding the whole
|
||||
object turns reasoning off: Bedrock Converse and Vertex silently discard a non-string
|
||||
``reasoning_effort``, and Bedrock Mantle rejects the request outright.
|
||||
"""
|
||||
responses_api_request = {"reasoning": {"effort": "medium", "summary": "auto"}}
|
||||
|
||||
result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request(
|
||||
model=model,
|
||||
input="hi",
|
||||
responses_api_request=responses_api_request,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
assert result["reasoning_effort"] == "medium"
|
||||
|
||||
def test_responses_mode_model_keeps_the_whole_reasoning_object(self):
|
||||
"""
|
||||
The one consumer of the object form is ``litellm.completion`` bridging a ``mode: responses``
|
||||
model back onto the Responses API, which has no native Responses config of its own. That
|
||||
path reassembles ``{effort, summary}``, so the object must survive for it.
|
||||
"""
|
||||
responses_api_request = {"reasoning": {"effort": "medium", "summary": "auto"}}
|
||||
|
||||
result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request(
|
||||
model="gpt-5.4-pro",
|
||||
input="hi",
|
||||
responses_api_request=responses_api_request,
|
||||
custom_llm_provider="azure_ai",
|
||||
)
|
||||
|
||||
assert result["reasoning_effort"] == {"effort": "medium", "summary": "auto"}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reasoning, expected",
|
||||
[
|
||||
({"effort": "high"}, "high"),
|
||||
("low", "low"),
|
||||
({"summary": "auto"}, None),
|
||||
({}, None),
|
||||
(None, None),
|
||||
],
|
||||
)
|
||||
def test_reasoning_param_shapes_map_to_reasoning_effort(self, reasoning, expected):
|
||||
"""
|
||||
An object without ``effort`` carries nothing Chat Completions can use, so no
|
||||
``reasoning_effort`` is sent at all (the bridge drops None-valued params).
|
||||
"""
|
||||
result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request(
|
||||
model="anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
input="hi",
|
||||
responses_api_request={"reasoning": reasoning},
|
||||
custom_llm_provider="bedrock",
|
||||
)
|
||||
|
||||
assert result.get("reasoning_effort") == expected
|
||||
assert ("reasoning_effort" in result) is (expected is not None)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, expected_thinking",
|
||||
[
|
||||
("global.anthropic.claude-sonnet-5", {"type": "adaptive"}),
|
||||
(
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
{"type": "enabled", "budget_tokens": 2048},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_reasoning_summary_still_enables_thinking_on_bedrock(self, model, expected_thinking):
|
||||
"""
|
||||
End to end through Bedrock Converse's own param mapping: the effort a Responses request asks
|
||||
for must survive into ``thinking``, whether the model takes an adaptive effort or a legacy
|
||||
token budget. Forwarding the object instead leaves ``thinking`` unset and the model never
|
||||
reasons, which is the failure this guards.
|
||||
"""
|
||||
from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig
|
||||
|
||||
bridged = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request(
|
||||
model=model,
|
||||
input="hi",
|
||||
responses_api_request={"reasoning": {"effort": "medium", "summary": "auto"}},
|
||||
custom_llm_provider="bedrock",
|
||||
)
|
||||
|
||||
mapped = AmazonConverseConfig().map_openai_params(
|
||||
{"reasoning_effort": bridged["reasoning_effort"]}, {}, model, True
|
||||
)
|
||||
|
||||
assert mapped["thinking"] == expected_thinking
|
||||
|
||||
def test_bedrock_anthropic_responses_tools_yield_only_function_toolspec(self):
|
||||
"""
|
||||
End-to-end (no network) of the LIT-3858 acceptance criterion: the mixed tools array
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue