@@ -205,6 +205,35 @@ def split_json_objects(raw: str) -> list[str]:
205205 return expanded
206206
207207
208+ def _merge_reasoning_details (details : list , chunk : object ) -> None :
209+ """Merge reasoning_details delta items into a list in-place.
210+
211+ Items sharing an integer index are merged: text and summary deltas
212+ concatenate when string-valued, other fields overwrite with the latest
213+ delta. Items without a usable index are appended in arrival order.
214+ """
215+ items = chunk if isinstance (chunk , list ) else ([chunk ] if isinstance (chunk , dict ) else [])
216+ for item in items :
217+ if not isinstance (item , dict ):
218+ continue
219+ idx = item .get ('index' )
220+ existing = None
221+ if isinstance (idx , int ) and idx >= 0 :
222+ existing = next ((e for e in details if e .get ('index' ) == idx ), None )
223+ if existing is None :
224+ new_entry = dict (item )
225+ new_entry .setdefault ('type' , 'reasoning.text' )
226+ details .append (new_entry )
227+ continue
228+ for k , v in item .items ():
229+ if k in ('text' , 'summary' ):
230+ if isinstance (v , str ):
231+ base = existing .get (k )
232+ existing [k ] = (base + v ) if isinstance (base , str ) else v
233+ else :
234+ existing [k ] = v
235+
236+
208237def get_citation_source_from_tool_result (
209238 tool_name : str , tool_params : dict , tool_result : str , tool_id : str = ''
210239) -> list [dict ]:
@@ -509,6 +538,8 @@ def serialize_output(output: list) -> str:
509538 pass
510539
511540 reasoning_content = '' .join (reasoning_parts ).strip ()
541+ if not reasoning_content :
542+ continue # no displayable text; item stays in output for round-trip
512543
513544 duration = item .get ('duration' )
514545 status = item .get ('status' , 'in_progress' )
@@ -3581,15 +3612,43 @@ async def non_streaming_chat_response_handler(response, ctx):
35813612 # otherwise generate from response content
35823613 response_output = response_data .get ('output' )
35833614 if not response_output :
3584- response_output = [
3615+ message_obj = choices [0 ].get ('message' , {})
3616+ reasoning_text = (
3617+ message_obj .get ('reasoning_content' )
3618+ or message_obj .get ('reasoning' )
3619+ )
3620+ reasoning_details = message_obj .get ('reasoning_details' )
3621+
3622+ response_output = []
3623+
3624+ if reasoning_text or reasoning_details :
3625+ r_item = {
3626+ 'type' : 'reasoning' ,
3627+ 'id' : output_id ('r' ),
3628+ 'status' : 'completed' ,
3629+ 'start_tag' : '<think>' ,
3630+ 'end_tag' : '</think>' ,
3631+ 'attributes' : {'type' : 'reasoning_content' },
3632+ 'content' : [{'type' : 'output_text' , 'text' : reasoning_text }] if reasoning_text else [],
3633+ 'summary' : None ,
3634+ }
3635+ if reasoning_details :
3636+ r_item ['reasoning_details' ] = (
3637+ reasoning_details
3638+ if isinstance (reasoning_details , list )
3639+ else [reasoning_details ]
3640+ )
3641+ response_output .append (r_item )
3642+
3643+ response_output .append (
35853644 {
35863645 'type' : 'message' ,
35873646 'id' : output_id ('msg' ),
35883647 'status' : 'completed' ,
35893648 'role' : 'assistant' ,
35903649 'content' : [{'type' : 'output_text' , 'text' : content }],
35913650 }
3592- ]
3651+ )
35933652
35943653 await event_emitter (
35953654 {
@@ -3939,6 +3998,8 @@ def set_last_text(out, text):
39393998
39403999 # Initialize output: use existing from message if continuing, else create new
39414000 existing_output = message .get ('output' ) if message else None
4001+ _pending_reasoning_details = []
4002+
39424003 if existing_output :
39434004 output = existing_output
39444005 else :
@@ -4322,9 +4383,14 @@ async def flush_pending_delta_data(threshold: int = 0):
43224383 or delta .get ('reasoning' )
43234384 or delta .get ('thinking' )
43244385 )
4386+ reasoning_details_chunk = delta .get ('reasoning_details' )
4387+
4388+ # Only create a reasoning item for visible reasoning text.
4389+ # Details-only deltas (e.g. Gemini encrypted blobs) are
4390+ # buffered to avoid splitting the assistant message mid-stream.
43254391 if reasoning_content :
43264392 if not output or output [- 1 ].get ('type' ) != 'reasoning' :
4327- reasoning_item = {
4393+ output . append ( {
43284394 'type' : 'reasoning' ,
43294395 'id' : output_id ('r' ),
43304396 'status' : 'in_progress' ,
@@ -4334,23 +4400,37 @@ async def flush_pending_delta_data(threshold: int = 0):
43344400 'content' : [],
43354401 'summary' : None ,
43364402 'started_at' : time .time (),
4337- }
4338- output .append (reasoning_item )
4339- else :
4340- reasoning_item = output [- 1 ]
4403+ })
43414404
4342- # Append to reasoning content
4405+ if reasoning_content :
4406+ reasoning_item = output [- 1 ]
43434407 parts = reasoning_item .get ('content' , [])
43444408 if parts and parts [- 1 ].get ('type' ) == 'output_text' :
43454409 parts [- 1 ]['text' ] += reasoning_content
43464410 else :
4347- reasoning_item ['content' ] = [
4348- {
4349- 'type' : 'output_text' ,
4350- 'text' : reasoning_content ,
4351- }
4352- ]
4411+ reasoning_item ['content' ] = [{'type' : 'output_text' , 'text' : reasoning_content }]
43534412
4413+ # Flush any buffered details-only chunks into this reasoning item.
4414+ if _pending_reasoning_details :
4415+ _merge_reasoning_details (
4416+ reasoning_item .setdefault ('reasoning_details' , []),
4417+ _pending_reasoning_details ,
4418+ )
4419+ _pending_reasoning_details .clear ()
4420+
4421+ # Accumulate raw structured reasoning_details for provider round-trip.
4422+ if reasoning_details_chunk :
4423+ if output and output [- 1 ].get ('type' ) == 'reasoning' :
4424+ _merge_reasoning_details (
4425+ output [- 1 ].setdefault ('reasoning_details' , []),
4426+ reasoning_details_chunk ,
4427+ )
4428+ else :
4429+ # Buffer until a safe boundary (end-of-stream or real reasoning text).
4430+ items = reasoning_details_chunk if isinstance (reasoning_details_chunk , list ) else [reasoning_details_chunk ]
4431+ _pending_reasoning_details .extend (items )
4432+
4433+ if reasoning_content or reasoning_details_chunk :
43544434 data = {'content' : serialize_output (full_output ())}
43554435
43564436 if value :
@@ -4566,6 +4646,30 @@ async def flush_pending_delta_data(threshold: int = 0):
45664646 )
45674647 reasoning_item ['status' ] = 'completed'
45684648
4649+ # Flush any buffered reasoning_details that never found a reasoning item.
4650+ if _pending_reasoning_details :
4651+ target = next ((item for item in output if item .get ('type' ) == 'reasoning' ), None )
4652+ if target is None :
4653+ target = {
4654+ 'type' : 'reasoning' ,
4655+ 'id' : output_id ('r' ),
4656+ 'status' : 'completed' ,
4657+ 'start_tag' : '<think>' ,
4658+ 'end_tag' : '</think>' ,
4659+ 'attributes' : {'type' : 'reasoning_content' },
4660+ 'content' : [],
4661+ 'summary' : None ,
4662+ 'started_at' : time .time (),
4663+ 'ended_at' : time .time (),
4664+ 'duration' : 0 ,
4665+ }
4666+ output .insert (0 , target )
4667+ _merge_reasoning_details (
4668+ target .setdefault ('reasoning_details' , []),
4669+ _pending_reasoning_details ,
4670+ )
4671+ _pending_reasoning_details .clear ()
4672+
45694673 if response_tool_calls :
45704674 tool_calls .append (_split_tool_calls (response_tool_calls ))
45714675
0 commit comments