@@ -232,6 +232,25 @@ pub fn process_response_part(
232232 process_response_part_impl ( part, last_signature, handling)
233233}
234234
235+ /// Gemini 2.x includes thoughtSignature on first chunk as metadata, not actual thinking.
236+ fn process_response_part_for_model (
237+ part : & Value ,
238+ last_signature : & mut Option < String > ,
239+ model_version : Option < & str > ,
240+ ) -> Option < MessageContent > {
241+ let is_gemini_2 = model_version
242+ . map ( |m| m. starts_with ( "gemini-2" ) )
243+ . unwrap_or ( false ) ;
244+
245+ let has_signature = part. get ( THOUGHT_SIGNATURE_KEY ) . is_some ( ) ;
246+ let handling = if has_signature && !is_gemini_2 {
247+ SignedTextHandling :: SignedTextAsThinking
248+ } else {
249+ SignedTextHandling :: SignedTextAsRegularText
250+ } ;
251+ process_response_part_impl ( part, last_signature, handling)
252+ }
253+
235254fn process_response_part_non_streaming (
236255 part : & Value ,
237256 last_signature : & mut Option < String > ,
@@ -461,6 +480,8 @@ where
461480 }
462481 }
463482
483+ let model_version = chunk. get( "modelVersion" ) . and_then( |v| v. as_str( ) ) ;
484+
464485 let parts = chunk
465486 . get( "candidates" )
466487 . and_then( |v| v. as_array( ) )
@@ -471,7 +492,7 @@ where
471492
472493 if let Some ( parts) = parts {
473494 for part in parts {
474- if let Some ( content) = process_response_part ( part, & mut last_signature) {
495+ if let Some ( content) = process_response_part_for_model ( part, & mut last_signature, model_version ) {
475496 let message = Message :: new(
476497 Role :: Assistant ,
477498 chrono:: Utc :: now( ) . timestamp( ) ,
@@ -1072,18 +1093,71 @@ mod tests {
10721093 async fn test_streaming_with_thought_signature ( ) {
10731094 use futures:: StreamExt ;
10741095
1075- let signed_stream = concat ! (
1096+ let gemini3_stream = concat ! (
10761097 r#"data: {"candidates": [{"content": {"role": "model", "# ,
1077- r#""parts": [{"text": "Begin", "thoughtSignature": "sig123"}]}}]}"# ,
1098+ r#""parts": [{"text": "Begin", "thoughtSignature": "sig123"}]}}], "# ,
1099+ r#""modelVersion": "gemini-3-pro"}"# ,
10781100 "\n " ,
10791101 r#"data: {"candidates": [{"content": {"role": "model", "# ,
1080- r#""parts": [{"text": " middle"}]}}]}"# ,
1102+ r#""parts": [{"text": " end"}]}}], "modelVersion": "gemini-3-pro"}"#
1103+ ) ;
1104+ let lines: Vec < Result < String , anyhow:: Error > > =
1105+ gemini3_stream. lines ( ) . map ( |l| Ok ( l. to_string ( ) ) ) . collect ( ) ;
1106+ let stream = Box :: pin ( futures:: stream:: iter ( lines) ) ;
1107+ let mut message_stream = std:: pin:: pin!( response_to_streaming_message( stream) ) ;
1108+
1109+ let mut text_parts = Vec :: new ( ) ;
1110+ let mut thinking_parts = Vec :: new ( ) ;
1111+
1112+ while let Some ( result) = message_stream. next ( ) . await {
1113+ let ( message, _usage) = result. unwrap ( ) ;
1114+ if let Some ( msg) = message {
1115+ match msg. content . first ( ) {
1116+ Some ( MessageContent :: Text ( text) ) => text_parts. push ( text. text . clone ( ) ) ,
1117+ Some ( MessageContent :: Thinking ( t) ) => thinking_parts. push ( t. thinking . clone ( ) ) ,
1118+ _ => { }
1119+ }
1120+ }
1121+ }
1122+
1123+ assert_eq ! ( thinking_parts, vec![ "Begin" ] ) ;
1124+ assert_eq ! ( text_parts, vec![ " end" ] ) ;
1125+
1126+ let gemini25_stream = concat ! (
1127+ r#"data: {"candidates": [{"content": {"role": "model", "# ,
1128+ r#""parts": [{"text": "Begin", "thoughtSignature": "sig123"}]}}], "# ,
1129+ r#""modelVersion": "gemini-2.5-pro"}"# ,
1130+ "\n " ,
1131+ r#"data: {"candidates": [{"content": {"role": "model", "# ,
1132+ r#""parts": [{"text": " end"}]}}], "modelVersion": "gemini-2.5-pro"}"#
1133+ ) ;
1134+ let lines: Vec < Result < String , anyhow:: Error > > =
1135+ gemini25_stream. lines ( ) . map ( |l| Ok ( l. to_string ( ) ) ) . collect ( ) ;
1136+ let stream = Box :: pin ( futures:: stream:: iter ( lines) ) ;
1137+ let mut message_stream = std:: pin:: pin!( response_to_streaming_message( stream) ) ;
1138+
1139+ let mut text_parts = Vec :: new ( ) ;
1140+
1141+ while let Some ( result) = message_stream. next ( ) . await {
1142+ let ( message, _usage) = result. unwrap ( ) ;
1143+ if let Some ( msg) = message {
1144+ if let Some ( MessageContent :: Text ( text) ) = msg. content . first ( ) {
1145+ text_parts. push ( text. text . clone ( ) ) ;
1146+ }
1147+ }
1148+ }
1149+
1150+ assert_eq ! ( text_parts, vec![ "Begin" , " end" ] ) ;
1151+
1152+ let unknown_stream = concat ! (
1153+ r#"data: {"candidates": [{"content": {"role": "model", "# ,
1154+ r#""parts": [{"text": "Begin", "thoughtSignature": "sig123"}]}}]}"# ,
10811155 "\n " ,
10821156 r#"data: {"candidates": [{"content": {"role": "model", "# ,
10831157 r#""parts": [{"text": " end"}]}}]}"#
10841158 ) ;
10851159 let lines: Vec < Result < String , anyhow:: Error > > =
1086- signed_stream . lines ( ) . map ( |l| Ok ( l. to_string ( ) ) ) . collect ( ) ;
1160+ unknown_stream . lines ( ) . map ( |l| Ok ( l. to_string ( ) ) ) . collect ( ) ;
10871161 let stream = Box :: pin ( futures:: stream:: iter ( lines) ) ;
10881162 let mut message_stream = std:: pin:: pin!( response_to_streaming_message( stream) ) ;
10891163
@@ -1094,20 +1168,15 @@ mod tests {
10941168 let ( message, _usage) = result. unwrap ( ) ;
10951169 if let Some ( msg) = message {
10961170 match msg. content . first ( ) {
1097- Some ( MessageContent :: Text ( text) ) => {
1098- text_parts. push ( text. text . clone ( ) ) ;
1099- }
1100- Some ( MessageContent :: Thinking ( thinking) ) => {
1101- thinking_parts. push ( thinking. thinking . clone ( ) ) ;
1102- assert_eq ! ( thinking. signature, "sig123" ) ;
1103- }
1171+ Some ( MessageContent :: Text ( text) ) => text_parts. push ( text. text . clone ( ) ) ,
1172+ Some ( MessageContent :: Thinking ( t) ) => thinking_parts. push ( t. thinking . clone ( ) ) ,
11041173 _ => { }
11051174 }
11061175 }
11071176 }
11081177
11091178 assert_eq ! ( thinking_parts, vec![ "Begin" ] ) ;
1110- assert_eq ! ( text_parts, vec![ " middle" , " end"] ) ;
1179+ assert_eq ! ( text_parts, vec![ " end" ] ) ;
11111180 }
11121181
11131182 #[ tokio:: test]
0 commit comments