fix(gemini): resolve 400 Bad Request by sanitizing thought_signature and improving tool name resolution
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

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.
This commit is contained in:
2026-03-06 14:59:04 +00:00
parent 149a7c3a29
commit d32386df3f

View File

@@ -222,6 +222,16 @@ impl GeminiProvider {
let mut contents: Vec<GeminiContent> = Vec::new(); let mut contents: Vec<GeminiContent> = Vec::new();
let mut system_parts = 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 { for msg in messages {
if msg.role == "system" { if msg.role == "system" {
for part in msg.content { for part in msg.content {
@@ -261,7 +271,14 @@ impl GeminiProvider {
}) })
.unwrap_or_default(); .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). // Gemini API requires 'response' to be a JSON object (google.protobuf.Struct).
// If it is an array or primitive, wrap it in an object. // If it is an array or primitive, wrap it in an object.
@@ -322,10 +339,13 @@ impl GeminiProvider {
let args = serde_json::from_str::<Value>(&tc.function.arguments) let args = serde_json::from_str::<Value>(&tc.function.arguments)
.unwrap_or_else(|_| serde_json::json!({})); .unwrap_or_else(|_| serde_json::json!({}));
// RESTORE: Use tc.id as thought_signature. // RESTORE: Only use tc.id as thought_signature if it's NOT a synthetic ID.
// Gemini 3 models require this field for any function call in the history. // Synthetic IDs (starting with 'call_') cause 400 errors as they are not valid Base64 for the TYPE_BYTES field.
// We include it regardless of format to ensure the model has context. let thought_signature = if tc.id.starts_with("call_") {
let thought_signature = Some(tc.id.clone()); None
} else {
Some(tc.id.clone())
};
parts.push(GeminiPart { parts.push(GeminiPart {
text: None, text: None,