diff --git a/litellm/llms/anthropic/messages/adapters/handler.py b/litellm/llms/anthropic/messages/adapters/handler.py index 7f790ffe1c9..a9b9b7072f0 100644 --- a/litellm/llms/anthropic/messages/adapters/handler.py +++ b/litellm/llms/anthropic/messages/adapters/handler.py @@ -84,15 +84,46 @@ class _ChatTargetReasoning: summary: str | None = None +def _provider_error_code(body: object) -> str | None: + mapping: Final = as_str_mapping(body) + if mapping is None: + return None + nested: Final = as_str_mapping(mapping.get("error")) + source: Final = nested if nested is not None else mapping + code: Final = source.get("code") + return code if isinstance(code, str) else None + + +def _logged_rejection_code(error: litellm.BadRequestError) -> str: + if error.code == "invalid_encrypted_content" or _provider_error_code(error.body) == "invalid_encrypted_content": + return "invalid_encrypted_content" + if "invalid_encrypted_content" in str(error): + return "invalid_encrypted_content" + return "could_not_be_verified" + + +def _completion_text(completion_kwargs: _CompletionKwargs, key: str) -> str: + value: Final = completion_kwargs.get(key) + return value if isinstance(value, str) else "" + + def _messages_without_rejected_reasoning( messages: _AnthropicMessages, error: litellm.BadRequestError, -) -> _AnthropicMessages | None: + completion_kwargs: _CompletionKwargs, +) -> "_AnthropicMessages | None": error_message: Final = str(error) rejected: Final = "invalid_encrypted_content" in error_message or "could not be verified" in error_message if not rejected or not messages_carry_encrypted_reasoning(messages): return None - verbose_logger.warning("Backend rejected replayed encrypted reasoning, retrying once without it: %s", error) + verbose_logger.warning( + "Backend rejected replayed encrypted reasoning, retrying once without it. " + "provider=%s model=%s status=%s code=%s", + _completion_text(completion_kwargs, "custom_llm_provider"), + _completion_text(completion_kwargs, "model"), + error.status_code, + _logged_rejection_code(error), + ) return _ANTHROPIC_MESSAGES.validate_python(strip_encrypted_reasoning_blocks_from_anthropic_messages(messages)) @@ -104,7 +135,7 @@ async def _acompletion_without_rejected_reasoning( try: return completion_kwargs, await litellm.acompletion(**completion_kwargs) except litellm.BadRequestError as error: - replayed_messages: Final = _messages_without_rejected_reasoning(messages, error) + replayed_messages: Final = _messages_without_rejected_reasoning(messages, error, completion_kwargs) if replayed_messages is None: raise replay_kwargs: Final = prepare_replay(replayed_messages) @@ -119,7 +150,7 @@ def _completion_without_rejected_reasoning( try: return completion_kwargs, litellm.completion(**completion_kwargs) except litellm.BadRequestError as error: - replayed_messages: Final = _messages_without_rejected_reasoning(messages, error) + replayed_messages: Final = _messages_without_rejected_reasoning(messages, error, completion_kwargs) if replayed_messages is None: raise replay_kwargs: Final = prepare_replay(replayed_messages) diff --git a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py index b622920b8fe..1ddaa56560c 100644 --- a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py @@ -5827,25 +5827,40 @@ def _sent_reasoning_items(request: httpx.Request) -> list[object]: @pytest.mark.usefixtures("_chat_bridge_httpx") async def test_async_messages_retry_once_without_reasoning_the_backend_cannot_decrypt( respx_mock: respx.MockRouter, + caplog: pytest.LogCaptureFixture, ) -> None: route: Final = respx_mock.post(url__regex=_RETRY_URL).mock( side_effect=[_RETRY_REJECTION, httpx.Response(200, json=_RETRY_REPLY)] ) - response: Final = await litellm.anthropic.messages.acreate( - model="azure/gpt-5.4-nano", - api_base=_RETRY_API_BASE, - api_key="fake-key", - api_version="2025-04-01-preview", - max_tokens=64, - tools=_RETRY_TOOLS, - messages=_history_with_foreign_reasoning(), - ) + with caplog.at_level(logging.WARNING, logger="LiteLLM"): + response: Final = await litellm.anthropic.messages.acreate( + model="azure/gpt-5.4-nano", + api_base=_RETRY_API_BASE, + api_key="fake-key", + api_version="2025-04-01-preview", + max_tokens=64, + tools=_RETRY_TOOLS, + messages=_history_with_foreign_reasoning(), + ) + warnings: Final = tuple( + record.getMessage() + for record in caplog.records + if record.levelno >= logging.WARNING and record.name == "LiteLLM" + ) + retry_warnings: Final = tuple(message for message in warnings if "retrying once without it" in message) assert route.call_count == 2 assert len(_sent_reasoning_items(route.calls[0].request)) == 1 assert _sent_reasoning_items(route.calls[1].request) == [] assert [block["text"] for block in response["content"] if block["type"] == "text"] == ["Oslo"] + assert retry_warnings == ( + "Backend rejected replayed encrypted reasoning, retrying once without it. " + "provider=azure model=azure/gpt-5.4-nano status=400 code=invalid_encrypted_content", + ) + assert all( + "gAAA_foreign" not in message and "litellm_encrypted_reasoning:" not in message for message in warnings + ) @pytest.mark.usefixtures("_chat_bridge_httpx")