fix: preserve Gemini thought_signature for tool call continuations
- Add thought_signature field to ToolCall and FunctionCall structs - Add alias for reasoning_details in OpenRouterMessage - Expand ReasoningContent with format/index fields - Add debug logging for reasoning block tracking Fixes Gemini 3 "Function call is missing a thought_signature" errors
This commit is contained in:
@@ -525,6 +525,30 @@ Use `search_memory` when you encounter a problem you might have solved before or
|
||||
// Add assistant message with tool calls
|
||||
// IMPORTANT: Preserve reasoning blocks for "thinking" models (Gemini 3, etc.)
|
||||
// These contain thought_signature that must be sent back for continuations.
|
||||
|
||||
// Debug: Log if we have reasoning/thought_signature to preserve
|
||||
if let Some(ref reasoning) = response.reasoning {
|
||||
let has_sig = reasoning.iter().any(|r| r.thought_signature.is_some());
|
||||
tracing::debug!(
|
||||
"Preserving {} reasoning blocks (has_thought_signature: {})",
|
||||
reasoning.len(),
|
||||
has_sig
|
||||
);
|
||||
}
|
||||
// Also check for thought_signature in tool_calls themselves (Gemini format)
|
||||
for tc in tool_calls {
|
||||
let has_tc_sig = tc.thought_signature.is_some();
|
||||
let has_fn_sig = tc.function.thought_signature.is_some();
|
||||
if has_tc_sig || has_fn_sig {
|
||||
tracing::debug!(
|
||||
"Tool call '{}' has thought_signature (tool_call: {}, function: {})",
|
||||
tc.function.name,
|
||||
has_tc_sig,
|
||||
has_fn_sig
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(ChatMessage {
|
||||
role: Role::Assistant,
|
||||
content: response.content.clone().map(MessageContent::text),
|
||||
|
||||
@@ -88,17 +88,34 @@ pub enum MessageContent {
|
||||
/// the model to resume its chain of thought.
|
||||
///
|
||||
/// Reference: https://openrouter.ai/docs/use-cases/reasoning-tokens
|
||||
/// Reference: https://ai.google.dev/gemini-api/docs/thought-signatures
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReasoningContent {
|
||||
/// The reasoning/thinking content (may be redacted or empty for some models)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// The reasoning/thinking content (may be redacted or empty for some models).
|
||||
/// OpenRouter uses both `content` and `text` fields depending on the model.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
alias = "text"
|
||||
)]
|
||||
pub content: Option<String>,
|
||||
/// Encrypted thought signature for resuming reasoning (required for Gemini)
|
||||
|
||||
/// Encrypted thought signature for resuming reasoning (required for Gemini 3).
|
||||
/// This MUST be preserved and sent back in subsequent requests for tool call continuations.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thought_signature: Option<String>,
|
||||
/// Type of reasoning block (typically "thinking")
|
||||
|
||||
/// Type of reasoning block (e.g., "thinking", "reasoning.text")
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_type: Option<String>,
|
||||
|
||||
/// Format of the reasoning content (e.g., "unknown")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<String>,
|
||||
|
||||
/// Index of the reasoning block (for ordered reasoning)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub index: Option<u32>,
|
||||
}
|
||||
|
||||
impl ReasoningContent {
|
||||
@@ -209,6 +226,9 @@ pub struct ToolCall {
|
||||
#[serde(rename = "type")]
|
||||
pub call_type: String,
|
||||
pub function: FunctionCall,
|
||||
/// Thought signature for Gemini 3 models (may be at this level instead of function level).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
/// Function call details.
|
||||
@@ -218,6 +238,10 @@ pub struct FunctionCall {
|
||||
/// Arguments as a JSON string. May be empty or missing for no-argument functions.
|
||||
#[serde(default)]
|
||||
pub arguments: String,
|
||||
/// Thought signature for Gemini 3 models. Must be preserved and sent back with tool results.
|
||||
/// When present, this allows Gemini to resume its chain of thought after a tool call.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
/// Tool definition for the LLM.
|
||||
|
||||
@@ -112,11 +112,36 @@ impl OpenRouterClient {
|
||||
.ok_or_else(|| LlmError::parse_error("No choices in response".to_string()))?;
|
||||
|
||||
// Log if we received reasoning blocks (for debugging thinking models)
|
||||
if choice.message.reasoning.is_some() {
|
||||
if let Some(ref reasoning) = choice.message.reasoning {
|
||||
let has_thought_sig = reasoning.iter().any(|r| r.thought_signature.is_some());
|
||||
tracing::debug!(
|
||||
"Received {} reasoning blocks from model",
|
||||
choice.message.reasoning.as_ref().map_or(0, |r| r.len())
|
||||
"Received {} reasoning blocks from model (has_thought_signature: {})",
|
||||
reasoning.len(),
|
||||
has_thought_sig
|
||||
);
|
||||
// Log thought_signature details for debugging Gemini issues
|
||||
for (i, r) in reasoning.iter().enumerate() {
|
||||
if r.thought_signature.is_some() {
|
||||
tracing::debug!("Reasoning block {} has thought_signature", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log if tool calls have thought_signatures attached (Gemini format)
|
||||
if let Some(ref tool_calls) = choice.message.tool_calls {
|
||||
for tc in tool_calls {
|
||||
// Check both ToolCall level and FunctionCall level
|
||||
let has_tc_sig = tc.thought_signature.is_some();
|
||||
let has_fn_sig = tc.function.thought_signature.is_some();
|
||||
if has_tc_sig || has_fn_sig {
|
||||
tracing::debug!(
|
||||
"Tool call '{}' has thought_signature (tool_call: {}, function: {})",
|
||||
tc.function.name,
|
||||
has_tc_sig,
|
||||
has_fn_sig
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ChatResponse {
|
||||
@@ -290,7 +315,8 @@ struct OpenRouterMessage {
|
||||
tool_calls: Option<Vec<ToolCall>>,
|
||||
/// Reasoning blocks from "thinking" models (Gemini 3, etc.)
|
||||
/// Contains thought_signature that must be preserved for tool call continuations.
|
||||
#[serde(default)]
|
||||
/// OpenRouter uses `reasoning` or `reasoning_details` depending on the model/provider.
|
||||
#[serde(default, alias = "reasoning_details")]
|
||||
reasoning: Option<Vec<ReasoningContent>>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user