@@ -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 ]:
@@ -511,6 +540,8 @@ def serialize_output(output: list) -> str:
511540 pass
512541
513542 reasoning_content = '' .join (reasoning_parts ).strip ()
543+ if not reasoning_content :
544+ continue # no displayable text; item stays in output for round-trip
514545
515546 duration = item .get ('duration' )
516547 status = item .get ('status' , 'in_progress' )
@@ -3459,15 +3490,43 @@ async def non_streaming_chat_response_handler(response, ctx):
34593490 # otherwise generate from response content
34603491 response_output = response_data .get ('output' )
34613492 if not response_output :
3462- response_output = [
3493+ message_obj = choices [0 ].get ('message' , {})
3494+ reasoning_text = (
3495+ message_obj .get ('reasoning_content' )
3496+ or message_obj .get ('reasoning' )
3497+ )
3498+ reasoning_details = message_obj .get ('reasoning_details' )
3499+
3500+ response_output = []
3501+
3502+ if reasoning_text or reasoning_details :
3503+ r_item = {
3504+ 'type' : 'reasoning' ,
3505+ 'id' : output_id ('r' ),
3506+ 'status' : 'completed' ,
3507+ 'start_tag' : '<think>' ,
3508+ 'end_tag' : '</think>' ,
3509+ 'attributes' : {'type' : 'reasoning_content' },
3510+ 'content' : [{'type' : 'output_text' , 'text' : reasoning_text }] if reasoning_text else [],
3511+ 'summary' : None ,
3512+ }
3513+ if reasoning_details :
3514+ r_item ['reasoning_details' ] = (
3515+ reasoning_details
3516+ if isinstance (reasoning_details , list )
3517+ else [reasoning_details ]
3518+ )
3519+ response_output .append (r_item )
3520+
3521+ response_output .append (
34633522 {
34643523 'type' : 'message' ,
34653524 'id' : output_id ('msg' ),
34663525 'status' : 'completed' ,
34673526 'role' : 'assistant' ,
34683527 'content' : [{'type' : 'output_text' , 'text' : content }],
34693528 }
3470- ]
3529+ )
34713530
34723531 await event_emitter (
34733532 {
@@ -3832,6 +3891,7 @@ def set_last_text(out, text):
38323891 ]
38333892 else :
38343893 output = []
3894+ _pending_reasoning_details = []
38353895
38363896 usage = None
38373897 prior_output = []
@@ -4180,9 +4240,14 @@ async def flush_pending_delta_data(threshold: int = 0):
41804240 or delta .get ('reasoning' )
41814241 or delta .get ('thinking' )
41824242 )
4243+ reasoning_details_chunk = delta .get ('reasoning_details' )
4244+
4245+ # Only create a reasoning item for visible reasoning text.
4246+ # Details-only deltas (e.g. Gemini encrypted blobs) are
4247+ # buffered to avoid splitting the assistant message mid-stream.
41834248 if reasoning_content :
41844249 if not output or output [- 1 ].get ('type' ) != 'reasoning' :
4185- reasoning_item = {
4250+ output . append ( {
41864251 'type' : 'reasoning' ,
41874252 'id' : output_id ('r' ),
41884253 'status' : 'in_progress' ,
@@ -4192,23 +4257,37 @@ async def flush_pending_delta_data(threshold: int = 0):
41924257 'content' : [],
41934258 'summary' : None ,
41944259 'started_at' : time .time (),
4195- }
4196- output .append (reasoning_item )
4197- else :
4198- reasoning_item = output [- 1 ]
4260+ })
41994261
4200- # Append to reasoning content
4262+ if reasoning_content :
4263+ reasoning_item = output [- 1 ]
42014264 parts = reasoning_item .get ('content' , [])
42024265 if parts and parts [- 1 ].get ('type' ) == 'output_text' :
42034266 parts [- 1 ]['text' ] += reasoning_content
42044267 else :
4205- reasoning_item ['content' ] = [
4206- {
4207- 'type' : 'output_text' ,
4208- 'text' : reasoning_content ,
4209- }
4210- ]
4268+ reasoning_item ['content' ] = [{'type' : 'output_text' , 'text' : reasoning_content }]
42114269
4270+ # Flush any buffered details-only chunks into this reasoning item.
4271+ if _pending_reasoning_details :
4272+ _merge_reasoning_details (
4273+ reasoning_item .setdefault ('reasoning_details' , []),
4274+ _pending_reasoning_details ,
4275+ )
4276+ _pending_reasoning_details .clear ()
4277+
4278+ # Accumulate raw structured reasoning_details for provider round-trip.
4279+ if reasoning_details_chunk :
4280+ if output and output [- 1 ].get ('type' ) == 'reasoning' :
4281+ _merge_reasoning_details (
4282+ output [- 1 ].setdefault ('reasoning_details' , []),
4283+ reasoning_details_chunk ,
4284+ )
4285+ else :
4286+ # Buffer until a safe boundary (end-of-stream or real reasoning text).
4287+ items = reasoning_details_chunk if isinstance (reasoning_details_chunk , list ) else [reasoning_details_chunk ]
4288+ _pending_reasoning_details .extend (items )
4289+
4290+ if reasoning_content or reasoning_details_chunk :
42124291 data = {'content' : serialize_output (full_output ())}
42134292
42144293 if value :
@@ -4422,6 +4501,30 @@ async def flush_pending_delta_data(threshold: int = 0):
44224501 )
44234502 reasoning_item ['status' ] = 'completed'
44244503
4504+ # Flush any buffered reasoning_details that never found a reasoning item.
4505+ if _pending_reasoning_details :
4506+ target = next ((item for item in output if item .get ('type' ) == 'reasoning' ), None )
4507+ if target is None :
4508+ target = {
4509+ 'type' : 'reasoning' ,
4510+ 'id' : output_id ('r' ),
4511+ 'status' : 'completed' ,
4512+ 'start_tag' : '<think>' ,
4513+ 'end_tag' : '</think>' ,
4514+ 'attributes' : {'type' : 'reasoning_content' },
4515+ 'content' : [],
4516+ 'summary' : None ,
4517+ 'started_at' : time .time (),
4518+ 'ended_at' : time .time (),
4519+ 'duration' : 0 ,
4520+ }
4521+ output .insert (0 , target )
4522+ _merge_reasoning_details (
4523+ target .setdefault ('reasoning_details' , []),
4524+ _pending_reasoning_details ,
4525+ )
4526+ _pending_reasoning_details .clear ()
4527+
44254528 if response_tool_calls :
44264529 tool_calls .append (_split_tool_calls (response_tool_calls ))
44274530
0 commit comments