From d8402383f0260f7a40ffe4023dae864fca68102d Mon Sep 17 00:00:00 2001 From: Sung-jin Brian Hong Date: Mon, 22 Sep 2025 14:33:56 +0900 Subject: [PATCH] fix: handle multiple thinking blocks with signatures in chatcmpl converter Previously only the last signature was preserved when multiple thinking blocks had signatures. Now all signatures are collected and stored with newline delimiters, ensuring proper reconstruction of each thinking block. --- src/agents/models/chatcmpl_converter.py | 24 ++++++++++------------ tests/test_anthropic_thinking_blocks.py | 27 ++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/agents/models/chatcmpl_converter.py b/src/agents/models/chatcmpl_converter.py index 96f02a5fe..0ece1664b 100644 --- a/src/agents/models/chatcmpl_converter.py +++ b/src/agents/models/chatcmpl_converter.py @@ -107,7 +107,7 @@ def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TRespon if hasattr(message, "thinking_blocks") and message.thinking_blocks: # Store thinking text in content and signature in encrypted_content reasoning_item.content = [] - signature = None + signatures: list[str] = [] for block in message.thinking_blocks: if isinstance(block, dict): thinking_text = block.get("thinking", "") @@ -116,15 +116,12 @@ def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TRespon Content(text=thinking_text, type="reasoning_text") ) # Store the signature if present - if block.get("signature"): - signature = block.get("signature") + if signature := block.get("signature"): + signatures.append(signature) - # Store only the last signature in encrypted_content - # If there are multiple thinking blocks, this should be a problem. - # In practice, there should only be one signature for the entire reasoning step. - # Tested with: claude-sonnet-4-20250514 - if signature: - reasoning_item.encrypted_content = signature + # Store the signatures in encrypted_content with newline delimiter + if signatures: + reasoning_item.encrypted_content = "\n".join(signatures) items.append(reasoning_item) @@ -518,7 +515,8 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam: elif reasoning_item := cls.maybe_reasoning_message(item): # Reconstruct thinking blocks from content (text) and encrypted_content (signature) content_items = reasoning_item.get("content", []) - signature = reasoning_item.get("encrypted_content") + encrypted_content = reasoning_item.get("encrypted_content") + signatures = encrypted_content.split("\n") if encrypted_content else [] if content_items and preserve_thinking_blocks: # Reconstruct thinking blocks from content and signature @@ -532,9 +530,9 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam: "type": "thinking", "thinking": content_item.get("text", ""), } - # Add signature if available - if signature: - thinking_block["signature"] = signature + # Add signatures if available + if signatures: + thinking_block["signature"] = signatures.pop(0) pending_thinking_blocks.append(thinking_block) # 8) If we haven't recognized it => fail or ignore diff --git a/tests/test_anthropic_thinking_blocks.py b/tests/test_anthropic_thinking_blocks.py index 933be2c0e..35446efe4 100644 --- a/tests/test_anthropic_thinking_blocks.py +++ b/tests/test_anthropic_thinking_blocks.py @@ -125,7 +125,12 @@ def test_anthropic_thinking_blocks_with_tool_calls(): "Let me use the weather tool to get this information." ), "signature": "TestSignature123", - } + }, + { + "type": "thinking", + "thinking": ("We should use the city Tokyo as the city."), + "signature": "TestSignature456", + }, ], tool_calls=[ ChatCompletionMessageToolCall( @@ -143,7 +148,7 @@ def test_anthropic_thinking_blocks_with_tool_calls(): reasoning_items = [ item for item in output_items if hasattr(item, "type") and item.type == "reasoning" ] - assert len(reasoning_items) == 1, "Should have exactly one reasoning item" + assert len(reasoning_items) == 1, "Should have exactly two reasoning items" reasoning_item = reasoning_items[0] @@ -159,7 +164,9 @@ def test_anthropic_thinking_blocks_with_tool_calls(): assert hasattr(reasoning_item, "encrypted_content"), ( "Reasoning item should have encrypted_content" ) - assert reasoning_item.encrypted_content == "TestSignature123", "Signature should be preserved" + assert reasoning_item.encrypted_content == "TestSignature123\nTestSignature456", ( + "Signature should be preserved" + ) # Verify tool calls are present tool_call_items = [ @@ -210,6 +217,20 @@ def test_anthropic_thinking_blocks_with_tool_calls(): "Signature should be preserved in thinking block" ) + first_content = content[1] + assert first_content.get("type") == "thinking", ( + f"Second content must be 'thinking' type for Anthropic compatibility, " + f"but got '{first_content.get('type')}'" + ) + expected_thinking = "We should use the city Tokyo as the city." + assert first_content.get("thinking") == expected_thinking, ( + "Thinking content should be preserved" + ) + # Signature should also be preserved + assert first_content.get("signature") == "TestSignature456", ( + "Signature should be preserved in thinking block" + ) + # Verify tool calls are preserved tool_calls = assistant_msg.get("tool_calls", []) assert len(cast(list[Any], tool_calls)) == 1, "Tool calls should be preserved"