Skip to content

Commit e47b47a

Browse files
authored
fix(chatcmpl): preserve all LiteLLM thinking-block signatures in converter (#1779)
1 parent 566b188 commit e47b47a

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)