mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(anthropic): keep encrypted reasoning when OpenAI thinking routes the chat adapter to Responses
With use_chat_completions_url_for_anthropic_messages on, an openai model with thinking enabled is rewritten to openai/responses/, which accepts the same organization's encrypted reasoning. The keep check now uses the same routing predicate, so turn 2 replays the block instead of stripping it.
This commit is contained in:
parent
1fa046752a
commit
0b2cfa5c1e
3 changed files with 69 additions and 28 deletions
|
|
@ -25,6 +25,7 @@ from litellm.litellm_core_utils.tool_fields import (
|
|||
from litellm.llms.anthropic.common_utils import strip_encrypted_reasoning_blocks_from_anthropic_messages
|
||||
from litellm.llms.anthropic.pass_through.adapters.transformation import (
|
||||
AnthropicAdapter,
|
||||
openai_thinking_routes_to_responses,
|
||||
)
|
||||
from litellm.llms.anthropic.pass_through.context_management import (
|
||||
AnthropicContextManagementError,
|
||||
|
|
@ -52,7 +53,6 @@ from litellm.types.llms.anthropic_messages.anthropic_response import (
|
|||
)
|
||||
from litellm.types.llms.openai import OpenAIWebSearchOptions
|
||||
from litellm.types.utils import ModelResponse
|
||||
from litellm.utils import get_model_info
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.chat_completions.dispatch import ChatResult
|
||||
|
|
@ -462,34 +462,14 @@ class LiteLLMMessagesToCompletionTransformationHandler:
|
|||
If the user provides a `summary` field in the thinking dict, it is passed
|
||||
through to the OpenAI reasoning params (opt-in per OpenAI spec).
|
||||
"""
|
||||
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: Final = completion_kwargs.get("model")
|
||||
try:
|
||||
model_info: Final = get_model_info(model=cast(str, model), custom_llm_provider=custom_llm_provider)
|
||||
if model_info and model_info.get("supports_reasoning") is False:
|
||||
# Model doesn't support reasoning/responses API, don't route
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if not isinstance(model, str) or thinking is None:
|
||||
return
|
||||
if not openai_thinking_routes_to_responses(model, completion_kwargs.get("custom_llm_provider"), thinking):
|
||||
return
|
||||
|
||||
if isinstance(model, str) and model and "responses/" not in model:
|
||||
local_model: Final = model.removeprefix(f"{custom_llm_provider}/")
|
||||
completion_kwargs["model"] = f"{custom_llm_provider}/responses/{local_model}"
|
||||
if "responses/" not in model:
|
||||
completion_kwargs["model"] = f"openai/responses/{model.removeprefix('openai/')}"
|
||||
|
||||
auto_summary: Final = is_reasoning_auto_summary_enabled()
|
||||
|
||||
|
|
|
|||
|
|
@ -227,11 +227,36 @@ class ChatRouteInputs:
|
|||
tools: Sequence[Mapping[str, object]] | None = None
|
||||
reasoning_effort: str | Mapping[str, object] | None = None
|
||||
api_base: str | None = None
|
||||
thinking: Mapping[str, object] | None = None
|
||||
|
||||
|
||||
_NO_ROUTE_INPUTS: Final = ChatRouteInputs()
|
||||
|
||||
|
||||
def _inferred_provider(model: str) -> str | None:
|
||||
try:
|
||||
return litellm.utils.get_llm_provider(model=model)[1]
|
||||
except Exception: # noqa: BLE001 # an unknown model name has no inferred provider
|
||||
return None
|
||||
|
||||
|
||||
def openai_thinking_routes_to_responses(
|
||||
model: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
thinking: Mapping[str, object] | None,
|
||||
) -> bool:
|
||||
if not model or thinking is None or thinking.get("type") != "enabled":
|
||||
return False
|
||||
provider: Final = custom_llm_provider or _inferred_provider(model)
|
||||
if provider != "openai":
|
||||
return False
|
||||
try:
|
||||
model_info: Final = litellm.get_model_info(model=model, custom_llm_provider=provider)
|
||||
except Exception: # noqa: BLE001 # a model missing from the cost map still routes, as before
|
||||
return True
|
||||
return model_info.get("supports_reasoning") is not False
|
||||
|
||||
|
||||
def _keeps_encrypted_reasoning_for_responses(
|
||||
model: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
|
|
@ -239,7 +264,9 @@ def _keeps_encrypted_reasoning_for_responses(
|
|||
) -> bool:
|
||||
from litellm.responses.bridge_check import chat_completion_ends_in_responses_api
|
||||
|
||||
return chat_completion_ends_in_responses_api(
|
||||
return openai_thinking_routes_to_responses(
|
||||
model, custom_llm_provider, route.thinking
|
||||
) or chat_completion_ends_in_responses_api(
|
||||
model,
|
||||
custom_llm_provider,
|
||||
tools=route.tools,
|
||||
|
|
@ -1321,6 +1348,7 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
tools=new_kwargs.get("tools"),
|
||||
reasoning_effort=new_kwargs.get("reasoning_effort"),
|
||||
api_base=api_base,
|
||||
thinking=anthropic_message_request.get("thinking"),
|
||||
),
|
||||
)
|
||||
## ADD SYSTEM MESSAGE TO MESSAGES
|
||||
|
|
|
|||
|
|
@ -5566,3 +5566,36 @@ async def test_messages_do_not_retry_other_rejections(
|
|||
)
|
||||
|
||||
assert route.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("_chat_bridge_httpx")
|
||||
async def test_openai_thinking_on_the_chat_adapter_keeps_reasoning_its_responses_route_replays(
|
||||
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "use_chat_completions_url_for_anthropic_messages", True)
|
||||
route: Final = respx_mock.post(url__regex=r".*openai-thinking\.example/v1/responses").mock(
|
||||
return_value=httpx.Response(200, json=_RETRY_REPLY)
|
||||
)
|
||||
|
||||
await litellm.anthropic.messages.acreate(
|
||||
model="openai/gpt-5-mini",
|
||||
api_base="https://openai-thinking.example/v1",
|
||||
api_key="fake-key",
|
||||
max_tokens=2048,
|
||||
thinking={"type": "enabled", "budget_tokens": 1024},
|
||||
messages=[
|
||||
{"role": "user", "content": "Which city is colder?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "redacted_thinking", "data": encrypted_reasoning_signature("gAAA_same_org")},
|
||||
{"type": "text", "text": "Oslo."},
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "And by how much?"},
|
||||
],
|
||||
)
|
||||
|
||||
assert route.call_count == 1
|
||||
assert [item["encrypted_content"] for item in _sent_reasoning_items(route.calls[0].request)] == ["gAAA_same_org"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue