From 5670f6c7d49bb2255d9a9460336b74e4ea2157be Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 14 Apr 2026 10:03:10 +0530 Subject: [PATCH] fix(anthropic): tighten thinking-signature retry (Greptile) - Omit messages whose list content is empty after stripping thinking blocks - Retry only on HTTP 400 plus invalid-signature body match - Return response inline from retry loop; drop unreachable None guard - Tests: thinking-only turn dropped, non-400 no retry Made-with: Cursor --- litellm/llms/anthropic/common_utils.py | 8 +++++- .../anthropic_messages/transformation.py | 8 ++++-- litellm/llms/custom_httpx/llm_http_handler.py | 12 +++------ .../anthropic/test_anthropic_common_utils.py | 26 +++++++++++++++++-- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 4f7f3814e74..3be5a0c816f 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -760,6 +760,9 @@ def strip_thinking_blocks_from_anthropic_messages(messages: List[Any]) -> List[A """ Return a new message list with thinking / redacted_thinking content blocks removed from each message. Used to recover from invalid thinking signatures on retry. + + Messages whose content is a list and becomes empty after stripping are omitted, + since Anthropic rejects empty content arrays. """ out: List[Any] = [] for m in messages: @@ -769,7 +772,7 @@ def strip_thinking_blocks_from_anthropic_messages(messages: List[Any]) -> List[A mm = copy.deepcopy(m) content = mm.get("content") if isinstance(content, list): - mm["content"] = [ + filtered = [ b for b in content if not ( @@ -777,6 +780,9 @@ def strip_thinking_blocks_from_anthropic_messages(messages: List[Any]) -> List[A and b.get("type") in ("thinking", "redacted_thinking") ) ] + if not filtered: + continue + mm["content"] = filtered out.append(mm) return out diff --git a/litellm/llms/base_llm/anthropic_messages/transformation.py b/litellm/llms/base_llm/anthropic_messages/transformation.py index 40063c0fb9a..733faca8532 100644 --- a/litellm/llms/base_llm/anthropic_messages/transformation.py +++ b/litellm/llms/base_llm/anthropic_messages/transformation.py @@ -141,7 +141,9 @@ class BaseAnthropicMessagesConfig(ABC): is_anthropic_invalid_thinking_signature_error, ) - return is_anthropic_invalid_thinking_signature_error(e.response.text) + return e.response.status_code == 400 and is_anthropic_invalid_thinking_signature_error( + e.response.text + ) def transform_anthropic_messages_request_on_http_error( self, e: httpx.HTTPStatusError, request_data: dict @@ -154,6 +156,8 @@ class BaseAnthropicMessagesConfig(ABC): strip_thinking_blocks_from_anthropic_messages_request_dict, ) - if is_anthropic_invalid_thinking_signature_error(e.response.text): + if e.response.status_code == 400 and is_anthropic_invalid_thinking_signature_error( + e.response.text + ): strip_thinking_blocks_from_anthropic_messages_request_dict(request_data) return request_data diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 9155fbb4aca..60efa45df62 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -1833,7 +1833,6 @@ class BaseLLMHTTPHandler: max_attempts = max(provider_config.max_retry_on_anthropic_messages_http_error, 1) litellm_params_dict = dict(litellm_params) optional_params_dict = dict(litellm_params) - response: Optional[httpx.Response] = None for attempt_idx in range(max_attempts): try: response = await async_httpx_client.post( @@ -1844,6 +1843,7 @@ class BaseLLMHTTPHandler: logging_obj=logging_obj, ) response.raise_for_status() + return response except httpx.HTTPStatusError as e: hit_max_attempt = attempt_idx + 1 == max_attempts should_retry = provider_config.should_retry_anthropic_messages_on_http_error( @@ -1874,14 +1874,10 @@ class BaseLLMHTTPHandler: raise self._handle_error(e=e, provider_config=provider_config) except Exception as e: raise self._handle_error(e=e, provider_config=provider_config) - break - if response is None: - raise self._handle_error( - e=ValueError("No response from Anthropic /v1/messages"), - provider_config=provider_config, - ) - return response + raise RuntimeError( + "unreachable: anthropic messages HTTP retry loop exited without return" + ) async def async_anthropic_messages_handler( self, diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index ba344403912..d48d7716a8e 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -1181,6 +1181,24 @@ class TestAnthropicThinkingSignatureSelfHeal: assert out[1]["content"][0]["type"] == "text" assert messages[1]["content"][0]["type"] == "thinking" + def test_strip_thinking_blocks_drops_message_when_only_thinking_blocks(self): + from litellm.llms.anthropic.common_utils import ( + strip_thinking_blocks_from_anthropic_messages, + ) + + messages = [ + {"role": "user", "content": "hi"}, + { + "role": "assistant", + "content": [ + {"type": "thinking", "thinking": "plan", "signature": "sig"}, + ], + }, + ] + out = strip_thinking_blocks_from_anthropic_messages(messages) + assert len(out) == 1 + assert out[0]["role"] == "user" + def test_strip_thinking_blocks_from_anthropic_messages_request_dict(self): from litellm.llms.anthropic.common_utils import ( strip_thinking_blocks_from_anthropic_messages_request_dict, @@ -1204,7 +1222,7 @@ class TestAnthropicThinkingSignatureSelfHeal: } strip_thinking_blocks_from_anthropic_messages_request_dict(data) assert "thinking" not in data - assert data["messages"][0]["content"] == [] + assert data["messages"] == [] def test_anthropic_messages_config_http_retry_helpers(self): import httpx @@ -1230,6 +1248,10 @@ class TestAnthropicThinkingSignatureSelfHeal: err_bad = httpx.HTTPStatusError("bad", request=req, response=resp_bad) assert config.should_retry_anthropic_messages_on_http_error(err_bad, {}) is False + resp_500 = httpx.Response(500, request=req, text=err_text) + err_500 = httpx.HTTPStatusError("bad", request=req, response=resp_500) + assert config.should_retry_anthropic_messages_on_http_error(err_500, {}) is False + data = { "model": "claude-sonnet-4-20250514", "messages": [ @@ -1248,4 +1270,4 @@ class TestAnthropicThinkingSignatureSelfHeal: } config.transform_anthropic_messages_request_on_http_error(err, data) assert "thinking" not in data - assert data["messages"][0]["content"] == [] + assert data["messages"] == []