From 2eba53089572cbbca2fb3851341c15f3cbd2ae44 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 21 Jan 2026 15:11:55 -0700 Subject: [PATCH] feat: Added support for thought-signatures for Gemini 3+ models --- src/client/vertexai.rs | 26 ++++++++++++++++++++++---- src/function/mod.rs | 10 ++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/client/vertexai.rs b/src/client/vertexai.rs index 4cfb90d..dd29072 100644 --- a/src/client/vertexai.rs +++ b/src/client/vertexai.rs @@ -219,7 +219,14 @@ pub async fn gemini_chat_completions_streaming( part["functionCall"]["name"].as_str(), part["functionCall"]["args"].as_object(), ) { - handler.tool_call(ToolCall::new(name.to_string(), json!(args), None))?; + let thought_signature = part["thoughtSignature"] + .as_str() + .or_else(|| part["thought_signature"].as_str()) + .map(|s| s.to_string()); + handler.tool_call( + ToolCall::new(name.to_string(), json!(args), None) + .with_thought_signature(thought_signature), + )?; } } } else if let Some("SAFETY") = data["promptFeedback"]["blockReason"] @@ -280,7 +287,14 @@ fn gemini_extract_chat_completions_text(data: &Value) -> Result { let model_parts: Vec = tool_results.iter().map(|tool_result| { - json!({ + let mut part = json!({ "functionCall": { "name": tool_result.call.name, "args": tool_result.call.arguments, } - }) + }); + if let Some(sig) = &tool_result.call.thought_signature { + part["thoughtSignature"] = json!(sig); + } + part }).collect(); let function_parts: Vec = tool_results.into_iter().map(|tool_result| { json!({ diff --git a/src/function/mod.rs b/src/function/mod.rs index 4307ab1..151d941 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -756,6 +756,10 @@ pub struct ToolCall { pub name: String, pub arguments: Value, pub id: Option, + /// Gemini 3's thought signature for stateful reasoning in function calling. + /// Must be preserved and sent back when submitting function responses. + #[serde(skip_serializing_if = "Option::is_none")] + pub thought_signature: Option, } type CallConfig = (String, String, Vec, HashMap); @@ -785,9 +789,15 @@ impl ToolCall { name, arguments, id, + thought_signature: None, } } + pub fn with_thought_signature(mut self, thought_signature: Option) -> Self { + self.thought_signature = thought_signature; + self + } + pub async fn eval(&self, config: &GlobalConfig) -> Result { let (call_name, cmd_name, mut cmd_args, envs) = match &config.read().agent { Some(agent) => self.extract_call_config_from_agent(config, agent)?,