@@ -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' )
@@ -3411,15 +3445,43 @@ async def non_streaming_chat_response_handler(response, ctx):
34113445 # otherwise generate from response content
34123446 response_output = response_data .get ('output' )
34133447 if not response_output :
3414- response_output = [
3448+ message_obj = choices [0 ].get ('message' , {})
3449+ reasoning_text = (
3450+ message_obj .get ('reasoning_content' )
3451+ or message_obj .get ('reasoning' )
3452+ )
3453+ reasoning_details = message_obj .get ('reasoning_details' )
3454+
3455+ response_output = []
3456+
3457+ if reasoning_text or reasoning_details :
3458+ r_item = {
3459+ 'type' : 'reasoning' ,
3460+ 'id' : output_id ('r' ),
3461+ 'status' : 'completed' ,
3462+ 'start_tag' : '<think>' ,
3463+ 'end_tag' : '</think>' ,
3464+ 'attributes' : {'type' : 'reasoning_content' },
3465+ 'content' : [{'type' : 'output_text' , 'text' : reasoning_text }] if reasoning_text else [],
3466+ 'summary' : None ,
3467+ }
3468+ if reasoning_details :
3469+ r_item ['reasoning_details' ] = (
3470+ reasoning_details
3471+ if isinstance (reasoning_details , list )
3472+ else [reasoning_details ]
3473+ )
3474+ response_output .append (r_item )
3475+
3476+ response_output .append (
34153477 {
34163478 'type' : 'message' ,
34173479 'id' : output_id ('msg' ),
34183480 'status' : 'completed' ,
34193481 'role' : 'assistant' ,
34203482 'content' : [{'type' : 'output_text' , 'text' : content }],
34213483 }
3422- ]
3484+ )
34233485
34243486 await event_emitter (
34253487 {
@@ -3783,6 +3845,7 @@ def set_last_text(out, text):
37833845 ]
37843846 else :
37853847 output = []
3848+ _pending_reasoning_details = []
37863849
37873850 usage = None
37883851 prior_output = []
@@ -4125,9 +4188,14 @@ async def flush_pending_delta_data(threshold: int = 0):
41254188 or delta .get ('reasoning' )
41264189 or delta .get ('thinking' )
41274190 )
4191+ reasoning_details_chunk = delta .get ('reasoning_details' )
4192+
4193+ # Only create a reasoning item for visible reasoning text.
4194+ # Details-only deltas (e.g. Gemini encrypted blobs) are
4195+ # buffered to avoid splitting the assistant message mid-stream.
41284196 if reasoning_content :
41294197 if not output or output [- 1 ].get ('type' ) != 'reasoning' :
4130- reasoning_item = {
4198+ output . append ( {
41314199 'type' : 'reasoning' ,
41324200 'id' : output_id ('r' ),
41334201 'status' : 'in_progress' ,
@@ -4137,23 +4205,37 @@ async def flush_pending_delta_data(threshold: int = 0):
41374205 'content' : [],
41384206 'summary' : None ,
41394207 'started_at' : time .time (),
4140- }
4141- output .append (reasoning_item )
4142- else :
4143- reasoning_item = output [- 1 ]
4208+ })
41444209
4145- # Append to reasoning content
4210+ if reasoning_content :
4211+ reasoning_item = output [- 1 ]
41464212 parts = reasoning_item .get ('content' , [])
41474213 if parts and parts [- 1 ].get ('type' ) == 'output_text' :
41484214 parts [- 1 ]['text' ] += reasoning_content
41494215 else :
4150- reasoning_item ['content' ] = [
4151- {
4152- 'type' : 'output_text' ,
4153- 'text' : reasoning_content ,
4154- }
4155- ]
4216+ reasoning_item ['content' ] = [{'type' : 'output_text' , 'text' : reasoning_content }]
41564217
4218+ # Flush any buffered details-only chunks into this reasoning item.
4219+ if _pending_reasoning_details :
4220+ _merge_reasoning_details (
4221+ reasoning_item .setdefault ('reasoning_details' , []),
4222+ _pending_reasoning_details ,
4223+ )
4224+ _pending_reasoning_details .clear ()
4225+
4226+ # Accumulate raw structured reasoning_details for provider round-trip.
4227+ if reasoning_details_chunk :
4228+ if output and output [- 1 ].get ('type' ) == 'reasoning' :
4229+ _merge_reasoning_details (
4230+ output [- 1 ].setdefault ('reasoning_details' , []),
4231+ reasoning_details_chunk ,
4232+ )
4233+ else :
4234+ # Buffer until a safe boundary (end-of-stream or real reasoning text).
4235+ items = reasoning_details_chunk if isinstance (reasoning_details_chunk , list ) else [reasoning_details_chunk ]
4236+ _pending_reasoning_details .extend (items )
4237+
4238+ if reasoning_content or reasoning_details_chunk :
41574239 data = {'content' : serialize_output (full_output ())}
41584240
41594241 if value :
@@ -4367,6 +4449,30 @@ async def flush_pending_delta_data(threshold: int = 0):
43674449 )
43684450 reasoning_item ['status' ] = 'completed'
43694451
4452+ # Flush any buffered reasoning_details that never found a reasoning item.
4453+ if _pending_reasoning_details :
4454+ target = next ((item for item in output if item .get ('type' ) == 'reasoning' ), None )
4455+ if target is None :
4456+ target = {
4457+ 'type' : 'reasoning' ,
4458+ 'id' : output_id ('r' ),
4459+ 'status' : 'completed' ,
4460+ 'start_tag' : '<think>' ,
4461+ 'end_tag' : '</think>' ,
4462+ 'attributes' : {'type' : 'reasoning_content' },
4463+ 'content' : [],
4464+ 'summary' : None ,
4465+ 'started_at' : time .time (),
4466+ 'ended_at' : time .time (),
4467+ 'duration' : 0 ,
4468+ }
4469+ output .insert (0 , target )
4470+ _merge_reasoning_details (
4471+ target .setdefault ('reasoning_details' , []),
4472+ _pending_reasoning_details ,
4473+ )
4474+ _pending_reasoning_details .clear ()
4475+
43704476 if response_tool_calls :
43714477 tool_calls .append (_split_tool_calls (response_tool_calls ))
43724478
0 commit comments