@@ -218,6 +218,38 @@ def split_json_objects(raw: str) -> list[str]:
218218 return expanded
219219
220220
221+ def _merge_reasoning_details (details : list , chunk : object ) -> None :
222+ """Merge reasoning_details delta items into a list in-place, keyed by index.
223+
224+ Items sharing the same index are merged: text and summary fields are
225+ concatenated; all other fields are overwritten with the latest value.
226+ chunk may be a list of items or a single dict.
227+ """
228+ items = chunk if isinstance (chunk , list ) else ([chunk ] if isinstance (chunk , dict ) else [])
229+ for item in items :
230+ if not isinstance (item , dict ):
231+ continue
232+ idx = item .get ('index' , 0 )
233+ if not isinstance (idx , int ) or idx < 0 :
234+ continue
235+ while len (details ) <= idx :
236+ details .append ({})
237+ existing = details [idx ]
238+ existing ['type' ] = item .get ('type' , existing .get ('type' , 'reasoning.text' ))
239+ if 'format' in item :
240+ existing ['format' ] = item ['format' ]
241+ if 'id' in item :
242+ existing ['id' ] = item ['id' ]
243+ if item .get ('text' ):
244+ existing ['text' ] = existing .get ('text' , '' ) + item ['text' ]
245+ if item .get ('summary' ):
246+ existing ['summary' ] = existing .get ('summary' , '' ) + item ['summary' ]
247+ if 'data' in item :
248+ existing ['data' ] = item ['data' ]
249+ if item .get ('signature' ):
250+ existing ['signature' ] = item ['signature' ]
251+
252+
221253def get_citation_source_from_tool_result (
222254 tool_name : str , tool_params : dict , tool_result : str , tool_id : str = ''
223255) -> list [dict ]:
@@ -524,6 +556,8 @@ def serialize_output(output: list) -> str:
524556 pass
525557
526558 reasoning_content = '' .join (reasoning_parts ).strip ()
559+ if not reasoning_content :
560+ continue # no displayable text; item stays in output for round-trip
527561
528562 duration = item .get ('duration' )
529563 status = item .get ('status' , 'in_progress' )
@@ -3421,15 +3455,43 @@ async def non_streaming_chat_response_handler(response, ctx):
34213455 # otherwise generate from response content
34223456 response_output = response_data .get ('output' )
34233457 if not response_output :
3424- response_output = [
3458+ message_obj = choices [0 ].get ('message' , {})
3459+ reasoning_text = (
3460+ message_obj .get ('reasoning_content' )
3461+ or message_obj .get ('reasoning' )
3462+ )
3463+ reasoning_details = message_obj .get ('reasoning_details' )
3464+
3465+ response_output = []
3466+
3467+ if reasoning_text or reasoning_details :
3468+ r_item = {
3469+ 'type' : 'reasoning' ,
3470+ 'id' : output_id ('r' ),
3471+ 'status' : 'completed' ,
3472+ 'start_tag' : '<think>' ,
3473+ 'end_tag' : '</think>' ,
3474+ 'attributes' : {'type' : 'reasoning_content' },
3475+ 'content' : [{'type' : 'output_text' , 'text' : reasoning_text }] if reasoning_text else [],
3476+ 'summary' : None ,
3477+ }
3478+ if reasoning_details :
3479+ r_item ['reasoning_details' ] = (
3480+ reasoning_details
3481+ if isinstance (reasoning_details , list )
3482+ else [reasoning_details ]
3483+ )
3484+ response_output .append (r_item )
3485+
3486+ response_output .append (
34253487 {
34263488 'type' : 'message' ,
34273489 'id' : output_id ('msg' ),
34283490 'status' : 'completed' ,
34293491 'role' : 'assistant' ,
34303492 'content' : [{'type' : 'output_text' , 'text' : content }],
34313493 }
3432- ]
3494+ )
34333495
34343496 await event_emitter (
34353497 {
@@ -3793,6 +3855,7 @@ def set_last_text(out, text):
37933855 ]
37943856 else :
37953857 output = []
3858+ _pending_reasoning_details = []
37963859
37973860 usage = None
37983861 prior_output = []
@@ -4141,9 +4204,14 @@ async def flush_pending_delta_data(threshold: int = 0):
41414204 or delta .get ('reasoning' )
41424205 or delta .get ('thinking' )
41434206 )
4207+ reasoning_details_chunk = delta .get ('reasoning_details' )
4208+
4209+ # Only create a reasoning item for visible reasoning text.
4210+ # Details-only deltas (e.g. Gemini encrypted blobs) are
4211+ # buffered to avoid splitting the assistant message mid-stream.
41444212 if reasoning_content :
41454213 if not output or output [- 1 ].get ('type' ) != 'reasoning' :
4146- reasoning_item = {
4214+ output . append ( {
41474215 'type' : 'reasoning' ,
41484216 'id' : output_id ('r' ),
41494217 'status' : 'in_progress' ,
@@ -4153,23 +4221,37 @@ async def flush_pending_delta_data(threshold: int = 0):
41534221 'content' : [],
41544222 'summary' : None ,
41554223 'started_at' : time .time (),
4156- }
4157- output .append (reasoning_item )
4158- else :
4159- reasoning_item = output [- 1 ]
4224+ })
41604225
4161- # Append to reasoning content
4226+ if reasoning_content :
4227+ reasoning_item = output [- 1 ]
41624228 parts = reasoning_item .get ('content' , [])
41634229 if parts and parts [- 1 ].get ('type' ) == 'output_text' :
41644230 parts [- 1 ]['text' ] += reasoning_content
41654231 else :
4166- reasoning_item ['content' ] = [
4167- {
4168- 'type' : 'output_text' ,
4169- 'text' : reasoning_content ,
4170- }
4171- ]
4232+ reasoning_item ['content' ] = [{'type' : 'output_text' , 'text' : reasoning_content }]
41724233
4234+ # Flush any buffered details-only chunks into this reasoning item.
4235+ if _pending_reasoning_details :
4236+ _merge_reasoning_details (
4237+ reasoning_item .setdefault ('reasoning_details' , []),
4238+ _pending_reasoning_details ,
4239+ )
4240+ _pending_reasoning_details .clear ()
4241+
4242+ # Accumulate raw structured reasoning_details for provider round-trip.
4243+ if reasoning_details_chunk :
4244+ if output and output [- 1 ].get ('type' ) == 'reasoning' :
4245+ _merge_reasoning_details (
4246+ output [- 1 ].setdefault ('reasoning_details' , []),
4247+ reasoning_details_chunk ,
4248+ )
4249+ else :
4250+ # Buffer until a safe boundary (end-of-stream or real reasoning text).
4251+ items = reasoning_details_chunk if isinstance (reasoning_details_chunk , list ) else [reasoning_details_chunk ]
4252+ _pending_reasoning_details .extend (items )
4253+
4254+ if reasoning_content or reasoning_details_chunk :
41734255 data = {'content' : serialize_output (full_output ())}
41744256
41754257 if value :
@@ -4383,6 +4465,30 @@ async def flush_pending_delta_data(threshold: int = 0):
43834465 )
43844466 reasoning_item ['status' ] = 'completed'
43854467
4468+ # Flush any buffered reasoning_details that never found a reasoning item.
4469+ if _pending_reasoning_details :
4470+ target = next ((item for item in output if item .get ('type' ) == 'reasoning' ), None )
4471+ if target is None :
4472+ target = {
4473+ 'type' : 'reasoning' ,
4474+ 'id' : output_id ('r' ),
4475+ 'status' : 'completed' ,
4476+ 'start_tag' : '<think>' ,
4477+ 'end_tag' : '</think>' ,
4478+ 'attributes' : {'type' : 'reasoning_content' },
4479+ 'content' : [],
4480+ 'summary' : None ,
4481+ 'started_at' : time .time (),
4482+ 'ended_at' : time .time (),
4483+ 'duration' : 0 ,
4484+ }
4485+ output .insert (0 , target )
4486+ _merge_reasoning_details (
4487+ target .setdefault ('reasoning_details' , []),
4488+ _pending_reasoning_details ,
4489+ )
4490+ _pending_reasoning_details .clear ()
4491+
43864492 if response_tool_calls :
43874493 tool_calls .append (_split_tool_calls (response_tool_calls ))
43884494
0 commit comments