Skip to content

Commit e989031

Browse files
authored
fix(google): handle thoughtSignature differently for Gemini 2.5 vs 3 (#6890)
Signed-off-by: rabi <ramishra@redhat.com>
1 parent ce95e04 commit e989031

1 file changed

Lines changed: 82 additions & 13 deletions

File tree

crates/goose/src/providers/formats/google.rs

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
235254
fn 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

Comments
 (0)