39
39
ResponseReasoningItemParam ,
40
40
)
41
41
from openai .types .responses .response_input_param import FunctionCallOutput , ItemReference , Message
42
- from openai .types .responses .response_reasoning_item import Summary
42
+ from openai .types .responses .response_reasoning_item import Content , Summary
43
43
44
44
from ..agent_output import AgentOutputSchemaBase
45
45
from ..exceptions import AgentsException , UserError
@@ -93,24 +93,38 @@ def convert_response_format(
93
93
def message_to_output_items (cls , message : ChatCompletionMessage ) -> list [TResponseOutputItem ]:
94
94
items : list [TResponseOutputItem ] = []
95
95
96
- # Handle reasoning content if available
96
+ # Check if message is agents.extentions.models.litellm_model.InternalChatCompletionMessage
97
+ # We can't actually import it here because litellm is an optional dependency
98
+ # So we use hasattr to check for reasoning_content and thinking_blocks
97
99
if hasattr (message , "reasoning_content" ) and message .reasoning_content :
98
100
reasoning_item = ResponseReasoningItem (
99
101
id = FAKE_RESPONSES_ID ,
100
102
summary = [Summary (text = message .reasoning_content , type = "summary_text" )],
101
103
type = "reasoning" ,
102
104
)
103
105
104
- # Store full thinking blocks for Anthropic compatibility
106
+ # Store thinking blocks for Anthropic compatibility
105
107
if hasattr (message , "thinking_blocks" ) and message .thinking_blocks :
106
- # Store thinking blocks in the reasoning item's content
107
- # Convert thinking blocks to Content objects
108
- from openai .types .responses .response_reasoning_item import Content
109
-
110
- reasoning_item .content = [
111
- Content (text = str (block .get ("thinking" , "" )), type = "reasoning_text" )
112
- for block in message .thinking_blocks
113
- ]
108
+ # Store thinking text in content and signature in encrypted_content
109
+ reasoning_item .content = []
110
+ signature = None
111
+ for block in message .thinking_blocks :
112
+ if isinstance (block , dict ):
113
+ thinking_text = block .get ("thinking" , "" )
114
+ if thinking_text :
115
+ reasoning_item .content .append (
116
+ Content (text = thinking_text , type = "reasoning_text" )
117
+ )
118
+ # Store the signature if present
119
+ if block .get ("signature" ):
120
+ signature = block .get ("signature" )
121
+
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
114
128
115
129
items .append (reasoning_item )
116
130
@@ -301,10 +315,18 @@ def extract_all_content(
301
315
def items_to_messages (
302
316
cls ,
303
317
items : str | Iterable [TResponseInputItem ],
318
+ preserve_thinking_blocks : bool = False ,
304
319
) -> list [ChatCompletionMessageParam ]:
305
320
"""
306
321
Convert a sequence of 'Item' objects into a list of ChatCompletionMessageParam.
307
322
323
+ Args:
324
+ items: A string or iterable of response input items to convert
325
+ preserve_thinking_blocks: Whether to preserve thinking blocks in tool calls
326
+ for reasoning models like Claude 4 Sonnet/Opus which support interleaved
327
+ thinking. When True, thinking blocks are reconstructed and included in
328
+ assistant messages with tool calls.
329
+
308
330
Rules:
309
331
- EasyInputMessage or InputMessage (role=user) => ChatCompletionUserMessageParam
310
332
- EasyInputMessage or InputMessage (role=system) => ChatCompletionSystemMessageParam
@@ -325,6 +347,7 @@ def items_to_messages(
325
347
326
348
result : list [ChatCompletionMessageParam ] = []
327
349
current_assistant_msg : ChatCompletionAssistantMessageParam | None = None
350
+ pending_thinking_blocks : list [dict [str , str ]] | None = None
328
351
329
352
def flush_assistant_message () -> None :
330
353
nonlocal current_assistant_msg
@@ -336,10 +359,11 @@ def flush_assistant_message() -> None:
336
359
current_assistant_msg = None
337
360
338
361
def ensure_assistant_message () -> ChatCompletionAssistantMessageParam :
339
- nonlocal current_assistant_msg
362
+ nonlocal current_assistant_msg , pending_thinking_blocks
340
363
if current_assistant_msg is None :
341
364
current_assistant_msg = ChatCompletionAssistantMessageParam (role = "assistant" )
342
365
current_assistant_msg ["tool_calls" ] = []
366
+
343
367
return current_assistant_msg
344
368
345
369
for item in items :
@@ -455,6 +479,13 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
455
479
456
480
elif func_call := cls .maybe_function_tool_call (item ):
457
481
asst = ensure_assistant_message ()
482
+
483
+ # If we have pending thinking blocks, use them as the content
484
+ # This is required for Anthropic API tool calls with interleaved thinking
485
+ if pending_thinking_blocks :
486
+ asst ["content" ] = pending_thinking_blocks # type: ignore
487
+ pending_thinking_blocks = None # Clear after using
488
+
458
489
tool_calls = list (asst .get ("tool_calls" , []))
459
490
arguments = func_call ["arguments" ] if func_call ["arguments" ] else "{}"
460
491
new_tool_call = ChatCompletionMessageFunctionToolCallParam (
@@ -483,9 +514,28 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
483
514
f"Encountered an item_reference, which is not supported: { item_ref } "
484
515
)
485
516
486
- # 7) reasoning message => not handled
487
- elif cls .maybe_reasoning_message (item ):
488
- pass
517
+ # 7) reasoning message => extract thinking blocks if present
518
+ elif reasoning_item := cls .maybe_reasoning_message (item ):
519
+ # Reconstruct thinking blocks from content (text) and encrypted_content (signature)
520
+ content_items = reasoning_item .get ("content" , [])
521
+ signature = reasoning_item .get ("encrypted_content" )
522
+
523
+ if content_items and preserve_thinking_blocks :
524
+ # Reconstruct thinking blocks from content and signature
525
+ pending_thinking_blocks = []
526
+ for content_item in content_items :
527
+ if (
528
+ isinstance (content_item , dict )
529
+ and content_item .get ("type" ) == "reasoning_text"
530
+ ):
531
+ thinking_block = {
532
+ "type" : "thinking" ,
533
+ "thinking" : content_item .get ("text" , "" ),
534
+ }
535
+ # Add signature if available
536
+ if signature :
537
+ thinking_block ["signature" ] = signature
538
+ pending_thinking_blocks .append (thinking_block )
489
539
490
540
# 8) If we haven't recognized it => fail or ignore
491
541
else :
0 commit comments