fix(gemini): ensure final finish_reason is 'tool_calls' if any tools were seen
Gemini often sends tool calls in one chunk and then 'STOP' in a final chunk. If we pass the raw 'stop' at the end, clients stop and ignore the previously received tool calls. We now track if any tools were seen and override the final 'stop' to 'tool_calls'.
This commit is contained in:
@@ -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<u32, String> = 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| {
|
||||
// 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() {
|
||||
|
||||
Reference in New Issue
Block a user