fix(anthropic): fix clear_thinking retry body after thinking strip

- restore top-level thinking when invalid-signature retry keeps clear_thinking_20251015
- keep the fix scoped to Anthropic Messages passthrough retry handling
- avoid carrying manual budget_tokens into adaptive thinking retries
- add behavior coverage for enabled, adaptive, disabled, missing, and non-adaptive cases
This commit is contained in:
mchtech 2026-05-09 11:12:39 +08:00
parent 0bcff0214a
commit a54af4f59a
2 changed files with 197 additions and 0 deletions

View file

@ -332,6 +332,48 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
)
return dict(anthropic_messages_request)
def transform_anthropic_messages_request_on_http_error(
self, e: httpx.HTTPStatusError, request_data: dict
) -> dict:
# The base retry removes invalid historical thinking blocks and also drops
# top-level thinking. Keep the caller's original mode so clear_thinking
# retries for Claude 4.6+ can still satisfy Anthropic's request contract.
original_thinking = request_data.get("thinking")
request_data = super().transform_anthropic_messages_request_on_http_error(
e=e, request_data=request_data
)
model = request_data.get("model")
context_management = request_data.get("context_management")
edits = (
context_management.get("edits")
if isinstance(context_management, dict)
else None
)
thinking = request_data.get("thinking")
has_active_thinking = isinstance(thinking, dict) and thinking.get("type") in (
"enabled",
"adaptive",
)
has_clear_thinking = isinstance(edits, list) and any(
isinstance(edit, dict) and edit.get("type") == "clear_thinking_20251015"
for edit in edits
)
if (
isinstance(model, str)
and isinstance(original_thinking, dict)
and original_thinking.get("type") in ("enabled", "adaptive")
and has_clear_thinking
and not has_active_thinking
and AnthropicModelInfo._is_adaptive_thinking_model(model)
):
# clear_thinking edits still require active thinking on Claude 4.6+ retries.
restored_thinking = dict(original_thinking)
if restored_thinking.get("type") == "adaptive":
# Adaptive thinking chooses its budget server-side.
restored_thinking.pop("budget_tokens", None)
request_data["thinking"] = restored_thinking
return request_data
def transform_anthropic_messages_response(
self,
model: str,

View file

@ -1141,6 +1141,18 @@ class TestPassthroughAuthToken:
class TestAnthropicThinkingSignatureSelfHeal:
"""Helpers for retrying after invalid encrypted thinking signatures."""
def _invalid_thinking_signature_error(self):
import httpx
req = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
err_text = (
'{"type":"error","error":{"type":"invalid_request_error",'
'"message":"messages.3.content.3: Invalid `signature` in `thinking` block"},'
'"request_id":"req_011Ca2EtQDxp7x6RGUY2jVn9"}'
)
resp = httpx.Response(400, request=req, text=err_text)
return httpx.HTTPStatusError("bad", request=req, response=resp)
def test_is_anthropic_invalid_thinking_signature_error_positive(self):
from litellm.llms.anthropic.common_utils import (
is_anthropic_invalid_thinking_signature_error,
@ -1229,6 +1241,149 @@ class TestAnthropicThinkingSignatureSelfHeal:
assert "thinking" not in data
assert data["messages"] == []
def test_anthropic_messages_retry_restores_enabled_thinking_for_clear_thinking(
self,
):
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
data = {
"model": "claude-sonnet-4-6",
"messages": [
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "x",
"signature": "y",
},
{"type": "text", "text": "answer"},
],
}
],
"context_management": {"edits": [{"type": "clear_thinking_20251015"}]},
"thinking": {"type": "enabled", "budget_tokens": 1024},
}
AnthropicMessagesConfig().transform_anthropic_messages_request_on_http_error(
self._invalid_thinking_signature_error(), data
)
assert data["thinking"] == {"type": "enabled", "budget_tokens": 1024}
assert data["messages"][0]["content"] == [{"type": "text", "text": "answer"}]
def test_anthropic_messages_retry_restores_adaptive_thinking_without_budget(
self,
):
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
data = {
"model": "claude-sonnet-4-6",
"messages": [
{
"role": "assistant",
"content": [
{
"type": "redacted_thinking",
"data": "encrypted",
},
{"type": "text", "text": "answer"},
],
}
],
"context_management": {"edits": [{"type": "clear_thinking_20251015"}]},
"thinking": {"type": "adaptive", "budget_tokens": 4096},
}
AnthropicMessagesConfig().transform_anthropic_messages_request_on_http_error(
self._invalid_thinking_signature_error(), data
)
assert data["thinking"] == {"type": "adaptive"}
assert data["messages"][0]["content"] == [{"type": "text", "text": "answer"}]
def test_anthropic_messages_retry_does_not_restore_disabled_thinking(
self,
):
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
data = {
"model": "claude-sonnet-4-6",
"messages": [{"role": "user", "content": "hi"}],
"context_management": {"edits": [{"type": "clear_thinking_20251015"}]},
"thinking": {"type": "disabled"},
}
AnthropicMessagesConfig().transform_anthropic_messages_request_on_http_error(
self._invalid_thinking_signature_error(), data
)
assert "thinking" not in data
def test_anthropic_messages_retry_does_not_restore_missing_thinking(
self,
):
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
data = {
"model": "claude-sonnet-4-6",
"messages": [{"role": "user", "content": "hi"}],
"context_management": {"edits": [{"type": "clear_thinking_20251015"}]},
}
AnthropicMessagesConfig().transform_anthropic_messages_request_on_http_error(
self._invalid_thinking_signature_error(), data
)
assert "thinking" not in data
def test_anthropic_messages_retry_does_not_restore_without_clear_thinking(
self,
):
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
data = {
"model": "claude-sonnet-4-6",
"messages": [{"role": "user", "content": "hi"}],
"thinking": {"type": "adaptive"},
}
AnthropicMessagesConfig().transform_anthropic_messages_request_on_http_error(
self._invalid_thinking_signature_error(), data
)
assert "thinking" not in data
def test_anthropic_messages_retry_does_not_restore_for_non_adaptive_model(
self,
):
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
data = {
"model": "claude-sonnet-4-20250514",
"messages": [{"role": "user", "content": "hi"}],
"context_management": {"edits": [{"type": "clear_thinking_20251015"}]},
"thinking": {"type": "enabled", "budget_tokens": 1024},
}
AnthropicMessagesConfig().transform_anthropic_messages_request_on_http_error(
self._invalid_thinking_signature_error(), data
)
assert "thinking" not in data
def test_anthropic_messages_config_http_retry_helpers(self):
import httpx