5555 ensure_tool_choice_supports_backend ,
5656)
5757from .fake_id import FAKE_RESPONSES_ID
58+ from .reasoning_content_replay import (
59+ ReasoningContentReplayContext ,
60+ ReasoningContentSource ,
61+ ShouldReplayReasoningContent ,
62+ default_should_replay_reasoning_content ,
63+ )
5864
5965ResponseInputContentWithAudioParam = Union [
6066 ResponseInputContentParam ,
@@ -422,6 +428,8 @@ def items_to_messages(
422428 model : str | None = None ,
423429 preserve_thinking_blocks : bool = False ,
424430 preserve_tool_output_all_content : bool = False ,
431+ base_url : str | None = None ,
432+ should_replay_reasoning_content : ShouldReplayReasoningContent | None = None ,
425433 ) -> list [ChatCompletionMessageParam ]:
426434 """
427435 Convert a sequence of 'Item' objects into a list of ChatCompletionMessageParam.
@@ -441,6 +449,12 @@ def items_to_messages(
441449 When True, all content types including images are preserved. This is useful
442450 for model providers (e.g. Anthropic via LiteLLM) that support processing
443451 non-text content in tool results.
452+ base_url: The request base URL, if the caller knows the concrete endpoint.
453+ This is used by reasoning-content replay hooks to distinguish direct
454+ provider calls from proxy or gateway requests.
455+ should_replay_reasoning_content: Optional hook that decides whether a
456+ reasoning item should be replayed into the next assistant message as
457+ `reasoning_content`.
444458
445459 Rules:
446460 - EasyInputMessage or InputMessage (role=user) => ChatCompletionUserMessageParam
@@ -464,8 +478,9 @@ def items_to_messages(
464478 current_assistant_msg : ChatCompletionAssistantMessageParam | None = None
465479 pending_thinking_blocks : list [dict [str , str ]] | None = None
466480 pending_reasoning_content : str | None = None # For DeepSeek reasoning_content
481+ normalized_base_url = base_url .rstrip ("/" ) if base_url is not None else None
467482
468- def flush_assistant_message () -> None :
483+ def flush_assistant_message (* , clear_pending_reasoning_content : bool = True ) -> None :
469484 nonlocal current_assistant_msg , pending_reasoning_content
470485 if current_assistant_msg is not None :
471486 # The API doesn't support empty arrays for tool_calls
@@ -475,7 +490,15 @@ def flush_assistant_message() -> None:
475490 pending_reasoning_content = None
476491 result .append (current_assistant_msg )
477492 current_assistant_msg = None
478- else :
493+ elif clear_pending_reasoning_content :
494+ pending_reasoning_content = None
495+
496+ def apply_pending_reasoning_content (
497+ assistant_msg : ChatCompletionAssistantMessageParam ,
498+ ) -> None :
499+ nonlocal pending_reasoning_content
500+ if pending_reasoning_content :
501+ assistant_msg ["reasoning_content" ] = pending_reasoning_content # type: ignore[typeddict-unknown-key]
479502 pending_reasoning_content = None
480503
481504 def ensure_assistant_message () -> ChatCompletionAssistantMessageParam :
@@ -485,6 +508,8 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
485508 current_assistant_msg ["content" ] = None
486509 current_assistant_msg ["tool_calls" ] = []
487510
511+ apply_pending_reasoning_content (current_assistant_msg )
512+
488513 return current_assistant_msg
489514
490515 for item in items :
@@ -553,7 +578,9 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
553578
554579 # 3) response output message => assistant
555580 elif resp_msg := cls .maybe_response_output_message (item ):
556- flush_assistant_message ()
581+ # A reasoning item can be followed by an assistant message and then tool calls
582+ # in the same turn, so preserve pending reasoning_content across this flush.
583+ flush_assistant_message (clear_pending_reasoning_content = False )
557584 new_asst = ChatCompletionAssistantMessageParam (role = "assistant" )
558585 contents = resp_msg ["content" ]
559586
@@ -594,6 +621,7 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
594621 pending_thinking_blocks = None # Clear after using
595622
596623 new_asst ["tool_calls" ] = []
624+ apply_pending_reasoning_content (new_asst )
597625 current_assistant_msg = new_asst
598626
599627 # 4) function/file-search calls => attach to assistant
@@ -619,11 +647,6 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
619647 elif func_call := cls .maybe_function_tool_call (item ):
620648 asst = ensure_assistant_message ()
621649
622- # If we have pending reasoning content for DeepSeek, add it to the assistant message
623- if pending_reasoning_content :
624- asst ["reasoning_content" ] = pending_reasoning_content # type: ignore[typeddict-unknown-key]
625- pending_reasoning_content = None # Clear after using
626-
627650 # If we have pending thinking blocks, use them as the content
628651 # This is required for Anthropic API tool calls with interleaved thinking
629652 if pending_thinking_blocks :
@@ -708,6 +731,7 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
708731
709732 item_provider_data : dict [str , Any ] = reasoning_item .get ("provider_data" , {}) # type: ignore[assignment]
710733 item_model = item_provider_data .get ("model" , "" )
734+ should_replay = False
711735
712736 if (
713737 model
@@ -740,17 +764,23 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
740764 # This preserves the original behavior
741765 pending_thinking_blocks = reconstructed_thinking_blocks
742766
743- # DeepSeek requires reasoning_content field in assistant messages with tool calls
744- # Items may not all originate from DeepSeek, so need to check for model match.
745- # For backward compatibility, if provider_data is missing, ignore the check.
746- elif (
747- model
748- and "deepseek" in model . lower ()
749- and (
750- ( item_model and "deepseek" in item_model . lower ())
751- or item_provider_data == {}
767+ if model is not None :
768+ replay_context = ReasoningContentReplayContext (
769+ model = model ,
770+ base_url = normalized_base_url ,
771+ reasoning = ReasoningContentSource (
772+ item = reasoning_item ,
773+ origin_model = item_model or None ,
774+ provider_data = item_provider_data ,
775+ ),
752776 )
753- ):
777+ should_replay = (
778+ should_replay_reasoning_content (replay_context )
779+ if should_replay_reasoning_content is not None
780+ else default_should_replay_reasoning_content (replay_context )
781+ )
782+
783+ if should_replay :
754784 summary_items = reasoning_item .get ("summary" , [])
755785 if summary_items :
756786 reasoning_texts = []
0 commit comments