From d32386df3f2ad580a973bbbed0d48ddf6df01958 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Fri, 6 Mar 2026 14:59:04 +0000 Subject: [PATCH] fix(gemini): resolve 400 Bad Request by sanitizing thought_signature and improving tool name resolution This commit fixes the Gemini API 'Invalid value at thought_signature' error by ensuring synthetic 'call_' IDs are not passed into the TYPE_BYTES field. It also adds a pre-pass to correctly resolve function names from tool call IDs for tool responses. --- src/providers/gemini.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/providers/gemini.rs b/src/providers/gemini.rs index 9720cdcb..1f989be5 100644 --- a/src/providers/gemini.rs +++ b/src/providers/gemini.rs @@ -222,6 +222,16 @@ impl GeminiProvider { let mut contents: Vec = Vec::new(); let mut system_parts = Vec::new(); + // PRE-PASS: Build tool_id -> function_name mapping for tool responses + let mut tool_id_to_name = std::collections::HashMap::new(); + for msg in &messages { + if let Some(tool_calls) = &msg.tool_calls { + for tc in tool_calls { + tool_id_to_name.insert(tc.id.clone(), tc.function.name.clone()); + } + } + } + for msg in messages { if msg.role == "system" { for part in msg.content { @@ -261,7 +271,14 @@ impl GeminiProvider { }) .unwrap_or_default(); - let name = msg.name.clone().or_else(|| msg.tool_call_id.clone()).unwrap_or_else(|| "unknown_function".to_string()); + // RESOLVE: Use msg.name if present, otherwise look up by tool_call_id + let name = msg.name.clone() + .or_else(|| { + msg.tool_call_id.as_ref() + .and_then(|id| tool_id_to_name.get(id).cloned()) + }) + .or_else(|| msg.tool_call_id.clone()) + .unwrap_or_else(|| "unknown_function".to_string()); // Gemini API requires 'response' to be a JSON object (google.protobuf.Struct). // If it is an array or primitive, wrap it in an object. @@ -322,10 +339,13 @@ impl GeminiProvider { let args = serde_json::from_str::(&tc.function.arguments) .unwrap_or_else(|_| serde_json::json!({})); - // RESTORE: Use tc.id as thought_signature. - // Gemini 3 models require this field for any function call in the history. - // We include it regardless of format to ensure the model has context. - let thought_signature = Some(tc.id.clone()); + // RESTORE: Only use tc.id as thought_signature if it's NOT a synthetic ID. + // Synthetic IDs (starting with 'call_') cause 400 errors as they are not valid Base64 for the TYPE_BYTES field. + let thought_signature = if tc.id.starts_with("call_") { + None + } else { + Some(tc.id.clone()) + }; parts.push(GeminiPart { text: None,