1919
2020from google .genai import types
2121
22+ from ..features import FeatureName
23+ from ..features import is_feature_enabled
2224from ..models .llm_response import LlmResponse
2325
2426
@@ -35,6 +37,30 @@ def __init__(self):
3537 self ._usage_metadata = None
3638 self ._response = None
3739
40+ # For progressive SSE streaming mode: accumulate parts in order
41+ self ._parts_sequence : list [types .Part ] = []
42+ self ._current_text_buffer : str = ''
43+ self ._current_text_is_thought : Optional [bool ] = None
44+ self ._finish_reason : Optional [types .FinishReason ] = None
45+
46+ def _flush_text_buffer_to_sequence (self ):
47+ """Flush current text buffer to parts sequence.
48+
49+ This helper is used in progressive SSE mode to maintain part ordering.
50+ It only merges consecutive text parts of the same type (thought or regular).
51+ """
52+ if self ._current_text_buffer :
53+ if self ._current_text_is_thought :
54+ self ._parts_sequence .append (
55+ types .Part (text = self ._current_text_buffer , thought = True )
56+ )
57+ else :
58+ self ._parts_sequence .append (
59+ types .Part .from_text (text = self ._current_text_buffer )
60+ )
61+ self ._current_text_buffer = ''
62+ self ._current_text_is_thought = None
63+
3864 async def process_response (
3965 self , response : types .GenerateContentResponse
4066 ) -> AsyncGenerator [LlmResponse , None ]:
@@ -51,6 +77,42 @@ async def process_response(
5177 self ._response = response
5278 llm_response = LlmResponse .create (response )
5379 self ._usage_metadata = llm_response .usage_metadata
80+
81+ # ========== Progressive SSE Streaming (new feature) ==========
82+ # Save finish_reason for final aggregation
83+ if llm_response .finish_reason :
84+ self ._finish_reason = llm_response .finish_reason
85+
86+ if is_feature_enabled (FeatureName .PROGRESSIVE_SSE_STREAMING ):
87+ # Accumulate parts while preserving their order
88+ # Only merge consecutive text parts of the same type (thought or regular)
89+ if llm_response .content and llm_response .content .parts :
90+ for part in llm_response .content .parts :
91+ if part .text :
92+ # Check if we need to flush the current buffer first
93+ # (when text type changes from thought to regular or vice versa)
94+ if (
95+ self ._current_text_buffer
96+ and part .thought != self ._current_text_is_thought
97+ ):
98+ self ._flush_text_buffer_to_sequence ()
99+
100+ # Accumulate text to buffer
101+ if not self ._current_text_buffer :
102+ self ._current_text_is_thought = part .thought
103+ self ._current_text_buffer += part .text
104+ else :
105+ # Non-text part (function_call, bytes, etc.)
106+ # Flush any buffered text first, then add the non-text part
107+ self ._flush_text_buffer_to_sequence ()
108+ self ._parts_sequence .append (part )
109+
110+ # Mark ALL intermediate chunks as partial
111+ llm_response .partial = True
112+ yield llm_response
113+ return
114+
115+ # ========== Non-Progressive SSE Streaming (old behavior) ==========
54116 if (
55117 llm_response .content
56118 and llm_response .content .parts
@@ -89,6 +151,36 @@ def close(self) -> Optional[LlmResponse]:
89151 Returns:
90152 The aggregated LlmResponse.
91153 """
154+ # ========== Progressive SSE Streaming (new feature) ==========
155+ if is_feature_enabled (FeatureName .PROGRESSIVE_SSE_STREAMING ):
156+ # Always generate final aggregated response in progressive mode
157+ if self ._response and self ._response .candidates :
158+ # Flush any remaining text buffer to complete the sequence
159+ self ._flush_text_buffer_to_sequence ()
160+
161+ # Use the parts sequence which preserves original ordering
162+ final_parts = self ._parts_sequence
163+
164+ if final_parts :
165+ candidate = self ._response .candidates [0 ]
166+ finish_reason = self ._finish_reason or candidate .finish_reason
167+
168+ return LlmResponse (
169+ content = types .ModelContent (parts = final_parts ),
170+ error_code = None
171+ if finish_reason == types .FinishReason .STOP
172+ else finish_reason ,
173+ error_message = None
174+ if finish_reason == types .FinishReason .STOP
175+ else candidate .finish_message ,
176+ usage_metadata = self ._usage_metadata ,
177+ finish_reason = finish_reason ,
178+ partial = False ,
179+ )
180+
181+ return None
182+
183+ # ========== Non-Progressive SSE Streaming (old behavior) ==========
92184 if (
93185 (self ._text or self ._thought_text )
94186 and self ._response
0 commit comments