Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -582,15 +582,43 @@ def translate_anthropic_messages_to_openai(
else:
assistant_content = assistant_message_str

# Gate ``thinking_blocks`` on the target backend. Non-Anthropic
# OpenAI-compatible backends (e.g. Fireworks, DeepSeek) reject
# the Anthropic-specific ``thinking_blocks`` field with "Extra
# inputs are not permitted", breaking multi-turn conversations.
# Only Anthropic Claude backends (including Bedrock ARNs that
# point at Claude) understand the field and its signed
# signatures. For everyone else, drop ``thinking_blocks`` and
# convert to the OpenAI-style ``reasoning_content`` string,
# which those backends accept and consume. When the model is
# unknown (``None``) we conservatively preserve the field to
# keep the prior behaviour. See #27946.
preserve_thinking_blocks = (
model is None or self.is_anthropic_claude_model(model) or self.is_bedrock_arn_model(model)
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
assistant_message = ChatCompletionAssistantMessage(
role="assistant",
content=assistant_content,
thinking_blocks=(thinking_blocks if len(thinking_blocks) > 0 else None),
thinking_blocks=(
thinking_blocks if (preserve_thinking_blocks and len(thinking_blocks) > 0) else None
),
)
if len(tool_calls) > 0:
assistant_message["tool_calls"] = tool_calls # type: ignore
if len(thinking_blocks) > 0:
assistant_message["thinking_blocks"] = thinking_blocks # type: ignore
if preserve_thinking_blocks:
assistant_message["thinking_blocks"] = thinking_blocks # type: ignore
else:
# Concatenate every unredacted thinking block. Redacted
# blocks carry no readable text (only encrypted ``data``)
# and are dropped.
reasoning_content = "".join(
block.get("thinking", "") # type: ignore[union-attr]
for block in thinking_blocks
if block.get("type") == "thinking"
)
if reasoning_content:
assistant_message["reasoning_content"] = reasoning_content # type: ignore
new_messages.append(assistant_message)

return new_messages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,92 @@ def test_translate_anthropic_messages_to_openai_thinking_blocks():
assert result[1]["tool_calls"][0]["id"] == "toolu_01234"


def _thinking_and_redacted_messages():
return [
AnthropicMessagesUserMessageParam(
role="user",
content=[{"type": "text", "text": "What is 2+2?"}],
),
AnthopicMessagesAssistantMessageParam(
role="assistant",
content=[
{
"type": "thinking",
"thinking": "The user asks 2+2.",
"signature": "sig123",
},
{
"type": "redacted_thinking",
"data": "REDACTED",
},
{
"type": "thinking",
"thinking": " It is 4.",
"signature": "sig456",
},
{"type": "text", "text": "4"},
],
),
AnthropicMessagesUserMessageParam(
role="user",
content=[{"type": "text", "text": "Now multiply that by 3."}],
),
]


@pytest.mark.parametrize(
"model", ["glm-4.6", "azure/my-glm-deployment", "deepseek/deepseek-reasoner"]
)
def test_translate_anthropic_messages_to_openai_strips_thinking_blocks_for_non_anthropic(
model,
):
"""Non-Anthropic backends reject the Anthropic-specific ``thinking_blocks``
field ("Extra inputs are not permitted"). The adapter must drop it and
convert the unredacted blocks to a single ``reasoning_content`` string."""

adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_messages_to_openai(
messages=_thinking_and_redacted_messages(), model=model
)

assistant = result[1]
assert assistant["role"] == "assistant"
# raw thinking_blocks must NOT be forwarded to a non-Anthropic backend.
# The key may be present as ``None`` (the constructor default), which
# litellm strips before serializing the request; what matters is that the
# actual blocks are not attached.
assert assistant.get("thinking_blocks") is None
# unredacted blocks are concatenated; the redacted block (no text) is dropped
assert assistant["reasoning_content"] == "The user asks 2+2. It is 4."


@pytest.mark.parametrize(
"model",
[
"claude-sonnet-4",
"anthropic/claude-3-5-sonnet",
"vertex_ai/claude-opus",
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-claude-profile",
],
)
def test_translate_anthropic_messages_to_openai_preserves_thinking_blocks_for_anthropic(
model,
):
"""Anthropic Claude backends understand ``thinking_blocks`` (and their
signed signatures), so the field must be preserved unchanged and
``reasoning_content`` must not be added."""

adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_messages_to_openai(
messages=_thinking_and_redacted_messages(), model=model
)

assistant = result[1]
assert "thinking_blocks" in assistant
assert len(assistant["thinking_blocks"]) == 3
assert "reasoning_content" not in assistant


def test_translate_anthropic_messages_to_openai_tool_message_placement():
"""Test that tool result messages are placed before user messages in the conversation order."""

Expand Down
Loading