fix: support Gemini 3's thought signatures (#5806)

Signed-off-by: Salvatore Testa <sal@withpersona.com>
This commit is contained in:
Salvatore Testa
2025-11-19 21:28:27 -08:00
committed by GitHub
parent 5f3a31dcbf
commit cfdf01567d
9 changed files with 64 additions and 6 deletions
+15
View File
@@ -53,6 +53,8 @@ pub struct ToolRequest {
#[serde(with = "tool_result_serde")]
#[schema(value_type = Object)]
pub tool_call: ToolResult<CallToolRequestParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
impl ToolRequest {
@@ -201,6 +203,19 @@ impl MessageContent {
MessageContent::ToolRequest(ToolRequest {
id: id.into(),
tool_call,
thought_signature: None,
})
}
pub fn tool_request_with_signature<S1: Into<String>, S2: Into<String>>(
id: S1,
tool_call: ToolResult<CallToolRequestParam>,
thought_signature: Option<S2>,
) -> Self {
MessageContent::ToolRequest(ToolRequest {
id: id.into(),
tool_call,
thought_signature: thought_signature.map(|s| s.into()),
})
}
+26 -5
View File
@@ -53,9 +53,14 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
}
}
parts.push(json!({
"functionCall": function_call_part
}));
let mut part = Map::new();
part.insert("functionCall".to_string(), json!(function_call_part));
if let Some(signature) = &request.thought_signature {
part.insert("thoughtSignature".to_string(), json!(signature));
}
parts.push(json!(part));
}
Err(e) => {
parts.push(json!({"text":format!("Error: {}", e)}));
@@ -121,6 +126,12 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
}
}
}
MessageContent::Thinking(thinking) => {
let mut part = Map::new();
part.insert("text".to_string(), json!(thinking.thinking));
part.insert("thoughtSignature".to_string(), json!(thinking.signature));
parts.push(json!(part));
}
_ => {}
}
@@ -269,8 +280,17 @@ pub fn response_to_message(response: Value) -> Result<Message> {
.unwrap_or(&binding);
for part in parts {
let thought_signature = part
.get("thoughtSignature")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
content.push(MessageContent::text(text.to_string()));
if let Some(sig) = thought_signature {
content.push(MessageContent::thinking(text.to_string(), sig));
} else {
content.push(MessageContent::text(text.to_string()));
}
} else if let Some(function_call) = part.get("functionCall") {
let id: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
@@ -294,12 +314,13 @@ pub fn response_to_message(response: Value) -> Result<Message> {
} else {
let parameters = function_call.get("args");
if let Some(params) = parameters {
content.push(MessageContent::tool_request(
content.push(MessageContent::tool_request_with_signature(
id,
Ok(CallToolRequestParam {
name: name.into(),
arguments: Some(object(params.clone())),
}),
thought_signature,
));
}
}
+1
View File
@@ -30,6 +30,7 @@ pub const GOOGLE_KNOWN_MODELS: &[&str] = &[
"gemini-2.0-flash-exp",
"gemini-2.0-flash-preview-image-generation",
"gemini-2.0-flash-lite",
"gemini-3-pro-preview",
];
pub const GOOGLE_DOC_URL: &str = "https://ai.google.dev/gemini-api/docs/models";
@@ -113,6 +113,7 @@ mod tests {
name: "shell".into(),
arguments: Some(object!({"command": "rm -rf /"})),
}),
thought_signature: None,
}];
let results = inspector.inspect(&tool_requests, &[]).await.unwrap();
+1
View File
@@ -295,6 +295,7 @@ mod tests {
name: "test_tool".into(),
arguments: Some(object!({})),
}),
thought_signature: None,
};
let permission_result = PermissionCheckResult {