@@ -219,6 +219,35 @@ def split_json_objects(raw: str) -> list[str]:
219219 return expanded
220220
221221
222+ def _merge_reasoning_details (details : list , chunk : object ) -> None :
223+ """Merge reasoning_details delta items into a list in-place.
224+
225+ Items sharing an integer index are merged: text and summary deltas
226+ concatenate when string-valued, other fields overwrite with the latest
227+ delta. Items without a usable index are appended in arrival order.
228+ """
229+ items = chunk if isinstance (chunk , list ) else ([chunk ] if isinstance (chunk , dict ) else [])
230+ for item in items :
231+ if not isinstance (item , dict ):
232+ continue
233+ idx = item .get ('index' )
234+ existing = None
235+ if isinstance (idx , int ) and idx >= 0 :
236+ existing = next ((e for e in details if e .get ('index' ) == idx ), None )
237+ if existing is None :
238+ new_entry = dict (item )
239+ new_entry .setdefault ('type' , 'reasoning.text' )
240+ details .append (new_entry )
241+ continue
242+ for k , v in item .items ():
243+ if k in ('text' , 'summary' ):
244+ if isinstance (v , str ):
245+ base = existing .get (k )
246+ existing [k ] = (base + v ) if isinstance (base , str ) else v
247+ else :
248+ existing [k ] = v
249+
250+
222251def get_citation_source_from_tool_result (
223252 tool_name : str , tool_params : dict , tool_result : str , tool_id : str = ''
224253) -> list [dict ]:
@@ -525,6 +554,8 @@ def serialize_output(output: list) -> str:
525554 pass
526555
527556 reasoning_content = '' .join (reasoning_parts ).strip ()
557+ if not reasoning_content :
558+ continue # no displayable text; item stays in output for round-trip
528559
529560 duration = item .get ('duration' )
530561 status = item .get ('status' , 'in_progress' )
@@ -3473,15 +3504,43 @@ async def non_streaming_chat_response_handler(response, ctx):
34733504 # otherwise generate from response content
34743505 response_output = response_data .get ('output' )
34753506 if not response_output :
3476- response_output = [
3507+ message_obj = choices [0 ].get ('message' , {})
3508+ reasoning_text = (
3509+ message_obj .get ('reasoning_content' )
3510+ or message_obj .get ('reasoning' )
3511+ )
3512+ reasoning_details = message_obj .get ('reasoning_details' )
3513+
3514+ response_output = []
3515+
3516+ if reasoning_text or reasoning_details :
3517+ r_item = {
3518+ 'type' : 'reasoning' ,
3519+ 'id' : output_id ('r' ),
3520+ 'status' : 'completed' ,
3521+ 'start_tag' : '<think>' ,
3522+ 'end_tag' : '</think>' ,
3523+ 'attributes' : {'type' : 'reasoning_content' },
3524+ 'content' : [{'type' : 'output_text' , 'text' : reasoning_text }] if reasoning_text else [],
3525+ 'summary' : None ,
3526+ }
3527+ if reasoning_details :
3528+ r_item ['reasoning_details' ] = (
3529+ reasoning_details
3530+ if isinstance (reasoning_details , list )
3531+ else [reasoning_details ]
3532+ )
3533+ response_output .append (r_item )
3534+
3535+ response_output .append (
34773536 {
34783537 'type' : 'message' ,
34793538 'id' : output_id ('msg' ),
34803539 'status' : 'completed' ,
34813540 'role' : 'assistant' ,
34823541 'content' : [{'type' : 'output_text' , 'text' : content }],
34833542 }
3484- ]
3543+ )
34853544
34863545 await event_emitter (
34873546 {
@@ -3846,6 +3905,7 @@ def set_last_text(out, text):
38463905 ]
38473906 else :
38483907 output = []
3908+ _pending_reasoning_details = []
38493909
38503910 usage = None
38513911 prior_output = []
@@ -4194,9 +4254,14 @@ async def flush_pending_delta_data(threshold: int = 0):
41944254 or delta .get ('reasoning' )
41954255 or delta .get ('thinking' )
41964256 )
4257+ reasoning_details_chunk = delta .get ('reasoning_details' )
4258+
4259+ # Only create a reasoning item for visible reasoning text.
4260+ # Details-only deltas (e.g. Gemini encrypted blobs) are
4261+ # buffered to avoid splitting the assistant message mid-stream.
41974262 if reasoning_content :
41984263 if not output or output [- 1 ].get ('type' ) != 'reasoning' :
4199- reasoning_item = {
4264+ output . append ( {
42004265 'type' : 'reasoning' ,
42014266 'id' : output_id ('r' ),
42024267 'status' : 'in_progress' ,
@@ -4206,23 +4271,37 @@ async def flush_pending_delta_data(threshold: int = 0):
42064271 'content' : [],
42074272 'summary' : None ,
42084273 'started_at' : time .time (),
4209- }
4210- output .append (reasoning_item )
4211- else :
4212- reasoning_item = output [- 1 ]
4274+ })
42134275
4214- # Append to reasoning content
4276+ if reasoning_content :
4277+ reasoning_item = output [- 1 ]
42154278 parts = reasoning_item .get ('content' , [])
42164279 if parts and parts [- 1 ].get ('type' ) == 'output_text' :
42174280 parts [- 1 ]['text' ] += reasoning_content
42184281 else :
4219- reasoning_item ['content' ] = [
4220- {
4221- 'type' : 'output_text' ,
4222- 'text' : reasoning_content ,
4223- }
4224- ]
4282+ reasoning_item ['content' ] = [{'type' : 'output_text' , 'text' : reasoning_content }]
42254283
4284+ # Flush any buffered details-only chunks into this reasoning item.
4285+ if _pending_reasoning_details :
4286+ _merge_reasoning_details (
4287+ reasoning_item .setdefault ('reasoning_details' , []),
4288+ _pending_reasoning_details ,
4289+ )
4290+ _pending_reasoning_details .clear ()
4291+
4292+ # Accumulate raw structured reasoning_details for provider round-trip.
4293+ if reasoning_details_chunk :
4294+ if output and output [- 1 ].get ('type' ) == 'reasoning' :
4295+ _merge_reasoning_details (
4296+ output [- 1 ].setdefault ('reasoning_details' , []),
4297+ reasoning_details_chunk ,
4298+ )
4299+ else :
4300+ # Buffer until a safe boundary (end-of-stream or real reasoning text).
4301+ items = reasoning_details_chunk if isinstance (reasoning_details_chunk , list ) else [reasoning_details_chunk ]
4302+ _pending_reasoning_details .extend (items )
4303+
4304+ if reasoning_content or reasoning_details_chunk :
42264305 data = {'content' : serialize_output (full_output ())}
42274306
42284307 if value :
@@ -4436,6 +4515,30 @@ async def flush_pending_delta_data(threshold: int = 0):
44364515 )
44374516 reasoning_item ['status' ] = 'completed'
44384517
4518+ # Flush any buffered reasoning_details that never found a reasoning item.
4519+ if _pending_reasoning_details :
4520+ target = next ((item for item in output if item .get ('type' ) == 'reasoning' ), None )
4521+ if target is None :
4522+ target = {
4523+ 'type' : 'reasoning' ,
4524+ 'id' : output_id ('r' ),
4525+ 'status' : 'completed' ,
4526+ 'start_tag' : '<think>' ,
4527+ 'end_tag' : '</think>' ,
4528+ 'attributes' : {'type' : 'reasoning_content' },
4529+ 'content' : [],
4530+ 'summary' : None ,
4531+ 'started_at' : time .time (),
4532+ 'ended_at' : time .time (),
4533+ 'duration' : 0 ,
4534+ }
4535+ output .insert (0 , target )
4536+ _merge_reasoning_details (
4537+ target .setdefault ('reasoning_details' , []),
4538+ _pending_reasoning_details ,
4539+ )
4540+ _pending_reasoning_details .clear ()
4541+
44394542 if response_tool_calls :
44404543 tool_calls .append (_split_tool_calls (response_tool_calls ))
44414544
0 commit comments