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
This commit is contained in:
Sameer Kankute 2026-04-14 10:03:10 +05:30
parent 0f453cc59d
commit 5670f6c7d4
No known key found for this signature in database
4 changed files with 41 additions and 13 deletions

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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"] == []