From 59c81a30a0e1678acd74cdcddcd55ecad68ff241 Mon Sep 17 00:00:00 2001 From: Rohith sai <81635089+whitehatjr1001@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:30:05 +0530 Subject: [PATCH] fix(anthropic): route thinking requests through OpenAI responses (#20755) * fix(anthropic): route thinking requests through OpenAI responses * Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Krish Dholakia Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../adapters/handler.py | 55 +++++++++++++++++++ ...erimental_pass_through_messages_handler.py | 9 ++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py index a17eba75b3..a3e06d7ab3 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py @@ -30,6 +30,56 @@ ANTHROPIC_ADAPTER = AnthropicAdapter() class LiteLLMMessagesToCompletionTransformationHandler: + @staticmethod + def _route_openai_thinking_to_responses_api_if_needed( + completion_kwargs: Dict[str, Any], + *, + thinking: Optional[Dict[str, Any]], + ) -> None: + """ + When users call `litellm.anthropic.messages.*` with a non-Anthropic model and + `thinking={"type": "enabled", ...}`, LiteLLM converts this into OpenAI + `reasoning_effort`. + + For OpenAI models, Chat Completions typically does not return reasoning text + (only token accounting). To return a thinking-like content block in the + Anthropic response format, we route the request through OpenAI's Responses API + and request a reasoning summary. + """ + custom_llm_provider = completion_kwargs.get("custom_llm_provider") + if custom_llm_provider is None: + try: + _, inferred_provider, _, _ = litellm.utils.get_llm_provider( + model=cast(str, completion_kwargs.get("model")) + ) + custom_llm_provider = inferred_provider + except Exception: + custom_llm_provider = None + + if custom_llm_provider != "openai": + return + + if not isinstance(thinking, dict) or thinking.get("type") != "enabled": + return + + model = completion_kwargs.get("model") + if isinstance(model, str) and model and not model.startswith("responses/"): + + reasoning_effort = completion_kwargs.get("reasoning_effort") + if isinstance(reasoning_effort, str) and reasoning_effort: + completion_kwargs["reasoning_effort"] = { + "effort": reasoning_effort, + "summary": "detailed", + } + elif isinstance(reasoning_effort, dict): + if ( + "summary" not in reasoning_effort + and "generate_summary" not in reasoning_effort + ): + updated_reasoning_effort = dict(reasoning_effort) + updated_reasoning_effort["summary"] = "detailed" + completion_kwargs["reasoning_effort"] = updated_reasoning_effort + @staticmethod def _prepare_completion_kwargs( *, @@ -123,6 +173,11 @@ class LiteLLMMessagesToCompletionTransformationHandler: ): completion_kwargs[key] = value + LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed( + completion_kwargs, + thinking=thinking, + ) + return completion_kwargs, tool_name_mapping @staticmethod diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 66d62aae1e..80fd3ab698 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -185,7 +185,14 @@ def test_openai_model_with_thinking_converts_to_reasoning_effort(): # Verify reasoning_effort is set (converted from thinking) assert "reasoning_effort" in call_kwargs, "reasoning_effort should be passed to completion" - assert call_kwargs["reasoning_effort"] == "minimal", f"reasoning_effort should be 'minimal' for budget_tokens=1024, got {call_kwargs.get('reasoning_effort')}" + assert call_kwargs["reasoning_effort"] == { + "effort": "minimal", + "summary": "detailed", + }, f"reasoning_effort should request a reasoning summary for OpenAI responses API, got {call_kwargs.get('reasoning_effort')}" + + # Verify OpenAI thinking requests are routed to the Responses API + assert call_kwargs.get("model") == "responses/gpt-5.2" + # Verify thinking is NOT passed (non-Claude model) assert "thinking" not in call_kwargs, "thinking should NOT be passed for non-Claude models"