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