diff --git a/src/providers/gemini.rs b/src/providers/gemini.rs index be094c96..9720cdcb 100644 --- a/src/providers/gemini.rs +++ b/src/providers/gemini.rs @@ -844,6 +844,7 @@ impl super::Provider for GeminiProvider { // Track tool call IDs by their part index to ensure stability during streaming. // Gemini doesn't always include the thoughtSignature in every chunk for the same part. let mut tool_call_ids: std::collections::HashMap = std::collections::HashMap::new(); + let mut seen_tool_calls = false; while let Some(event) = es.next().await { match event { @@ -887,6 +888,7 @@ impl super::Provider for GeminiProvider { let mut deltas = Vec::new(); for (p_idx, p) in content_obj.parts.iter().enumerate() { if let Some(fc) = &p.function_call { + seen_tool_calls = true; let tool_call_idx = p_idx as u32; // Attempt to find a signature in sibling fields @@ -927,18 +929,22 @@ impl super::Provider for GeminiProvider { let tool_calls = if deltas.is_empty() { None } else { Some(deltas) }; // Determine finish_reason - // STRATEGY: If we have tool calls, the finish_reason MUST be "tool_calls" - // to comply with OpenAI-style expectations and ensure the client continues. - let finish_reason = if tool_calls.is_some() { - Some("tool_calls".to_string()) - } else { - candidate.finish_reason.as_ref().map(|fr| { - match fr.as_str() { - "STOP" => "stop".to_string(), - _ => fr.to_lowercase(), - } - }) - }; + // STRATEGY: If we have tool calls in this chunk, OR if we have seen them + // previously in the stream, the finish_reason MUST be "tool_calls" + // if the provider signals a stop. This ensures the client executes tools. + let mut finish_reason = candidate.finish_reason.as_ref().map(|fr| { + match fr.as_str() { + "STOP" => "stop".to_string(), + _ => fr.to_lowercase(), + } + }); + + if seen_tool_calls && finish_reason.as_deref() == Some("stop") { + finish_reason = Some("tool_calls".to_string()); + } else if tool_calls.is_some() && finish_reason.is_none() { + // Optional: Could signal tool_calls here too, but OpenAI often waits until EOF. + // For now we only override it at the actual stop signal. + } // Avoid emitting completely empty chunks unless they carry usage. if !content.is_empty() || reasoning_content.is_some() || tool_calls.is_some() || stream_usage.is_some() {