mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
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:
parent
0bcff0214a
commit
a54af4f59a
2 changed files with 197 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue