Skip to content

Commit d840238

Browse files
committed
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.
1 parent 7a4a22f commit d840238

File tree

2 files changed

+35
-16
lines changed

2 files changed

+35
-16
lines changed

src/agents/models/chatcmpl_converter.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TRespon
107107
if hasattr(message, "thinking_blocks") and message.thinking_blocks:
108108
# Store thinking text in content and signature in encrypted_content
109109
reasoning_item.content = []
110-
signature = None
110+
signatures: list[str] = []
111111
for block in message.thinking_blocks:
112112
if isinstance(block, dict):
113113
thinking_text = block.get("thinking", "")
@@ -116,15 +116,12 @@ def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TRespon
116116
Content(text=thinking_text, type="reasoning_text")
117117
)
118118
# Store the signature if present
119-
if block.get("signature"):
120-
signature = block.get("signature")
119+
if signature := block.get("signature"):
120+
signatures.append(signature)
121121

122-
# Store only the last signature in encrypted_content
123-
# If there are multiple thinking blocks, this should be a problem.
124-
# In practice, there should only be one signature for the entire reasoning step.
125-
# Tested with: claude-sonnet-4-20250514
126-
if signature:
127-
reasoning_item.encrypted_content = signature
122+
# Store the signatures in encrypted_content with newline delimiter
123+
if signatures:
124+
reasoning_item.encrypted_content = "\n".join(signatures)
128125

129126
items.append(reasoning_item)
130127

@@ -518,7 +515,8 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
518515
elif reasoning_item := cls.maybe_reasoning_message(item):
519516
# Reconstruct thinking blocks from content (text) and encrypted_content (signature)
520517
content_items = reasoning_item.get("content", [])
521-
signature = reasoning_item.get("encrypted_content")
518+
encrypted_content = reasoning_item.get("encrypted_content")
519+
signatures = encrypted_content.split("\n") if encrypted_content else []
522520

523521
if content_items and preserve_thinking_blocks:
524522
# Reconstruct thinking blocks from content and signature
@@ -532,9 +530,9 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
532530
"type": "thinking",
533531
"thinking": content_item.get("text", ""),
534532
}
535-
# Add signature if available
536-
if signature:
537-
thinking_block["signature"] = signature
533+
# Add signatures if available
534+
if signatures:
535+
thinking_block["signature"] = signatures.pop(0)
538536
pending_thinking_blocks.append(thinking_block)
539537

540538
# 8) If we haven't recognized it => fail or ignore

tests/test_anthropic_thinking_blocks.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,12 @@ def test_anthropic_thinking_blocks_with_tool_calls():
125125
"Let me use the weather tool to get this information."
126126
),
127127
"signature": "TestSignature123",
128-
}
128+
},
129+
{
130+
"type": "thinking",
131+
"thinking": ("We should use the city Tokyo as the city."),
132+
"signature": "TestSignature456",
133+
},
129134
],
130135
tool_calls=[
131136
ChatCompletionMessageToolCall(
@@ -143,7 +148,7 @@ def test_anthropic_thinking_blocks_with_tool_calls():
143148
reasoning_items = [
144149
item for item in output_items if hasattr(item, "type") and item.type == "reasoning"
145150
]
146-
assert len(reasoning_items) == 1, "Should have exactly one reasoning item"
151+
assert len(reasoning_items) == 1, "Should have exactly two reasoning items"
147152

148153
reasoning_item = reasoning_items[0]
149154

@@ -159,7 +164,9 @@ def test_anthropic_thinking_blocks_with_tool_calls():
159164
assert hasattr(reasoning_item, "encrypted_content"), (
160165
"Reasoning item should have encrypted_content"
161166
)
162-
assert reasoning_item.encrypted_content == "TestSignature123", "Signature should be preserved"
167+
assert reasoning_item.encrypted_content == "TestSignature123\nTestSignature456", (
168+
"Signature should be preserved"
169+
)
163170

164171
# Verify tool calls are present
165172
tool_call_items = [
@@ -210,6 +217,20 @@ def test_anthropic_thinking_blocks_with_tool_calls():
210217
"Signature should be preserved in thinking block"
211218
)
212219

220+
first_content = content[1]
221+
assert first_content.get("type") == "thinking", (
222+
f"Second content must be 'thinking' type for Anthropic compatibility, "
223+
f"but got '{first_content.get('type')}'"
224+
)
225+
expected_thinking = "We should use the city Tokyo as the city."
226+
assert first_content.get("thinking") == expected_thinking, (
227+
"Thinking content should be preserved"
228+
)
229+
# Signature should also be preserved
230+
assert first_content.get("signature") == "TestSignature456", (
231+
"Signature should be preserved in thinking block"
232+
)
233+
213234
# Verify tool calls are preserved
214235
tool_calls = assistant_msg.get("tool_calls", [])
215236
assert len(cast(list[Any], tool_calls)) == 1, "Tool calls should be preserved"

0 commit comments

Comments
 (0)