fix(gemini): resolve compilation errors and enable Gemini 3 reasoning
This commit is contained in:
@@ -362,6 +362,7 @@ pub(super) async fn handle_test_provider(
|
||||
messages: vec![crate::models::UnifiedMessage {
|
||||
role: "user".to_string(),
|
||||
content: vec![crate::models::ContentPart::Text { text: "Hi".to_string() }],
|
||||
reasoning_content: None,
|
||||
tool_calls: None,
|
||||
name: None,
|
||||
tool_call_id: None,
|
||||
|
||||
@@ -223,6 +223,7 @@ pub struct UnifiedRequest {
|
||||
pub struct UnifiedMessage {
|
||||
pub role: String,
|
||||
pub content: Vec<ContentPart>,
|
||||
pub reasoning_content: Option<String>,
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
pub name: Option<String>,
|
||||
pub tool_call_id: Option<String>,
|
||||
@@ -337,6 +338,7 @@ impl TryFrom<ChatCompletionRequest> for UnifiedRequest {
|
||||
UnifiedMessage {
|
||||
role: msg.role,
|
||||
content,
|
||||
reasoning_content: msg.reasoning_content,
|
||||
tool_calls: msg.tool_calls,
|
||||
name: msg.name,
|
||||
tool_call_id: msg.tool_call_id,
|
||||
|
||||
@@ -55,6 +55,8 @@ struct GeminiPart {
|
||||
function_call: Option<GeminiFunctionCall>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
function_response: Option<GeminiFunctionResponse>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
thought: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -64,9 +66,12 @@ struct GeminiInlineData {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GeminiFunctionCall {
|
||||
name: String,
|
||||
args: Value,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -225,6 +230,7 @@ impl GeminiProvider {
|
||||
inline_data: None,
|
||||
function_call: None,
|
||||
function_response: None,
|
||||
thought: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -263,39 +269,64 @@ impl GeminiProvider {
|
||||
name,
|
||||
response: response_value,
|
||||
}),
|
||||
thought: None,
|
||||
});
|
||||
} else if msg.role == "assistant" && msg.tool_calls.is_some() {
|
||||
// Assistant messages with tool_calls
|
||||
if let Some(tool_calls) = &msg.tool_calls {
|
||||
for p in &msg.content {
|
||||
if let ContentPart::Text { text } = p {
|
||||
if !text.trim().is_empty() {
|
||||
parts.push(GeminiPart {
|
||||
text: Some(text.clone()),
|
||||
inline_data: None,
|
||||
function_call: None,
|
||||
function_response: None,
|
||||
});
|
||||
}
|
||||
} else if msg.role == "assistant" {
|
||||
// Assistant messages: handle text, thought (reasoning), and tool_calls
|
||||
for p in &msg.content {
|
||||
if let ContentPart::Text { text } = p {
|
||||
if !text.trim().is_empty() {
|
||||
parts.push(GeminiPart {
|
||||
text: Some(text.clone()),
|
||||
inline_data: None,
|
||||
function_call: None,
|
||||
function_response: None,
|
||||
thought: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If reasoning_content is present, include it as a 'thought' part
|
||||
if let Some(reasoning) = &msg.reasoning_content {
|
||||
if !reasoning.trim().is_empty() {
|
||||
parts.push(GeminiPart {
|
||||
text: None,
|
||||
inline_data: None,
|
||||
function_call: None,
|
||||
function_response: None,
|
||||
thought: Some(reasoning.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tool_calls) = &msg.tool_calls {
|
||||
for tc in tool_calls {
|
||||
let args = serde_json::from_str::<Value>(&tc.function.arguments)
|
||||
.unwrap_or_else(|_| serde_json::json!({}));
|
||||
|
||||
// RESTORE: Use tc.id as thought_signature if it was originally one
|
||||
let thought_signature = if tc.id.starts_with("sig_") || !tc.id.contains('-') {
|
||||
Some(tc.id.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
parts.push(GeminiPart {
|
||||
text: None,
|
||||
inline_data: None,
|
||||
function_call: Some(GeminiFunctionCall {
|
||||
name: tc.function.name.clone(),
|
||||
args,
|
||||
thought_signature,
|
||||
}),
|
||||
function_response: None,
|
||||
thought: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular text/image messages
|
||||
// Regular text/image messages (mostly user)
|
||||
for part in msg.content {
|
||||
match part {
|
||||
ContentPart::Text { text } => {
|
||||
@@ -305,6 +336,7 @@ impl GeminiProvider {
|
||||
inline_data: None,
|
||||
function_call: None,
|
||||
function_response: None,
|
||||
thought: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -322,6 +354,7 @@ impl GeminiProvider {
|
||||
}),
|
||||
function_call: None,
|
||||
function_response: None,
|
||||
thought: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -333,7 +366,6 @@ impl GeminiProvider {
|
||||
}
|
||||
|
||||
// STRATEGY: Strictly enforce alternating roles.
|
||||
// If current message has the same role as the last one, merge their parts.
|
||||
if let Some(last_content) = contents.last_mut() {
|
||||
if last_content.role.as_ref() == Some(&role) {
|
||||
last_content.parts.extend(parts);
|
||||
@@ -357,13 +389,13 @@ impl GeminiProvider {
|
||||
inline_data: None,
|
||||
function_call: None,
|
||||
function_response: None,
|
||||
thought: None,
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Final check: ensure we don't have empty contents after filtering.
|
||||
// If the last message was merged or filtered, we might have an empty array.
|
||||
if contents.is_empty() && system_parts.is_empty() {
|
||||
return Err(AppError::ProviderError("No valid content parts after filtering".to_string()));
|
||||
}
|
||||
@@ -485,13 +517,18 @@ impl GeminiProvider {
|
||||
let calls: Vec<ToolCall> = parts
|
||||
.iter()
|
||||
.filter_map(|p| p.function_call.as_ref())
|
||||
.map(|fc| ToolCall {
|
||||
id: format!("call_{}", Uuid::new_v4().simple()),
|
||||
call_type: "function".to_string(),
|
||||
function: FunctionCall {
|
||||
name: fc.name.clone(),
|
||||
arguments: serde_json::to_string(&fc.args).unwrap_or_else(|_| "{}".to_string()),
|
||||
},
|
||||
.map(|fc| {
|
||||
// CAPTURE: Use thought_signature as the ID if available
|
||||
let id = fc.thought_signature.clone().unwrap_or_else(|| format!("call_{}", Uuid::new_v4().simple()));
|
||||
|
||||
ToolCall {
|
||||
id,
|
||||
call_type: "function".to_string(),
|
||||
function: FunctionCall {
|
||||
name: fc.name.clone(),
|
||||
arguments: serde_json::to_string(&fc.args).unwrap_or_else(|_| "{}".to_string()),
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -504,14 +541,19 @@ impl GeminiProvider {
|
||||
.iter()
|
||||
.filter_map(|p| p.function_call.as_ref())
|
||||
.enumerate()
|
||||
.map(|(i, fc)| ToolCallDelta {
|
||||
index: i as u32,
|
||||
id: Some(format!("call_{}", Uuid::new_v4().simple())),
|
||||
call_type: Some("function".to_string()),
|
||||
function: Some(FunctionCallDelta {
|
||||
name: Some(fc.name.clone()),
|
||||
arguments: Some(serde_json::to_string(&fc.args).unwrap_or_else(|_| "{}".to_string())),
|
||||
}),
|
||||
.map(|(i, fc)| {
|
||||
// CAPTURE: Use thought_signature as the ID if available
|
||||
let id = fc.thought_signature.clone().unwrap_or_else(|| format!("call_{}", Uuid::new_v4().simple()));
|
||||
|
||||
ToolCallDelta {
|
||||
index: i as u32,
|
||||
id: Some(id),
|
||||
call_type: Some("function".to_string()),
|
||||
function: Some(FunctionCallDelta {
|
||||
name: Some(fc.name.clone()),
|
||||
arguments: Some(serde_json::to_string(&fc.args).unwrap_or_else(|_| "{}".to_string())),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -648,11 +690,15 @@ impl super::Provider for GeminiProvider {
|
||||
|
||||
let candidate = gemini_response.candidates.first();
|
||||
|
||||
// Extract text content (may be absent if only function calls)
|
||||
// Extract text content
|
||||
let content = candidate
|
||||
.and_then(|c| c.content.parts.iter().find_map(|p| p.text.clone()))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Extract reasoning (Gemini 3 'thought' parts)
|
||||
let reasoning_content = candidate
|
||||
.and_then(|c| c.content.parts.iter().find_map(|p| p.thought.clone()));
|
||||
|
||||
// Extract function calls → OpenAI tool_calls
|
||||
let tool_calls = candidate.and_then(|c| Self::extract_tool_calls(&c.content.parts));
|
||||
|
||||
@@ -679,7 +725,7 @@ impl super::Provider for GeminiProvider {
|
||||
|
||||
Ok(ProviderResponse {
|
||||
content,
|
||||
reasoning_content: None,
|
||||
reasoning_content,
|
||||
tool_calls,
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
@@ -821,6 +867,11 @@ impl super::Provider for GeminiProvider {
|
||||
.find_map(|p| p.text.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let reasoning_content = content_obj
|
||||
.parts
|
||||
.iter()
|
||||
.find_map(|p| p.thought.clone());
|
||||
|
||||
let tool_calls = Self::extract_tool_call_deltas(&content_obj.parts);
|
||||
|
||||
// Determine finish_reason
|
||||
@@ -832,10 +883,10 @@ impl super::Provider for GeminiProvider {
|
||||
});
|
||||
|
||||
// Avoid emitting completely empty chunks unless they carry usage.
|
||||
if !content.is_empty() || tool_calls.is_some() || stream_usage.is_some() {
|
||||
if !content.is_empty() || reasoning_content.is_some() || tool_calls.is_some() || stream_usage.is_some() {
|
||||
yield ProviderStreamChunk {
|
||||
content,
|
||||
reasoning_content: None,
|
||||
reasoning_content,
|
||||
finish_reason,
|
||||
tool_calls,
|
||||
model: model.clone(),
|
||||
|
||||
Reference in New Issue
Block a user