@@ -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 = []
@@ -4131,9 +4194,14 @@ async def flush_pending_delta_data(threshold: int = 0):
41314194 or delta .get ('reasoning' )
41324195 or delta .get ('thinking' )
41334196 )
4197+ reasoning_details_chunk = delta .get ('reasoning_details' )
4198+
4199+ # Only create a reasoning item for visible reasoning text.
4200+ # Details-only deltas (e.g. Gemini encrypted blobs) are
4201+ # buffered to avoid splitting the assistant message mid-stream.
41344202 if reasoning_content :
41354203 if not output or output [- 1 ].get ('type' ) != 'reasoning' :
4136- reasoning_item = {
4204+ output . append ( {
41374205 'type' : 'reasoning' ,
41384206 'id' : output_id ('r' ),
41394207 'status' : 'in_progress' ,
@@ -4143,23 +4211,37 @@ async def flush_pending_delta_data(threshold: int = 0):
41434211 'content' : [],
41444212 'summary' : None ,
41454213 'started_at' : time .time (),
4146- }
4147- output .append (reasoning_item )
4148- else :
4149- reasoning_item = output [- 1 ]
4214+ })
41504215
4151- # Append to reasoning content
4216+ if reasoning_content :
4217+ reasoning_item = output [- 1 ]
41524218 parts = reasoning_item .get ('content' , [])
41534219 if parts and parts [- 1 ].get ('type' ) == 'output_text' :
41544220 parts [- 1 ]['text' ] += reasoning_content
41554221 else :
4156- reasoning_item ['content' ] = [
4157- {
4158- 'type' : 'output_text' ,
4159- 'text' : reasoning_content ,
4160- }
4161- ]
4222+ reasoning_item ['content' ] = [{'type' : 'output_text' , 'text' : reasoning_content }]
41624223
4224+ # Flush any buffered details-only chunks into this reasoning item.
4225+ if _pending_reasoning_details :
4226+ _merge_reasoning_details (
4227+ reasoning_item .setdefault ('reasoning_details' , []),
4228+ _pending_reasoning_details ,
4229+ )
4230+ _pending_reasoning_details .clear ()
4231+
4232+ # Accumulate raw structured reasoning_details for provider round-trip.
4233+ if reasoning_details_chunk :
4234+ if output and output [- 1 ].get ('type' ) == 'reasoning' :
4235+ _merge_reasoning_details (
4236+ output [- 1 ].setdefault ('reasoning_details' , []),
4237+ reasoning_details_chunk ,
4238+ )
4239+ else :
4240+ # Buffer until a safe boundary (end-of-stream or real reasoning text).
4241+ items = reasoning_details_chunk if isinstance (reasoning_details_chunk , list ) else [reasoning_details_chunk ]
4242+ _pending_reasoning_details .extend (items )
4243+
4244+ if reasoning_content or reasoning_details_chunk :
41634245 data = {'content' : serialize_output (full_output ())}
41644246
41654247 if value :
@@ -4373,6 +4455,30 @@ async def flush_pending_delta_data(threshold: int = 0):
43734455 )
43744456 reasoning_item ['status' ] = 'completed'
43754457
4458+ # Flush any buffered reasoning_details that never found a reasoning item.
4459+ if _pending_reasoning_details :
4460+ target = next ((item for item in output if item .get ('type' ) == 'reasoning' ), None )
4461+ if target is None :
4462+ target = {
4463+ 'type' : 'reasoning' ,
4464+ 'id' : output_id ('r' ),
4465+ 'status' : 'completed' ,
4466+ 'start_tag' : '<think>' ,
4467+ 'end_tag' : '</think>' ,
4468+ 'attributes' : {'type' : 'reasoning_content' },
4469+ 'content' : [],
4470+ 'summary' : None ,
4471+ 'started_at' : time .time (),
4472+ 'ended_at' : time .time (),
4473+ 'duration' : 0 ,
4474+ }
4475+ output .insert (0 , target )
4476+ _merge_reasoning_details (
4477+ target .setdefault ('reasoning_details' , []),
4478+ _pending_reasoning_details ,
4479+ )
4480+ _pending_reasoning_details .clear ()
4481+
43764482 if response_tool_calls :
43774483 tool_calls .append (_split_tool_calls (response_tool_calls ))
43784484
0 commit comments