From 1d5ed799314ee8aa28d4c92d7fe749f3379800b7 Mon Sep 17 00:00:00 2001 From: Joshua Garnett Date: Sun, 9 Aug 2026 13:49:23 -0400 Subject: [PATCH 1/4] 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. --- .../transformation.py | 87 ++++++++++++--- .../test_litellm_completion_responses.py | 102 ++++++++++++++++++ 2 files changed, 172 insertions(+), 17 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index cc594f167c7..bd4108a0d27 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -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( diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 342ec4435a7..8e76ee315b7 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -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 From 3d648b7fe1adb0c36bf2615e9a06fe153e8f9122 Mon Sep 17 00:00:00 2001 From: Joshua Garnett Date: Sun, 9 Aug 2026 13:58:23 -0400 Subject: [PATCH 2/4] fix(responses): pass api_base to the responses bridge check The bridge probe called responses_api_bridge_check without api_base, so it resolved the OpenAI base from globals and environment rather than from the request, while litellm.completion runs the same check with the caller's value. Today the two cannot disagree: this path always supplies a reasoning_effort, which short-circuits the endpoint term in the only arm that reads it. Passing it anyway keeps the probe a faithful mirror of the definitive check rather than one that happens to agree. --- .../litellm_completion_transformation/transformation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index bd4108a0d27..8d790024cfe 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -316,6 +316,7 @@ class LiteLLMCompletionResponsesConfig: tools: Sequence[ChatCompletionToolParam | OpenAIMcpServerTool] | None, web_search_options: OpenAIWebSearchOptions | None, reasoning_param: Reasoning, + api_base: str | None, ) -> bool: """ Whether ``litellm.completion`` will route this model back onto the Responses API. @@ -333,6 +334,7 @@ class LiteLLMCompletionResponsesConfig: tools=tools, reasoning_effort=reasoning_param, reasoning_summary=reasoning_param.get("summary"), + api_base=api_base, ) 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}") @@ -346,6 +348,7 @@ class LiteLLMCompletionResponsesConfig: custom_llm_provider: str | None, tools: Sequence[ChatCompletionToolParam | OpenAIMcpServerTool] | None = None, web_search_options: OpenAIWebSearchOptions | None = None, + api_base: str | None = None, ) -> Reasoning | str | None: """ Map the Responses ``reasoning`` param onto Chat Completions ``reasoning_effort``. @@ -366,6 +369,7 @@ class LiteLLMCompletionResponsesConfig: tools=tools, web_search_options=web_search_options, reasoning_param=reasoning_param, + api_base=api_base, ): return reasoning_param return reasoning_param.get("effort") @@ -407,6 +411,7 @@ class LiteLLMCompletionResponsesConfig: custom_llm_provider=custom_llm_provider, tools=tools, web_search_options=web_search_options, + api_base=kwargs.get("api_base"), ) ) From 216af3826e6dce1516d36b6f99a9335f276dee35 Mon Sep 17 00:00:00 2001 From: Joshua Garnett Date: Sun, 9 Aug 2026 14:16:59 -0400 Subject: [PATCH 3/4] fix(responses): log the bridge check failure lazily The debug call built its message with an f-string, which test_logging_calls_do_not_build_their_message_eagerly rejects. Pass the exception as a %-style argument so the message is only built when the log is emitted. --- .../litellm_completion_transformation/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 8d790024cfe..c7f8ab24de4 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -337,7 +337,7 @@ class LiteLLMCompletionResponsesConfig: api_base=api_base, ) 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}") + verbose_logger.debug("responses bridge: reasoning effort mode check failed: %s", e) return False return model_info.get("mode") == "responses" From 0c83e831db6f420845d3deec0484da7ff717036b Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 12 Sep 2026 17:49:30 -0700 Subject: [PATCH 4/4] fix(responses): carry the reasoning summary as an alias, not inside reasoning_effort The bridge probe asked `responses_api_bridge_check` with the summary read straight off the Responses object, but `litellm.completion` reads it from `optional_params` via `peek_reasoning_summary_aliases`, which the bridged request never populated. So gpt-5, gpt-5.1 and azure/gpt-5 answered "bridging" to the probe and "not bridging" for real, and the object still landed on Chat Completions, which only takes a string `reasoning_effort` is now always the effort string, and `summary` rides the `reasoning_summary` alias that main.py already reassembles into `{effort, summary}` on the bridged path. The alias is emitted only when the probe says the model bridges, so no chat provider ever sees it, and the probe is now asked with the exact params this transform emits --- .../transformation.py | 75 +++++++++------- .../test_litellm_completion_responses.py | 88 +++++++++++++++++-- 2 files changed, 125 insertions(+), 38 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index c7f8ab24de4..e8aacac9e67 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -117,6 +117,14 @@ class ResponsesToolChatForm: web_search_options: OpenAIWebSearchOptions | None +@dataclass(frozen=True, slots=True) +class ResponsesReasoningChatForm: + """The Responses ``reasoning`` object as the two params Chat Completions takes.""" + + effort: str | None + summary: str | None + + if TYPE_CHECKING: from openai.types.responses.response_apply_patch_tool_call import ( ResponseApplyPatchToolCall, @@ -315,14 +323,15 @@ class LiteLLMCompletionResponsesConfig: custom_llm_provider: str | None, tools: Sequence[ChatCompletionToolParam | OpenAIMcpServerTool] | None, web_search_options: OpenAIWebSearchOptions | None, - reasoning_param: Reasoning, + reasoning_effort: str | None, + reasoning_summary: str | None, api_base: str | None, ) -> 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. + Delegates to the same check ``litellm.completion`` itself runs, and is asked with the + params this transform is about to emit, so the two cannot reach different answers. """ from litellm.main import responses_api_bridge_check @@ -332,8 +341,8 @@ class LiteLLMCompletionResponsesConfig: 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"), + reasoning_effort=reasoning_effort, + reasoning_summary=reasoning_summary, api_base=api_base, ) except Exception as e: # noqa: BLE001 # a capability probe must never fail the request it probes for @@ -342,37 +351,44 @@ class LiteLLMCompletionResponsesConfig: return model_info.get("mode") == "responses" @staticmethod - def _transform_reasoning_to_reasoning_effort( + def _transform_reasoning_for_chat_completion( reasoning_param: Reasoning | str | None, model: str, custom_llm_provider: str | None, tools: Sequence[ChatCompletionToolParam | OpenAIMcpServerTool] | None = None, web_search_options: OpenAIWebSearchOptions | None = None, api_base: str | None = None, - ) -> Reasoning | str | None: + ) -> ResponsesReasoningChatForm: """ - Map the Responses ``reasoning`` param onto Chat Completions ``reasoning_effort``. + Split the Responses ``reasoning`` object into the params Chat Completions understands. - 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. + ``reasoning_effort`` is a string enum there, so the object is never forwarded whole: a chat + provider either rejects it or silently drops it, and dropping it turns reasoning off while + still billing for the turn. ``summary`` has no chat equivalent, so it rides the + ``reasoning_summary`` alias, which ``litellm.completion`` reassembles into ``{effort, + summary}`` when it bridges the model back onto the Responses API, and is sent to nothing + else. """ if not reasoning_param: - return None + return ResponsesReasoningChatForm(effort=None, summary=None) if isinstance(reasoning_param, str): - return reasoning_param - if LiteLLMCompletionResponsesConfig._completion_bridges_back_to_responses_api( + return ResponsesReasoningChatForm(effort=reasoning_param, summary=None) + + effort: Final = reasoning_param.get("effort") + summary: Final = reasoning_param.get("summary") + if summary is None: + return ResponsesReasoningChatForm(effort=effort, summary=None) + + bridges_back: Final = 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, + reasoning_effort=effort, + reasoning_summary=summary, api_base=api_base, - ): - return reasoning_param - return reasoning_param.get("effort") + ) + return ResponsesReasoningChatForm(effort=effort, summary=summary if bridges_back else None) @staticmethod def transform_responses_api_request_to_chat_completion_request( @@ -404,15 +420,13 @@ class LiteLLMCompletionResponsesConfig: if text_param: response_format = LiteLLMCompletionResponsesConfig._transform_text_format_to_response_format(text_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, - api_base=kwargs.get("api_base"), - ) + reasoning: Final = LiteLLMCompletionResponsesConfig._transform_reasoning_for_chat_completion( + reasoning_param=responses_api_request.get("reasoning"), + model=model, + custom_llm_provider=custom_llm_provider, + tools=tools, + web_search_options=web_search_options, + api_base=kwargs.get("api_base"), ) litellm_completion_request: dict = { @@ -436,7 +450,8 @@ class LiteLLMCompletionResponsesConfig: "service_tier": kwargs.get("service_tier"), "web_search_options": web_search_options, "response_format": response_format, - "reasoning_effort": reasoning_effort, + "reasoning_effort": reasoning.effort, + "reasoning_summary": reasoning.summary, "context_management": responses_api_request.get("context_management"), # litellm specific params "custom_llm_provider": custom_llm_provider, diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 8e76ee315b7..3c78bbf79d7 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -2555,22 +2555,94 @@ class TestToolTransformation: assert result["reasoning_effort"] == "medium" - def test_responses_mode_model_keeps_the_whole_reasoning_object(self): + @pytest.mark.parametrize( + "model, custom_llm_provider", + [ + ("gpt-5.4-pro", "azure_ai"), + ("gpt-5", "openai"), + ("gpt-5.1", "openai"), + ("gpt-5", "azure"), + ], + ) + def test_bridged_model_carries_the_summary_as_an_alias(self, model, custom_llm_provider): """ - 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. + ``summary`` reaches a bridged model through the ``reasoning_summary`` alias, never smuggled + inside ``reasoning_effort``. ``litellm.completion`` reads that alias back with + ``peek_reasoning_summary_aliases`` and reassembles ``{effort, summary}``, so the far end + gets the same object it always did while no chat provider ever sees a non-string effort. """ - 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={"reasoning": {"effort": "medium", "summary": "auto"}}, + custom_llm_provider=custom_llm_provider, + ) + + assert result["reasoning_effort"] == "medium" + assert result["reasoning_summary"] == "auto" + + @pytest.mark.parametrize( + "model, custom_llm_provider", + [ + ("gpt-5", "openai"), + ("gpt-5.1", "openai"), + ("gpt-5", "azure"), + ], + ) + def test_gpt_5_summary_survives_the_bridge_it_claims_to_take(self, model, custom_llm_provider): + """ + Regression for the probe disagreeing with the real decision. The transform asked + ``responses_api_bridge_check`` with ``reasoning_summary`` taken straight off the Responses + object, but ``litellm.completion`` reads it from ``optional_params`` via + ``peek_reasoning_summary_aliases``, which the bridged request never populated. So these + models answered "bridging" to the probe and "not bridging" for real, and the object landed + on Chat Completions, which only takes a string. Emitting the alias makes the two agree. + """ + from litellm.main import responses_api_bridge_check + from litellm.utils import get_optional_params, peek_reasoning_summary_aliases + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model=model, + input="hi", + responses_api_request={"reasoning": {"effort": "medium", "summary": "auto"}}, + custom_llm_provider=custom_llm_provider, + ) + optional_params = get_optional_params( + model=model, + custom_llm_provider=custom_llm_provider, + reasoning_effort=result["reasoning_effort"], + reasoning_summary=result["reasoning_summary"], + ) + model_info, _ = responses_api_bridge_check( + model=model, + custom_llm_provider=custom_llm_provider, + reasoning_effort=result["reasoning_effort"], + reasoning_summary=peek_reasoning_summary_aliases(optional_params), + ) + + assert model_info.get("mode") == "responses" + + def test_a_failing_bridge_probe_falls_back_to_the_string_effort(self, monkeypatch): + """ + The probe is a capability question, so a model-info lookup blowing up must not fail the + request. It degrades to the chat-safe form: a string effort and no alias. + """ + import litellm.main + + def _boom(**_kwargs): + raise RuntimeError("model info unavailable") + + monkeypatch.setattr(litellm.main, "responses_api_bridge_check", _boom) result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( model="gpt-5.4-pro", input="hi", - responses_api_request=responses_api_request, + responses_api_request={"reasoning": {"effort": "medium", "summary": "auto"}}, custom_llm_provider="azure_ai", ) - assert result["reasoning_effort"] == {"effort": "medium", "summary": "auto"} + assert result["reasoning_effort"] == "medium" + assert "reasoning_summary" not in result @pytest.mark.parametrize( "reasoning, expected", @@ -2627,7 +2699,7 @@ class TestToolTransformation: {"reasoning_effort": bridged["reasoning_effort"]}, {}, model, True ) - assert mapped["thinking"] == expected_thinking + assert expected_thinking.items() <= mapped["thinking"].items() def test_bedrock_anthropic_responses_tools_yield_only_function_toolspec(self): """