Skip to content

Commit cb70562

Browse files
committed
fix(chatcmpl): preserve text content when adding Anthropic thinking blocks to tool calls
In the case of Anthropic Claude with extended thinking, this ensures that existing text content is properly converted to content array format before appending thinking blocks, preventing overwriting of original assistant message content. This happens in a rare case where there is a (output text, thinking block, tool call) all in one response. Error message is like below: ``` "litellm.BadRequestError: BedrockException - {\"message\":\"The model returned the following errors: messages.1.content.49: `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified. These blocks must remain as they were in the original response.\"}" ``` The error is misleading because the actual reason is because we didn't provide the text content along side the thinking blocks.
1 parent d91e39c commit cb70562

File tree

2 files changed

+26
-6
lines changed

2 files changed

+26
-6
lines changed

src/agents/models/chatcmpl_converter.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,20 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
480480
# If we have pending thinking blocks, use them as the content
481481
# This is required for Anthropic API tool calls with interleaved thinking
482482
if pending_thinking_blocks:
483-
asst["content"] = pending_thinking_blocks # type: ignore
483+
# If there is a text content, save it to append after thinking blocks
484+
# content type is Union[str, Iterable[ContentArrayOfContentPart], None]
485+
if "content" in asst and isinstance(asst["content"], str):
486+
text_content = ChatCompletionContentPartTextParam(
487+
text=asst["content"], type="text"
488+
)
489+
asst["content"] = [text_content]
490+
491+
if "content" not in asst or asst["content"] is None:
492+
asst["content"] = []
493+
494+
# Thinking blocks MUST come before any other content
495+
# We ignore type errors because pending_thinking_blocks is not openai standard
496+
asst["content"] = pending_thinking_blocks + asst["content"] # type: ignore
484497
pending_thinking_blocks = None # Clear after using
485498

486499
tool_calls = list(asst.get("tool_calls", []))

tests/test_anthropic_thinking_blocks.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,20 +217,27 @@ def test_anthropic_thinking_blocks_with_tool_calls():
217217
"Signature should be preserved in thinking block"
218218
)
219219

220-
first_content = content[1]
221-
assert first_content.get("type") == "thinking", (
220+
second_content = content[1]
221+
assert second_content.get("type") == "thinking", (
222222
f"Second content must be 'thinking' type for Anthropic compatibility, "
223-
f"but got '{first_content.get('type')}'"
223+
f"but got '{second_content.get('type')}'"
224224
)
225225
expected_thinking = "We should use the city Tokyo as the city."
226-
assert first_content.get("thinking") == expected_thinking, (
226+
assert second_content.get("thinking") == expected_thinking, (
227227
"Thinking content should be preserved"
228228
)
229229
# Signature should also be preserved
230-
assert first_content.get("signature") == "TestSignature456", (
230+
assert second_content.get("signature") == "TestSignature456", (
231231
"Signature should be preserved in thinking block"
232232
)
233233

234+
last_content = content[2]
235+
assert last_content.get("type") == "text", (
236+
f"First content must be 'text' type but got '{last_content.get('type')}'"
237+
)
238+
expected_text = "I'll check the weather for you."
239+
assert last_content.get("text") == expected_text, "Content text should be preserved"
240+
234241
# Verify tool calls are preserved
235242
tool_calls = assistant_msg.get("tool_calls", [])
236243
assert len(cast(list[Any], tool_calls)) == 1, "Tool calls should be preserved"

0 commit comments

Comments
 (0)