@@ -65,6 +65,7 @@ class StreamingState:
6565 function_calls : dict [int , ResponseFunctionToolCall ] = field (default_factory = dict )
6666 # Fields for real-time function call streaming
6767 function_call_streaming : dict [int , bool ] = field (default_factory = dict )
68+ # Stable output indexes for function calls, including fallback calls.
6869 function_call_output_idx : dict [int , int ] = field (default_factory = dict )
6970 # Store accumulated thinking text and signature for Anthropic compatibility
7071 thinking_text : str = ""
@@ -145,6 +146,17 @@ def _finish_reasoning_item(
145146 )
146147 state .reasoning_item_done = True
147148
149+ @staticmethod
150+ def _function_call_starting_index (state : StreamingState ) -> int :
151+ starting_index = 0
152+ if state .reasoning_content_index_and_output :
153+ starting_index += 1
154+ if state .text_content_index_and_output :
155+ starting_index += 1
156+ if state .refusal_content_index_and_output :
157+ starting_index += 1
158+ return starting_index
159+
148160 @classmethod
149161 async def handle_stream (
150162 cls ,
@@ -456,6 +468,10 @@ async def handle_stream(
456468 call_id = "" ,
457469 )
458470 state .function_call_streaming [tc_delta .index ] = False
471+ state .function_call_output_idx [tc_delta .index ] = (
472+ cls ._function_call_starting_index (state )
473+ + len (state .function_call_output_idx )
474+ )
459475
460476 tc_function = tc_delta .function
461477
@@ -527,25 +543,10 @@ async def handle_stream(
527543 and function_call .name
528544 and function_call .call_id
529545 ):
530- # Calculate the output index for this function call
531- function_call_starting_index = 0
532- if state .reasoning_content_index_and_output :
533- function_call_starting_index += 1
534- if state .text_content_index_and_output :
535- function_call_starting_index += 1
536- if state .refusal_content_index_and_output :
537- function_call_starting_index += 1
538-
539- # Add offset for already started function calls
540- function_call_starting_index += sum (
541- 1 for streaming in state .function_call_streaming .values () if streaming
542- )
546+ output_index = state .function_call_output_idx [tc_delta .index ]
543547
544- # Mark this function call as streaming and store its output index
548+ # Mark this function call as streaming.
545549 state .function_call_streaming [tc_delta .index ] = True
546- state .function_call_output_idx [tc_delta .index ] = (
547- function_call_starting_index
548- )
549550
550551 # Send initial function call added event
551552 func_call_item = ResponseFunctionToolCall (
@@ -570,7 +571,7 @@ async def handle_stream(
570571 func_call_item .provider_data = merged_provider_data # type: ignore[attr-defined]
571572 yield ResponseOutputItemAddedEvent (
572573 item = func_call_item ,
573- output_index = function_call_starting_index ,
574+ output_index = output_index ,
574575 type = "response.output_item.added" ,
575576 sequence_number = sequence_number .get_and_increment (),
576577 )
@@ -593,12 +594,7 @@ async def handle_stream(
593594 for event in cls ._finish_reasoning_item (state , sequence_number ):
594595 yield event
595596
596- function_call_starting_index = 0
597- if state .reasoning_content_index_and_output :
598- function_call_starting_index += 1
599-
600597 if state .text_content_index_and_output :
601- function_call_starting_index += 1
602598 # Send end event for this content part
603599 yield ResponseContentPartDoneEvent (
604600 content_index = state .text_content_index_and_output [0 ],
@@ -611,7 +607,6 @@ async def handle_stream(
611607 )
612608
613609 if state .refusal_content_index_and_output :
614- function_call_starting_index += 1
615610 # Send end event for this content part
616611 yield ResponseContentPartDoneEvent (
617612 content_index = state .refusal_content_index_and_output [0 ],
@@ -656,18 +651,7 @@ async def handle_stream(
656651 else :
657652 # Function call was not streamed (fallback to old behavior)
658653 # This handles edge cases where function name never arrived
659- fallback_starting_index = 0
660- if state .reasoning_content_index_and_output :
661- fallback_starting_index += 1
662- if state .text_content_index_and_output :
663- fallback_starting_index += 1
664- if state .refusal_content_index_and_output :
665- fallback_starting_index += 1
666-
667- # Add offset for already started function calls
668- fallback_starting_index += sum (
669- 1 for streaming in state .function_call_streaming .values () if streaming
670- )
654+ output_index = state .function_call_output_idx [index ]
671655
672656 # Build function call kwargs, include provider_data if present
673657 fallback_func_call_kwargs : dict [str , Any ] = {
@@ -690,20 +674,20 @@ async def handle_stream(
690674 # Send all events at once (backward compatibility)
691675 yield ResponseOutputItemAddedEvent (
692676 item = ResponseFunctionToolCall (** fallback_func_call_kwargs ),
693- output_index = fallback_starting_index ,
677+ output_index = output_index ,
694678 type = "response.output_item.added" ,
695679 sequence_number = sequence_number .get_and_increment (),
696680 )
697681 yield ResponseFunctionCallArgumentsDeltaEvent (
698682 delta = function_call .arguments ,
699683 item_id = FAKE_RESPONSES_ID ,
700- output_index = fallback_starting_index ,
684+ output_index = output_index ,
701685 type = "response.function_call_arguments.delta" ,
702686 sequence_number = sequence_number .get_and_increment (),
703687 )
704688 yield ResponseOutputItemDoneEvent (
705689 item = ResponseFunctionToolCall (** fallback_func_call_kwargs ),
706- output_index = fallback_starting_index ,
690+ output_index = output_index ,
707691 type = "response.output_item.done" ,
708692 sequence_number = sequence_number .get_and_increment (),
709693 )
0 commit comments