@@ -204,6 +204,35 @@ def split_json_objects(raw: str) -> list[str]:
204204 return expanded
205205
206206
207+ def _merge_reasoning_details (details : list , chunk : object ) -> None :
208+ """Merge reasoning_details delta items into a list in-place.
209+
210+ Items sharing an integer index are merged: text and summary deltas
211+ concatenate when string-valued, other fields overwrite with the latest
212+ delta. Items without a usable index are appended in arrival order.
213+ """
214+ items = chunk if isinstance (chunk , list ) else ([chunk ] if isinstance (chunk , dict ) else [])
215+ for item in items :
216+ if not isinstance (item , dict ):
217+ continue
218+ idx = item .get ('index' )
219+ existing = None
220+ if isinstance (idx , int ) and idx >= 0 :
221+ existing = next ((e for e in details if e .get ('index' ) == idx ), None )
222+ if existing is None :
223+ new_entry = dict (item )
224+ new_entry .setdefault ('type' , 'reasoning.text' )
225+ details .append (new_entry )
226+ continue
227+ for k , v in item .items ():
228+ if k in ('text' , 'summary' ):
229+ if isinstance (v , str ):
230+ base = existing .get (k )
231+ existing [k ] = (base + v ) if isinstance (base , str ) else v
232+ else :
233+ existing [k ] = v
234+
235+
207236def get_citation_source_from_tool_result (
208237 tool_name : str , tool_params : dict , tool_result : str , tool_id : str = ''
209238) -> list [dict ]:
@@ -508,6 +537,8 @@ def serialize_output(output: list) -> str:
508537 pass
509538
510539 reasoning_content = '' .join (reasoning_parts ).strip ()
540+ if not reasoning_content :
541+ continue # no displayable text; item stays in output for round-trip
511542
512543 duration = item .get ('duration' )
513544 status = item .get ('status' , 'in_progress' )
@@ -3496,15 +3527,43 @@ async def non_streaming_chat_response_handler(response, ctx):
34963527 # otherwise generate from response content
34973528 response_output = response_data .get ('output' )
34983529 if not response_output :
3499- response_output = [
3530+ message_obj = choices [0 ].get ('message' , {})
3531+ reasoning_text = (
3532+ message_obj .get ('reasoning_content' )
3533+ or message_obj .get ('reasoning' )
3534+ )
3535+ reasoning_details = message_obj .get ('reasoning_details' )
3536+
3537+ response_output = []
3538+
3539+ if reasoning_text or reasoning_details :
3540+ r_item = {
3541+ 'type' : 'reasoning' ,
3542+ 'id' : output_id ('r' ),
3543+ 'status' : 'completed' ,
3544+ 'start_tag' : '<think>' ,
3545+ 'end_tag' : '</think>' ,
3546+ 'attributes' : {'type' : 'reasoning_content' },
3547+ 'content' : [{'type' : 'output_text' , 'text' : reasoning_text }] if reasoning_text else [],
3548+ 'summary' : None ,
3549+ }
3550+ if reasoning_details :
3551+ r_item ['reasoning_details' ] = (
3552+ reasoning_details
3553+ if isinstance (reasoning_details , list )
3554+ else [reasoning_details ]
3555+ )
3556+ response_output .append (r_item )
3557+
3558+ response_output .append (
35003559 {
35013560 'type' : 'message' ,
35023561 'id' : output_id ('msg' ),
35033562 'status' : 'completed' ,
35043563 'role' : 'assistant' ,
35053564 'content' : [{'type' : 'output_text' , 'text' : content }],
35063565 }
3507- ]
3566+ )
35083567
35093568 await event_emitter (
35103569 {
@@ -3854,6 +3913,8 @@ def set_last_text(out, text):
38543913
38553914 # Initialize output: use existing from message if continuing, else create new
38563915 existing_output = message .get ('output' ) if message else None
3916+ _pending_reasoning_details = []
3917+
38573918 if existing_output :
38583919 output = existing_output
38593920 else :
@@ -4237,9 +4298,14 @@ async def flush_pending_delta_data(threshold: int = 0):
42374298 or delta .get ('reasoning' )
42384299 or delta .get ('thinking' )
42394300 )
4301+ reasoning_details_chunk = delta .get ('reasoning_details' )
4302+
4303+ # Only create a reasoning item for visible reasoning text.
4304+ # Details-only deltas (e.g. Gemini encrypted blobs) are
4305+ # buffered to avoid splitting the assistant message mid-stream.
42404306 if reasoning_content :
42414307 if not output or output [- 1 ].get ('type' ) != 'reasoning' :
4242- reasoning_item = {
4308+ output . append ( {
42434309 'type' : 'reasoning' ,
42444310 'id' : output_id ('r' ),
42454311 'status' : 'in_progress' ,
@@ -4249,23 +4315,37 @@ async def flush_pending_delta_data(threshold: int = 0):
42494315 'content' : [],
42504316 'summary' : None ,
42514317 'started_at' : time .time (),
4252- }
4253- output .append (reasoning_item )
4254- else :
4255- reasoning_item = output [- 1 ]
4318+ })
42564319
4257- # Append to reasoning content
4320+ if reasoning_content :
4321+ reasoning_item = output [- 1 ]
42584322 parts = reasoning_item .get ('content' , [])
42594323 if parts and parts [- 1 ].get ('type' ) == 'output_text' :
42604324 parts [- 1 ]['text' ] += reasoning_content
42614325 else :
4262- reasoning_item ['content' ] = [
4263- {
4264- 'type' : 'output_text' ,
4265- 'text' : reasoning_content ,
4266- }
4267- ]
4326+ reasoning_item ['content' ] = [{'type' : 'output_text' , 'text' : reasoning_content }]
42684327
4328+ # Flush any buffered details-only chunks into this reasoning item.
4329+ if _pending_reasoning_details :
4330+ _merge_reasoning_details (
4331+ reasoning_item .setdefault ('reasoning_details' , []),
4332+ _pending_reasoning_details ,
4333+ )
4334+ _pending_reasoning_details .clear ()
4335+
4336+ # Accumulate raw structured reasoning_details for provider round-trip.
4337+ if reasoning_details_chunk :
4338+ if output and output [- 1 ].get ('type' ) == 'reasoning' :
4339+ _merge_reasoning_details (
4340+ output [- 1 ].setdefault ('reasoning_details' , []),
4341+ reasoning_details_chunk ,
4342+ )
4343+ else :
4344+ # Buffer until a safe boundary (end-of-stream or real reasoning text).
4345+ items = reasoning_details_chunk if isinstance (reasoning_details_chunk , list ) else [reasoning_details_chunk ]
4346+ _pending_reasoning_details .extend (items )
4347+
4348+ if reasoning_content or reasoning_details_chunk :
42694349 data = {'content' : serialize_output (full_output ())}
42704350
42714351 if value :
@@ -4481,6 +4561,30 @@ async def flush_pending_delta_data(threshold: int = 0):
44814561 )
44824562 reasoning_item ['status' ] = 'completed'
44834563
4564+ # Flush any buffered reasoning_details that never found a reasoning item.
4565+ if _pending_reasoning_details :
4566+ target = next ((item for item in output if item .get ('type' ) == 'reasoning' ), None )
4567+ if target is None :
4568+ target = {
4569+ 'type' : 'reasoning' ,
4570+ 'id' : output_id ('r' ),
4571+ 'status' : 'completed' ,
4572+ 'start_tag' : '<think>' ,
4573+ 'end_tag' : '</think>' ,
4574+ 'attributes' : {'type' : 'reasoning_content' },
4575+ 'content' : [],
4576+ 'summary' : None ,
4577+ 'started_at' : time .time (),
4578+ 'ended_at' : time .time (),
4579+ 'duration' : 0 ,
4580+ }
4581+ output .insert (0 , target )
4582+ _merge_reasoning_details (
4583+ target .setdefault ('reasoning_details' , []),
4584+ _pending_reasoning_details ,
4585+ )
4586+ _pending_reasoning_details .clear ()
4587+
44844588 if response_tool_calls :
44854589 tool_calls .append (_split_tool_calls (response_tool_calls ))
44864590
0 commit comments