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 <krrishdholakia@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Rohith sai
2026-02-10 16:00:36 +05:30
committed by Sameer Kankute
co-authored by Krish Dholakia greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
parent 7f89508806
commit 59c81a30a0
2 changed files with 63 additions and 1 deletions
@@ -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
@@ -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"