@@ -227,38 +227,6 @@ enum SignedTextHandling {
227227 SignedTextAsRegularText ,
228228}
229229
230- pub fn process_response_part (
231- part : & Value ,
232- last_signature : & mut Option < String > ,
233- ) -> Option < MessageContent > {
234- let has_signature = part. get ( THOUGHT_SIGNATURE_KEY ) . is_some ( ) ;
235- let handling = if has_signature {
236- SignedTextHandling :: SignedTextAsThinking
237- } else {
238- SignedTextHandling :: SignedTextAsRegularText
239- } ;
240- process_response_part_impl ( part, last_signature, handling)
241- }
242-
243- /// Gemini 2.x includes thoughtSignature on first chunk as metadata, not actual thinking.
244- fn process_response_part_for_model (
245- part : & Value ,
246- last_signature : & mut Option < String > ,
247- model_version : Option < & str > ,
248- ) -> Option < MessageContent > {
249- let is_gemini_2 = model_version
250- . map ( |m| m. starts_with ( "gemini-2" ) )
251- . unwrap_or ( false ) ;
252-
253- let has_signature = part. get ( THOUGHT_SIGNATURE_KEY ) . is_some ( ) ;
254- let handling = if has_signature && !is_gemini_2 {
255- SignedTextHandling :: SignedTextAsThinking
256- } else {
257- SignedTextHandling :: SignedTextAsRegularText
258- } ;
259- process_response_part_impl ( part, last_signature, handling)
260- }
261-
262230fn process_response_part_non_streaming (
263231 part : & Value ,
264232 last_signature : & mut Option < String > ,
@@ -488,8 +456,6 @@ where
488456 }
489457 }
490458
491- let model_version = chunk. get( "modelVersion" ) . and_then( |v| v. as_str( ) ) ;
492-
493459 let parts = chunk
494460 . get( "candidates" )
495461 . and_then( |v| v. as_array( ) )
@@ -500,7 +466,9 @@ where
500466
501467 if let Some ( parts) = parts {
502468 for part in parts {
503- if let Some ( content) = process_response_part_for_model( part, & mut last_signature, model_version) {
469+ // Always emit text as regular text during streaming — we can't
470+ // know yet whether function calls will follow.
471+ if let Some ( content) = process_response_part_impl( part, & mut last_signature, SignedTextHandling :: SignedTextAsRegularText ) {
504472 let message = Message :: new(
505473 Role :: Assistant ,
506474 chrono:: Utc :: now( ) . timestamp( ) ,
@@ -1192,90 +1160,68 @@ mod tests {
11921160 async fn test_streaming_with_thought_signature ( ) {
11931161 use futures:: StreamExt ;
11941162
1195- let gemini3_stream = concat ! (
1196- r#"data: {"candidates": [{"content": {"role": "model", "# ,
1197- r#""parts": [{"text": "Begin", "thoughtSignature": "sig123"}]}}], "# ,
1198- r#""modelVersion": "gemini-3-pro"}"# ,
1199- "\n " ,
1200- r#"data: {"candidates": [{"content": {"role": "model", "# ,
1201- r#""parts": [{"text": " end"}]}}], "modelVersion": "gemini-3-pro"}"#
1202- ) ;
1203- let lines: Vec < Result < String , anyhow:: Error > > =
1204- gemini3_stream. lines ( ) . map ( |l| Ok ( l. to_string ( ) ) ) . collect ( ) ;
1205- let stream = Box :: pin ( futures:: stream:: iter ( lines) ) ;
1206- let mut message_stream = std:: pin:: pin!( response_to_streaming_message( stream) ) ;
1207-
1208- let mut text_parts = Vec :: new ( ) ;
1209- let mut thinking_parts = Vec :: new ( ) ;
1210-
1211- while let Some ( result) = message_stream. next ( ) . await {
1212- let ( message, _usage) = result. unwrap ( ) ;
1213- if let Some ( msg) = message {
1214- match msg. content . first ( ) {
1215- Some ( MessageContent :: Text ( text) ) => text_parts. push ( text. text . clone ( ) ) ,
1216- Some ( MessageContent :: Thinking ( t) ) => thinking_parts. push ( t. thinking . clone ( ) ) ,
1217- _ => { }
1163+ async fn collect_streaming_text ( raw : & str ) -> ( String , usize ) {
1164+ let lines: Vec < Result < String , anyhow:: Error > > =
1165+ raw. lines ( ) . map ( |l| Ok ( l. to_string ( ) ) ) . collect ( ) ;
1166+ let stream = Box :: pin ( futures:: stream:: iter ( lines) ) ;
1167+ let mut msg_stream = std:: pin:: pin!( response_to_streaming_message( stream) ) ;
1168+ let mut text = String :: new ( ) ;
1169+ let mut thinking = 0usize ;
1170+ while let Some ( Ok ( ( message, _) ) ) = msg_stream. next ( ) . await {
1171+ if let Some ( msg) = message {
1172+ for c in & msg. content {
1173+ match c {
1174+ MessageContent :: Text ( t) => text. push_str ( & t. text ) ,
1175+ MessageContent :: Thinking ( _) => thinking += 1 ,
1176+ _ => { }
1177+ }
1178+ }
12181179 }
12191180 }
1181+ ( text, thinking)
12201182 }
12211183
1222- assert_eq ! ( thinking_parts, vec![ "Begin" ] ) ;
1223- assert_eq ! ( text_parts, vec![ " end" ] ) ;
1224-
1225- let gemini25_stream = concat ! (
1184+ // First chunk signed
1185+ let ( text, thinking) = collect_streaming_text ( concat ! (
12261186 r#"data: {"candidates": [{"content": {"role": "model", "# ,
1227- r#""parts": [{"text": "Begin ", "thoughtSignature": "sig123 "}]}}], "# ,
1228- r#""modelVersion": "gemini-2.5-pro "}"# ,
1187+ r#""parts": [{"text": "Hello ", "thoughtSignature": "sig1 "}]}}], "# ,
1188+ r#""modelVersion": "gemini-3-flash-preview "}"# ,
12291189 "\n " ,
12301190 r#"data: {"candidates": [{"content": {"role": "model", "# ,
1231- r#""parts": [{"text": " end"}]}}], "modelVersion": "gemini-2.5-pro"}"#
1232- ) ;
1233- let lines: Vec < Result < String , anyhow:: Error > > =
1234- gemini25_stream. lines ( ) . map ( |l| Ok ( l. to_string ( ) ) ) . collect ( ) ;
1235- let stream = Box :: pin ( futures:: stream:: iter ( lines) ) ;
1236- let mut message_stream = std:: pin:: pin!( response_to_streaming_message( stream) ) ;
1237-
1238- let mut text_parts = Vec :: new ( ) ;
1239-
1240- while let Some ( result) = message_stream. next ( ) . await {
1241- let ( message, _usage) = result. unwrap ( ) ;
1242- if let Some ( msg) = message {
1243- if let Some ( MessageContent :: Text ( text) ) = msg. content . first ( ) {
1244- text_parts. push ( text. text . clone ( ) ) ;
1245- }
1246- }
1247- }
1248-
1249- assert_eq ! ( text_parts, vec![ "Begin" , " end" ] ) ;
1250-
1251- let unknown_stream = concat ! (
1191+ r#""parts": [{"text": " world"}]}}], "modelVersion": "gemini-3-flash-preview"}"#
1192+ ) )
1193+ . await ;
1194+ assert_eq ! ( thinking, 0 ) ;
1195+ assert_eq ! ( text, "Hello world" ) ;
1196+
1197+ // Last chunk signed (the reported truncation bug)
1198+ let ( text, thinking) = collect_streaming_text ( concat ! (
12521199 r#"data: {"candidates": [{"content": {"role": "model", "# ,
1253- r#""parts": [{"text": "Begin", "thoughtSignature": "sig123"}]}}]}"# ,
1200+ r#""parts": [{"text": "SECURITY.md: Project"}]}}], "# ,
1201+ r#""modelVersion": "gemini-3-flash-preview"}"# ,
12541202 "\n " ,
12551203 r#"data: {"candidates": [{"content": {"role": "model", "# ,
1256- r#""parts": [{"text": " end"}]}}]}"#
1257- ) ;
1258- let lines: Vec < Result < String , anyhow:: Error > > =
1259- unknown_stream. lines ( ) . map ( |l| Ok ( l. to_string ( ) ) ) . collect ( ) ;
1260- let stream = Box :: pin ( futures:: stream:: iter ( lines) ) ;
1261- let mut message_stream = std:: pin:: pin!( response_to_streaming_message( stream) ) ;
1262-
1263- let mut text_parts = Vec :: new ( ) ;
1264- let mut thinking_parts = Vec :: new ( ) ;
1265-
1266- while let Some ( result) = message_stream. next ( ) . await {
1267- let ( message, _usage) = result. unwrap ( ) ;
1268- if let Some ( msg) = message {
1269- match msg. content . first ( ) {
1270- Some ( MessageContent :: Text ( text) ) => text_parts. push ( text. text . clone ( ) ) ,
1271- Some ( MessageContent :: Thinking ( t) ) => thinking_parts. push ( t. thinking . clone ( ) ) ,
1272- _ => { }
1273- }
1274- }
1275- }
1276-
1277- assert_eq ! ( thinking_parts, vec![ "Begin" ] ) ;
1278- assert_eq ! ( text_parts, vec![ " end" ] ) ;
1204+ r#""parts": [{"text": " policies.\n\nRead it?", "thoughtSignature": "sig2"}]}}], "# ,
1205+ r#""modelVersion": "gemini-3-flash-preview"}"#
1206+ ) )
1207+ . await ;
1208+ assert_eq ! ( thinking, 0 ) ;
1209+ assert_eq ! ( text, "SECURITY.md: Project policies.\n \n Read it?" ) ;
1210+
1211+ // Intermediate chunk signed
1212+ let ( text, thinking) = collect_streaming_text ( concat ! (
1213+ r#"data: {"candidates": [{"content": {"role": "model", "# ,
1214+ r#""parts": [{"text": "one "}]}}], "modelVersion": "gemini-3-flash-preview"}"# ,
1215+ "\n " ,
1216+ r#"data: {"candidates": [{"content": {"role": "model", "# ,
1217+ r#""parts": [{"text": "two ", "thoughtSignature": "sig3"}]}}], "modelVersion": "gemini-3-flash-preview"}"# ,
1218+ "\n " ,
1219+ r#"data: {"candidates": [{"content": {"role": "model", "# ,
1220+ r#""parts": [{"text": "three"}]}}], "modelVersion": "gemini-3-flash-preview"}"#
1221+ ) )
1222+ . await ;
1223+ assert_eq ! ( thinking, 0 ) ;
1224+ assert_eq ! ( text, "one two three" ) ;
12791225 }
12801226
12811227 #[ tokio:: test]
0 commit comments