@@ -113,6 +113,58 @@ def _build_reasoning_item(
113113 }
114114
115115
116+ def _reasoning_item_from_output_item (item : object ) -> _BuiltReasoningItem | None :
117+ from openai .types .responses import ResponseReasoningItem
118+
119+ if isinstance (item , ResponseReasoningItem ):
120+ return _build_reasoning_item (
121+ item_id = item .id ,
122+ encrypted_content = getattr (item , "encrypted_content" , None ),
123+ summary_raw = item .summary ,
124+ )
125+ if isinstance (item , dict ) and item .get ("type" ) == "reasoning" :
126+ return _build_reasoning_item (
127+ item_id = item .get ("id" , "" ),
128+ encrypted_content = item .get ("encrypted_content" ),
129+ summary_raw = item .get ("summary" ),
130+ )
131+ return None
132+
133+
134+ def _reasoning_items_from_output_items (output_items : Sequence [object ]) -> tuple [_BuiltReasoningItem , ...]:
135+ return tuple (
136+ reasoning_item
137+ for reasoning_item in (_reasoning_item_from_output_item (item ) for item in output_items )
138+ if reasoning_item is not None
139+ )
140+
141+
142+ def _as_chat_reasoning_items (
143+ reasoning_items : Sequence [_BuiltReasoningItem ],
144+ ) -> list [ChatCompletionReasoningItem ] | None :
145+ if not reasoning_items :
146+ return None
147+ # cast-ok: _BuiltReasoningItem is the structural shape ChatCompletionReasoningItem
148+ # describes, and TypedDict invariance is what stops the two from unifying here.
149+ return cast (list [ChatCompletionReasoningItem ], list (reasoning_items ))
150+
151+
152+ def _map_incomplete_reason_to_finish_reason (incomplete_reason : str | None ) -> Literal ["length" , "content_filter" ]:
153+ if incomplete_reason == "content_filter" :
154+ return "content_filter"
155+ return "length"
156+
157+
158+ def _incomplete_reason_from_response_payload (response_payload : object ) -> str | None :
159+ if not isinstance (response_payload , Mapping ):
160+ return None
161+ incomplete_details : Final = response_payload .get ("incomplete_details" )
162+ if not isinstance (incomplete_details , Mapping ):
163+ return None
164+ reason : Final = incomplete_details .get ("reason" )
165+ return reason if isinstance (reason , str ) else None
166+
167+
116168class _ChatToolCallDict (ChatCompletionToolCallChunk , total = False ):
117169 provider_specific_fields : Mapping [str , object ]
118170
@@ -657,6 +709,27 @@ def _convert_response_output_to_choices(
657709
658710 return choices
659711
712+ @staticmethod
713+ def _build_empty_incomplete_choice (
714+ output_items : Sequence [object ],
715+ finish_reason : Literal ["length" , "content_filter" ],
716+ ) -> "Choices" :
717+ from litellm .types .utils import Choices , Message
718+
719+ reasoning_items : Final = _reasoning_items_from_output_items (output_items )
720+ reasoning_content : Final = " " .join (
721+ summary_block ["text" ]
722+ for reasoning_item in reasoning_items
723+ for summary_block in reasoning_item ["summary" ]
724+ if summary_block .get ("text" )
725+ )
726+ message : Final = Message (
727+ content = "" ,
728+ reasoning_content = reasoning_content if reasoning_content else None ,
729+ reasoning_items = _as_chat_reasoning_items (reasoning_items ),
730+ )
731+ return Choices (message = message , finish_reason = finish_reason , index = 0 )
732+
660733 @classmethod
661734 def _extract_output_from_completed_event (cls , parsed_chunk : Mapping [str , object ]) -> list [dict [str , object ]] | None :
662735 response_payload : Final = parsed_chunk .get ("response" )
@@ -763,11 +836,22 @@ def transform_response(
763836 handle_raw_dict_callback = self ._handle_raw_dict_response_item ,
764837 )
765838
766- if len (choices ) == 0 :
767- if raw_response .incomplete_details is not None and raw_response .incomplete_details .reason is not None :
768- raise ValueError (f"{ model } unable to complete request: { raw_response .incomplete_details .reason } " )
839+ response_is_incomplete : Final = raw_response .status == "incomplete" or (
840+ raw_response .incomplete_details is not None and raw_response .incomplete_details .reason is not None
841+ )
842+
843+ if len (choices ) == 0 and not response_is_incomplete :
844+ raise ValueError (f"Unknown items in responses API response: { output_items } " )
845+
846+ if response_is_incomplete :
847+ incomplete_finish_reason : Final = _map_incomplete_reason_to_finish_reason (
848+ raw_response .incomplete_details .reason if raw_response .incomplete_details is not None else None
849+ )
850+ if len (choices ) == 0 :
851+ choices .append (self ._build_empty_incomplete_choice (output_items , incomplete_finish_reason ))
769852 else :
770- raise ValueError (f"Unknown items in responses API response: { output_items } " )
853+ for choice in choices :
854+ choice .finish_reason = incomplete_finish_reason
771855
772856 setattr (model_response , "choices" , choices )
773857
@@ -1392,12 +1476,7 @@ def translate_responses_chunk_to_openai_stream(
13921476 )
13931477 ]
13941478 )
1395- elif event_type == "response.completed" :
1396- # Response is fully complete - now we can signal is_finished=True
1397- # This ensures we don't prematurely end the stream before tool_calls arrive
1398-
1399- # Check if response contains function_call items in output
1400- # to determine correct finish_reason
1479+ elif event_type in ("response.completed" , "response.incomplete" ):
14011480 response_data : Final = parsed_chunk .get ("response" , {})
14021481 output_items : Final = response_data .get ("output" , []) if response_data else []
14031482
@@ -1407,25 +1486,14 @@ def translate_responses_chunk_to_openai_stream(
14071486 if isinstance (item , dict )
14081487 )
14091488
1410- finish_reason : Final = "tool_calls" if has_function_calls else "stop"
1489+ finish_reason : Final = (
1490+ _map_incomplete_reason_to_finish_reason (_incomplete_reason_from_response_payload (response_data ))
1491+ if event_type == "response.incomplete"
1492+ else ("tool_calls" if has_function_calls else "stop" )
1493+ )
14111494
1412- # Extract reasoning items with encrypted_content for round-tripping
1413- completed_reasoning_items : list [_BuiltReasoningItem ] | None = None
1414- for item in output_items :
1415- if not isinstance (item , dict ) or item .get ("type" ) != "reasoning" :
1416- continue
1417- if completed_reasoning_items is None :
1418- completed_reasoning_items = []
1419- completed_reasoning_items .append (
1420- _build_reasoning_item (
1421- item_id = item .get ("id" , "" ),
1422- encrypted_content = item .get ("encrypted_content" ),
1423- summary_raw = item .get ("summary" ),
1424- )
1425- )
1426- completed_reasoning_items_typed : Final = cast (
1427- list [ChatCompletionReasoningItem ] | None ,
1428- completed_reasoning_items ,
1495+ terminal_reasoning_items_typed : Final = _as_chat_reasoning_items (
1496+ _reasoning_items_from_output_items (output_items )
14291497 )
14301498
14311499 usage = None
@@ -1439,7 +1507,7 @@ def translate_responses_chunk_to_openai_stream(
14391507 index = 0 ,
14401508 delta = Delta (
14411509 content = "" ,
1442- reasoning_items = completed_reasoning_items_typed ,
1510+ reasoning_items = terminal_reasoning_items_typed ,
14431511 ),
14441512 finish_reason = finish_reason ,
14451513 )
0 commit comments