3939 ResponseReasoningItemParam ,
4040)
4141from 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
4343
4444from ..agent_output import AgentOutputSchemaBase
4545from ..exceptions import AgentsException , UserError
@@ -93,24 +93,38 @@ def convert_response_format(
9393 def message_to_output_items (cls , message : ChatCompletionMessage ) -> list [TResponseOutputItem ]:
9494 items : list [TResponseOutputItem ] = []
9595
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
9799 if hasattr (message , "reasoning_content" ) and message .reasoning_content :
98100 reasoning_item = ResponseReasoningItem (
99101 id = FAKE_RESPONSES_ID ,
100102 summary = [Summary (text = message .reasoning_content , type = "summary_text" )],
101103 type = "reasoning" ,
102104 )
103105
104- # Store full thinking blocks for Anthropic compatibility
106+ # Store thinking blocks for Anthropic compatibility
105107 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
114128
115129 items .append (reasoning_item )
116130
@@ -301,10 +315,18 @@ def extract_all_content(
301315 def items_to_messages (
302316 cls ,
303317 items : str | Iterable [TResponseInputItem ],
318+ preserve_thinking_blocks : bool = False ,
304319 ) -> list [ChatCompletionMessageParam ]:
305320 """
306321 Convert a sequence of 'Item' objects into a list of ChatCompletionMessageParam.
307322
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+
308330 Rules:
309331 - EasyInputMessage or InputMessage (role=user) => ChatCompletionUserMessageParam
310332 - EasyInputMessage or InputMessage (role=system) => ChatCompletionSystemMessageParam
@@ -325,6 +347,7 @@ def items_to_messages(
325347
326348 result : list [ChatCompletionMessageParam ] = []
327349 current_assistant_msg : ChatCompletionAssistantMessageParam | None = None
350+ pending_thinking_blocks : list [dict [str , str ]] | None = None
328351
329352 def flush_assistant_message () -> None :
330353 nonlocal current_assistant_msg
@@ -336,10 +359,11 @@ def flush_assistant_message() -> None:
336359 current_assistant_msg = None
337360
338361 def ensure_assistant_message () -> ChatCompletionAssistantMessageParam :
339- nonlocal current_assistant_msg
362+ nonlocal current_assistant_msg , pending_thinking_blocks
340363 if current_assistant_msg is None :
341364 current_assistant_msg = ChatCompletionAssistantMessageParam (role = "assistant" )
342365 current_assistant_msg ["tool_calls" ] = []
366+
343367 return current_assistant_msg
344368
345369 for item in items :
@@ -455,6 +479,13 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
455479
456480 elif func_call := cls .maybe_function_tool_call (item ):
457481 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+
458489 tool_calls = list (asst .get ("tool_calls" , []))
459490 arguments = func_call ["arguments" ] if func_call ["arguments" ] else "{}"
460491 new_tool_call = ChatCompletionMessageFunctionToolCallParam (
@@ -483,9 +514,28 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
483514 f"Encountered an item_reference, which is not supported: { item_ref } "
484515 )
485516
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 )
489539
490540 # 8) If we haven't recognized it => fail or ignore
491541 else :
0 commit comments