mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix(anthropic): strip bridge reasoning in the native messages transform, not the empty-block pass
This commit is contained in:
parent
2bca7ff673
commit
751431dc3f
4 changed files with 126 additions and 22 deletions
|
|
@ -1202,6 +1202,30 @@ def strip_thinking_blocks_from_anthropic_messages(messages: list[Any]) -> list[A
|
|||
return out
|
||||
|
||||
|
||||
def _without_encrypted_reasoning_blocks(message: dict) -> dict | None: # mutable-ok: Anthropic message payload shape
|
||||
content: Final = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
return message
|
||||
kept: Final = [b for b in content if not is_encrypted_reasoning_block(b)] # mutable-ok: API message payload
|
||||
if len(kept) == len(content):
|
||||
return message
|
||||
if not kept:
|
||||
return None
|
||||
return {**message, "content": kept} # mutable-ok: API message payload
|
||||
|
||||
|
||||
def strip_encrypted_reasoning_blocks_from_anthropic_messages(
|
||||
messages: Sequence[dict], # mutable-ok: Anthropic message payload shape
|
||||
) -> list[dict]: # mutable-ok: AnthropicMessagesRequest.messages is typed list[dict]
|
||||
"""
|
||||
Drop thinking / redacted_thinking blocks that carry another provider's encrypted
|
||||
reasoning (a turn the Responses API bridge served) before the request reaches
|
||||
Anthropic, which cannot verify them. Anthropic's own signed blocks are kept.
|
||||
"""
|
||||
stripped: Final = (_without_encrypted_reasoning_blocks(m) for m in messages)
|
||||
return [m for m in stripped if m is not None] # mutable-ok: API message payload
|
||||
|
||||
|
||||
def strip_thinking_blocks_from_anthropic_messages_request_dict(
|
||||
data: dict[str, Any],
|
||||
) -> None:
|
||||
|
|
@ -1236,10 +1260,8 @@ def strip_empty_content_blocks_from_anthropic_messages(
|
|||
on the unified ``/v1/messages`` path. ``/v1/chat/completions`` already
|
||||
handles this in ``anthropic_messages_pt``; this helper provides the
|
||||
equivalent guarantee for the native Anthropic Messages path.
|
||||
A thinking or ``redacted_thinking`` block whose signature or data carries
|
||||
another provider's encrypted reasoning (a turn served by the Responses API
|
||||
bridge) is dropped too, since Anthropic cannot verify it; every other
|
||||
``redacted_thinking`` block is left alone.
|
||||
``redacted_thinking`` blocks are never touched: they carry opaque
|
||||
``data`` instead of thinking text.
|
||||
|
||||
Messages whose content is a list and becomes empty after stripping are
|
||||
omitted, matching :func:`strip_thinking_blocks_from_anthropic_messages`.
|
||||
|
|
@ -1252,11 +1274,7 @@ def strip_empty_content_blocks_from_anthropic_messages(
|
|||
out.append(m)
|
||||
continue
|
||||
content = m["content"]
|
||||
filtered = [ # mutable-ok: rebuilt message content list
|
||||
b
|
||||
for b in content
|
||||
if not _is_empty_text_block(b) and not is_empty_thinking_block(b) and not is_encrypted_reasoning_block(b)
|
||||
]
|
||||
filtered = [b for b in content if not _is_empty_text_block(b) and not is_empty_thinking_block(b)]
|
||||
if len(filtered) == len(content):
|
||||
out.append(m)
|
||||
elif filtered:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from ...common_utils import (
|
|||
AnthropicModelInfo,
|
||||
optionally_handle_anthropic_oauth,
|
||||
strip_advisor_blocks_from_messages,
|
||||
strip_encrypted_reasoning_blocks_from_anthropic_messages,
|
||||
)
|
||||
|
||||
DEFAULT_ANTHROPIC_API_VERSION: Final = "2023-06-01"
|
||||
|
|
@ -613,7 +614,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
messages = strip_advisor_blocks_from_messages(messages)
|
||||
|
||||
anthropic_messages_request: Final[AnthropicMessagesRequest] = AnthropicMessagesRequest(
|
||||
messages=messages,
|
||||
messages=strip_encrypted_reasoning_blocks_from_anthropic_messages(messages),
|
||||
max_tokens=max_tokens,
|
||||
model=model,
|
||||
**anthropic_messages_optional_request_params,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
encrypted_reasoning_signature,
|
||||
)
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
|
||||
AnthropicMessagesConfig,
|
||||
)
|
||||
|
||||
|
||||
def _transform(messages):
|
||||
return AnthropicMessagesConfig().transform_anthropic_messages_request(
|
||||
model="claude-sonnet-4-5",
|
||||
messages=messages,
|
||||
anthropic_messages_optional_request_params={"max_tokens": 1024},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
def test_reasoning_replayed_from_the_responses_bridge_never_reaches_anthropic():
|
||||
"""Claude Code resumed on a Claude model echoes the thinking blocks a gpt turn produced."""
|
||||
messages = [
|
||||
{"role": "user", "content": "Solve it."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "plan", "signature": encrypted_reasoning_signature("gAAAA_1")},
|
||||
{"type": "redacted_thinking", "data": encrypted_reasoning_signature("gAAAA_2")},
|
||||
{"type": "text", "text": "The answer."},
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "And the next one?"},
|
||||
]
|
||||
request = _transform(messages)
|
||||
assert request["messages"][1]["content"] == [{"type": "text", "text": "The answer."}]
|
||||
assert len(messages[1]["content"]) == 3
|
||||
|
||||
|
||||
def test_anthropic_signed_thinking_blocks_are_forwarded_untouched():
|
||||
messages = [
|
||||
{"role": "user", "content": "Solve it."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "plan", "signature": "EqQBCkYIAxgCIkA_anthropic_signed"},
|
||||
{"type": "redacted_thinking", "data": "EmwKAhgBEgy_anthropic_minted"},
|
||||
{"type": "text", "text": "The answer."},
|
||||
],
|
||||
},
|
||||
]
|
||||
assert _transform(messages)["messages"] == messages
|
||||
|
|
@ -42,12 +42,15 @@ FAKE_AUTH_TOKEN = "sk-ant-aut01-fake-auth-token-for-testing-123456789"
|
|||
def test_is_claude_code_one_shot_subagent_request(messages, system, expected):
|
||||
from litellm.llms.anthropic.common_utils import is_claude_code_one_shot_subagent_request
|
||||
|
||||
assert is_claude_code_one_shot_subagent_request(
|
||||
messages=messages,
|
||||
system=system,
|
||||
tools=None,
|
||||
user_agent="claude-cli/2.1.263 (external, cli)",
|
||||
) is expected
|
||||
assert (
|
||||
is_claude_code_one_shot_subagent_request(
|
||||
messages=messages,
|
||||
system=system,
|
||||
tools=None,
|
||||
user_agent="claude-cli/2.1.263 (external, cli)",
|
||||
)
|
||||
is expected
|
||||
)
|
||||
|
||||
|
||||
class TestOptionallyHandleAnthropicOAuth:
|
||||
|
|
@ -1541,8 +1544,8 @@ class TestAnthropicThinkingSignatureSelfHeal:
|
|||
out = strip_empty_content_blocks_from_anthropic_messages(msgs)
|
||||
assert [b["type"] for b in out[0]["content"]] == ["thinking"]
|
||||
|
||||
def test_strip_drops_encrypted_reasoning_blocks_from_the_responses_bridge(self):
|
||||
"""A session resumed on an Anthropic model replays reasoning only OpenAI can verify."""
|
||||
def test_strip_keeps_encrypted_reasoning_blocks_for_the_responses_bridge(self):
|
||||
"""The /v1/messages handler runs this before dispatch, so the bridge must still see the replay."""
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
encrypted_reasoning_signature,
|
||||
)
|
||||
|
|
@ -1556,14 +1559,46 @@ class TestAnthropicThinkingSignatureSelfHeal:
|
|||
"content": [
|
||||
{"type": "thinking", "thinking": "plan", "signature": encrypted_reasoning_signature("gAAAA_1")},
|
||||
{"type": "redacted_thinking", "data": encrypted_reasoning_signature("gAAAA_2")},
|
||||
{"type": "redacted_thinking", "data": "EmwKAhgBEgy_anthropic_minted"},
|
||||
{"type": "text", "text": "The answer."},
|
||||
],
|
||||
}
|
||||
]
|
||||
out = strip_empty_content_blocks_from_anthropic_messages(msgs)
|
||||
assert [b["type"] for b in out[0]["content"]] == ["redacted_thinking", "text"]
|
||||
assert len(msgs[0]["content"]) == 4
|
||||
assert strip_empty_content_blocks_from_anthropic_messages(msgs) == msgs
|
||||
|
||||
def test_strip_encrypted_reasoning_drops_only_the_bridge_tagged_blocks(self):
|
||||
"""A session resumed on an Anthropic model replays reasoning only OpenAI can verify."""
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
encrypted_reasoning_signature,
|
||||
)
|
||||
from litellm.llms.anthropic.common_utils import (
|
||||
strip_encrypted_reasoning_blocks_from_anthropic_messages,
|
||||
)
|
||||
|
||||
msgs = [
|
||||
{"role": "user", "content": "Solve it."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "plan", "signature": encrypted_reasoning_signature("gAAAA_1")},
|
||||
{"type": "redacted_thinking", "data": encrypted_reasoning_signature("gAAAA_2")},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "plan", "signature": encrypted_reasoning_signature("gAAAA_3")},
|
||||
{"type": "thinking", "thinking": "native", "signature": "EqQBCkYIAxgCIkA_anthropic_signed"},
|
||||
{"type": "redacted_thinking", "data": "EmwKAhgBEgy_anthropic_minted"},
|
||||
{"type": "text", "text": "The answer."},
|
||||
],
|
||||
},
|
||||
]
|
||||
out = strip_encrypted_reasoning_blocks_from_anthropic_messages(msgs)
|
||||
assert [m["role"] for m in out] == ["user", "assistant"]
|
||||
assert [b["type"] for b in out[1]["content"]] == ["thinking", "redacted_thinking", "text"]
|
||||
assert out[1]["content"][0]["signature"] == "EqQBCkYIAxgCIkA_anthropic_signed"
|
||||
assert len(msgs[1]["content"]) == 2
|
||||
assert len(msgs[2]["content"]) == 4
|
||||
|
||||
def test_strip_empty_text_blocks_treats_null_text_as_empty(self):
|
||||
from litellm.llms.anthropic.common_utils import (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue