diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8d6acfa84..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -env: - CARGO_TERM_COLOR: always - -jobs: - # Build Check 已移除 - 本地 pre-commit hook 已经包含了所有必要的检查 diff --git a/src-tauri/src/converter/README.md b/src-tauri/src/converter/README.md index 333b4597f..aa9c213d4 100644 --- a/src-tauri/src/converter/README.md +++ b/src-tauri/src/converter/README.md @@ -14,7 +14,7 @@ - `openai_to_cw.rs` - OpenAI → CodeWhisperer 转换(支持 web_search 工具) - `cw_to_openai.rs` - CodeWhisperer → OpenAI 转换 - `anthropic_to_openai.rs` - Anthropic → OpenAI 转换 -- `openai_to_antigravity.rs` - OpenAI → Antigravity 转换 +- `openai_to_antigravity.rs` - OpenAI → Antigravity (Gemini CLI) 转换 ## 工具类型支持 @@ -25,8 +25,18 @@ - `web_search`: 联网搜索工具(Codex/Kiro 格式) - `web_search_20250305`: 联网搜索工具(Claude Code 格式) +## Antigravity 转换说明 + +参考 CLIProxyAPI 实现,主要特性: +- 请求结构:`{ project, request: { contents, systemInstruction, generationConfig, tools, safetySettings }, model }` +- 工具定义:`parameters` → `parametersJsonSchema` +- 安全设置:自动附加默认 safety settings +- 思维链:支持 `reasoning_effort` 配置 +- Function Call:正确处理 `thoughtSignature` 和响应格式 + ## 更新日志 +- 2025-12-28: 修复 Antigravity 转换,对齐 CLIProxyAPI 实现 - 2025-12-27: 添加 web_search 工具支持,修复 Issue #49 ## 更新提醒 diff --git a/src-tauri/src/converter/anthropic_to_openai.rs b/src-tauri/src/converter/anthropic_to_openai.rs index 95814bfa8..7e905f599 100644 --- a/src-tauri/src/converter/anthropic_to_openai.rs +++ b/src-tauri/src/converter/anthropic_to_openai.rs @@ -45,9 +45,11 @@ pub fn convert_anthropic_to_openai(request: &AnthropicMessagesRequest) -> ChatCo messages: openai_messages, temperature: request.temperature, max_tokens: request.max_tokens, + top_p: None, stream: request.stream, tools, tool_choice: request.tool_choice.clone(), + reasoning_effort: None, } } diff --git a/src-tauri/src/converter/openai_to_antigravity.rs b/src-tauri/src/converter/openai_to_antigravity.rs index 9756a5ff0..2ffc0c5e7 100644 --- a/src-tauri/src/converter/openai_to_antigravity.rs +++ b/src-tauri/src/converter/openai_to_antigravity.rs @@ -1,8 +1,32 @@ -//! OpenAI 格式转换为 Antigravity (Gemini) 格式 +//! OpenAI 格式转换为 Antigravity (Gemini CLI) 格式 +//! +//! 本模块实现 OpenAI Chat Completions API 到 Antigravity/Gemini CLI API 的转换。 +//! 参考 CLIProxyAPI 的实现,确保请求格式与 Gemini CLI 兼容。 +//! +//! ## 主要功能 +//! - 消息格式转换(system/user/assistant/tool) +//! - 工具定义转换(parameters → parametersJsonSchema) +//! - 安全设置自动附加 +//! - 思维链配置(reasoning_effort) +//! +//! ## 更新日志 +//! - 2025-12-28: 修复请求格式,对齐 CLIProxyAPI 实现 + use crate::models::openai::*; use serde::{Deserialize, Serialize}; use uuid::Uuid; +// ============================================================================ +// 常量定义 +// ============================================================================ + +/// Gemini CLI 函数调用的 thought signature 标记 +const GEMINI_CLI_FUNCTION_THOUGHT_SIGNATURE: &str = "skip_thought_signature_validator"; + +// ============================================================================ +// 数据结构定义 +// ============================================================================ + /// Antigravity/Gemini 内容部分 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -15,6 +39,9 @@ pub struct GeminiPart { pub function_call: Option, #[serde(skip_serializing_if = "Option::is_none")] pub function_response: Option, + /// 思维签名,用于函数调用 + #[serde(skip_serializing_if = "Option::is_none")] + pub thought_signature: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -37,7 +64,13 @@ pub struct GeminiFunctionResponse { #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, pub name: String, - pub response: serde_json::Value, + pub response: GeminiFunctionResponseBody, +} + +/// Function Response 的响应体结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GeminiFunctionResponseBody { + pub result: serde_json::Value, } /// Antigravity/Gemini 内容 @@ -51,16 +84,29 @@ pub struct GeminiContent { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GeminiTool { - pub function_declarations: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub function_declarations: Option>, + /// Google Search 工具(透传) + #[serde(skip_serializing_if = "Option::is_none")] + pub google_search: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct GeminiFunctionDeclaration { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// Gemini CLI 使用 parametersJsonSchema 而非 parameters #[serde(skip_serializing_if = "Option::is_none")] - pub parameters: Option, + pub parameters_json_schema: Option, +} + +/// 安全设置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SafetySetting { + pub category: String, + pub threshold: String, } /// Antigravity/Gemini 生成配置 @@ -81,13 +127,18 @@ pub struct GeminiGenerationConfig { pub candidate_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thinking_config: Option, + /// 响应模态(TEXT, IMAGE) + #[serde(skip_serializing_if = "Option::is_none")] + pub response_modalities: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ThinkingConfig { - pub include_thoughts: bool, - pub thinking_budget: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_thoughts: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thinking_budget: Option, } /// Antigravity 请求体内部结构 @@ -105,8 +156,19 @@ pub struct AntigravityRequestInner { pub tool_config: Option, #[serde(skip_serializing_if = "Option::is_none")] pub session_id: Option, + /// 安全设置 + #[serde(skip_serializing_if = "Option::is_none")] + pub safety_settings: Option>, } +// ============================================================================ +// 辅助函数 +// ============================================================================ + +// ============================================================================ +// 辅助函数 +// ============================================================================ + /// 生成随机请求 ID fn generate_request_id() -> String { format!("agent-{}", Uuid::new_v4()) @@ -122,6 +184,32 @@ fn generate_session_id() -> String { format!("-{}", n) } +/// 获取默认安全设置 +fn default_safety_settings() -> Vec { + vec![ + SafetySetting { + category: "HARM_CATEGORY_HARASSMENT".to_string(), + threshold: "OFF".to_string(), + }, + SafetySetting { + category: "HARM_CATEGORY_HATE_SPEECH".to_string(), + threshold: "OFF".to_string(), + }, + SafetySetting { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT".to_string(), + threshold: "OFF".to_string(), + }, + SafetySetting { + category: "HARM_CATEGORY_DANGEROUS_CONTENT".to_string(), + threshold: "OFF".to_string(), + }, + SafetySetting { + category: "HARM_CATEGORY_CIVIC_INTEGRITY".to_string(), + threshold: "BLOCK_NONE".to_string(), + }, + ] +} + /// 模型名称映射 fn model_mapping(model: &str) -> &str { match model { @@ -137,6 +225,16 @@ fn model_mapping(model: &str) -> &str { } } +/// 检查模型是否支持思维链 +fn model_supports_thinking(model: &str) -> bool { + model.contains("2.5") || model.contains("3-pro") || model.contains("thinking") +} + +/// 检查模型是否使用离散思维级别(Gemini 3) +fn model_uses_thinking_levels(model: &str) -> bool { + model.contains("gemini-3") +} + /// 是否启用思维链 fn is_enable_thinking(model: &str) -> bool { model.ends_with("-thinking") @@ -146,32 +244,81 @@ fn is_enable_thinking(model: &str) -> bool { || model == "gpt-oss-120b-medium" } +// ============================================================================ +// 主转换函数 +// ============================================================================ + +// ============================================================================ +// 主转换函数 +// ============================================================================ + /// 将 OpenAI ChatCompletionRequest 转换为 Antigravity 请求体 +/// +/// 参考 CLIProxyAPI 的实现,确保请求格式正确。 pub fn convert_openai_to_antigravity_with_context( request: &ChatCompletionRequest, project_id: &str, ) -> serde_json::Value { let actual_model = model_mapping(&request.model); - let enable_thinking = is_enable_thinking(&request.model); + let supports_thinking = model_supports_thinking(actual_model); let mut contents: Vec = Vec::new(); let mut system_instruction: Option = None; - // 处理消息 + // 第一遍:收集 assistant tool_calls 的 id -> name 映射 + let mut tc_id_to_name: std::collections::HashMap = + std::collections::HashMap::new(); for msg in &request.messages { + if msg.role == "assistant" { + if let Some(tool_calls) = &msg.tool_calls { + for tc in tool_calls { + tc_id_to_name.insert(tc.id.clone(), tc.function.name.clone()); + } + } + } + } + + // 第二遍:收集 tool 响应 + let mut tool_responses: std::collections::HashMap = + std::collections::HashMap::new(); + for msg in &request.messages { + if msg.role == "tool" { + if let Some(tool_call_id) = &msg.tool_call_id { + let content = msg.get_content_text(); + tool_responses.insert(tool_call_id.clone(), content); + } + } + } + + // 第三遍:构建消息 + let messages_len = request.messages.len(); + for (idx, msg) in request.messages.iter().enumerate() { match msg.role.as_str() { "system" => { - let text = msg.get_content_text(); - if !text.is_empty() { - system_instruction = Some(GeminiContent { - role: "user".to_string(), - parts: vec![GeminiPart { - text: Some(text), - inline_data: None, - function_call: None, - function_response: None, - }], - }); + // system 消息只有在有其他消息时才作为 systemInstruction + if messages_len > 1 { + let text = msg.get_content_text(); + if !text.is_empty() { + system_instruction = Some(GeminiContent { + role: "user".to_string(), + parts: vec![GeminiPart { + text: Some(text), + inline_data: None, + function_call: None, + function_response: None, + thought_signature: None, + }], + }); + } + } else { + // 只有 system 消息时,作为 user 消息 + let parts = convert_user_content(msg); + if !parts.is_empty() { + contents.push(GeminiContent { + role: "user".to_string(), + parts, + }); + } } } "user" => { @@ -184,66 +331,179 @@ pub fn convert_openai_to_antigravity_with_context( } } "assistant" => { - let parts = convert_assistant_content(msg, &contents); - if !parts.is_empty() { - // 检查是否需要合并到上一条 model 消息 - let should_merge = if let Some(last) = contents.last() { - last.role == "model" - && msg.tool_calls.is_some() - && msg.get_content_text().is_empty() - } else { - false - }; + let mut parts = Vec::new(); - if should_merge { - if let Some(last) = contents.last_mut() { - last.parts.extend(parts); + // 文本内容 + let text = msg.get_content_text(); + if !text.is_empty() { + parts.push(GeminiPart { + text: Some(text), + inline_data: None, + function_call: None, + function_response: None, + thought_signature: None, + }); + } + + // 处理多模态内容(如图片) + if let Some(MessageContent::Parts(content_parts)) = &msg.content { + for part in content_parts { + if let ContentPart::ImageUrl { image_url } = part { + if let Some((mime, data)) = parse_data_url(&image_url.url) { + parts.push(GeminiPart { + text: None, + inline_data: Some(InlineData { + mime_type: mime, + data, + }), + function_call: None, + function_response: None, + thought_signature: None, + }); + } } - } else { + } + } + + // 工具调用 + if let Some(tool_calls) = &msg.tool_calls { + let mut function_ids: Vec = Vec::new(); + + for tc in tool_calls { + let args: serde_json::Value = serde_json::from_str(&tc.function.arguments) + .unwrap_or(serde_json::json!({})); + + parts.push(GeminiPart { + text: None, + inline_data: None, + function_call: Some(GeminiFunctionCall { + id: Some(tc.id.clone()), + name: tc.function.name.clone(), + args, // 直接使用 args,不要包装 + }), + function_response: None, + thought_signature: Some( + GEMINI_CLI_FUNCTION_THOUGHT_SIGNATURE.to_string(), + ), + }); + + function_ids.push(tc.id.clone()); + } + + // 添加 model 消息 + if !parts.is_empty() { contents.push(GeminiContent { role: "model".to_string(), parts, }); } + + // 紧接着添加 tool 响应作为 user 消息 + let mut tool_parts: Vec = Vec::new(); + for fid in &function_ids { + if let Some(name) = tc_id_to_name.get(fid) { + let resp = tool_responses.get(fid).cloned().unwrap_or_default(); + + // 解析响应内容 + let result_value: serde_json::Value = + if resp.is_empty() || resp == "null" { + serde_json::json!({}) + } else { + serde_json::from_str(&resp).unwrap_or_else(|_| { + // 非 JSON 内容,作为字符串 + serde_json::Value::String(resp.clone()) + }) + }; + + tool_parts.push(GeminiPart { + text: None, + inline_data: None, + function_call: None, + function_response: Some(GeminiFunctionResponse { + id: Some(fid.clone()), + name: name.clone(), + response: GeminiFunctionResponseBody { + result: result_value, + }, + }), + thought_signature: None, + }); + } + } + + if !tool_parts.is_empty() { + contents.push(GeminiContent { + role: "user".to_string(), + parts: tool_parts, + }); + } + } else if !parts.is_empty() { + contents.push(GeminiContent { + role: "model".to_string(), + parts, + }); } } "tool" => { - // Tool 响应 + // tool 消息已经在 assistant 处理时合并了,这里跳过 + // 但如果前面没有对应的 assistant tool_calls,需要单独处理 let tool_id = msg.tool_call_id.clone().unwrap_or_default(); - let content = msg.get_content_text(); - // 从之前的 model 消息中找到对应的 functionCall name - let function_name = find_function_name(&contents, &tool_id); - - let response_value = serde_json::json!({ "output": content }); - - let function_response = GeminiPart { - text: None, - inline_data: None, - function_call: None, - function_response: Some(GeminiFunctionResponse { - id: Some(tool_id), - name: function_name, - response: response_value, - }), - }; - - // 检查是否需要合并到上一条 user 消息 - let should_merge = if let Some(last) = contents.last() { - last.role == "user" && last.parts.iter().any(|p| p.function_response.is_some()) - } else { - false - }; - - if should_merge { - if let Some(last) = contents.last_mut() { - last.parts.push(function_response); - } - } else { - contents.push(GeminiContent { - role: "user".to_string(), - parts: vec![function_response], + // 检查是否已经被处理过 + let already_processed = idx > 0 + && request.messages[..idx].iter().rev().any(|m| { + m.role == "assistant" + && m.tool_calls + .as_ref() + .map(|tcs| tcs.iter().any(|tc| tc.id == tool_id)) + .unwrap_or(false) }); + + if !already_processed { + let content = msg.get_content_text(); + let function_name = tc_id_to_name.get(&tool_id).cloned().unwrap_or_default(); + + let result_value: serde_json::Value = if content.is_empty() || content == "null" + { + serde_json::json!({}) + } else { + serde_json::from_str(&content) + .unwrap_or_else(|_| serde_json::Value::String(content.clone())) + }; + + let function_response = GeminiPart { + text: None, + inline_data: None, + function_call: None, + function_response: Some(GeminiFunctionResponse { + id: Some(tool_id), + name: function_name, + response: GeminiFunctionResponseBody { + result: result_value, + }, + }), + thought_signature: None, + }; + + // 检查是否需要合并到上一条 user 消息 + let should_merge = contents + .last() + .map(|last| { + last.role == "user" + && last.parts.iter().any(|p| p.function_response.is_some()) + }) + .unwrap_or(false); + + if should_merge { + if let Some(last) = contents.last_mut() { + last.parts.push(function_response); + } + } else { + contents.push(GeminiContent { + role: "user".to_string(), + parts: vec![function_response], + }); + } } } _ => {} @@ -251,62 +511,101 @@ pub fn convert_openai_to_antigravity_with_context( } // 构建生成配置 - let generation_config = Some(GeminiGenerationConfig { - temperature: request.temperature.or(Some(1.0)), - max_output_tokens: request.max_tokens.map(|t| t as i32).or(Some(8096)), - top_p: Some(0.85), - top_k: Some(50), - stop_sequences: Some(vec![ - "<|user|>".to_string(), - "<|bot|>".to_string(), - "<|context_request|>".to_string(), - "<|endoftext|>".to_string(), - "<|end_of_turn|>".to_string(), - ]), - candidate_count: Some(1), - thinking_config: Some(ThinkingConfig { - include_thoughts: enable_thinking, - thinking_budget: if enable_thinking { 1024 } else { 0 }, - }), - }); - - // 转换工具 - let tools = request.tools.as_ref().map(|tools| { - tools - .iter() - .filter_map(|t| { - match t { - Tool::Function { function } => Some(GeminiTool { - function_declarations: vec![GeminiFunctionDeclaration { - name: function.name.clone(), - description: function.description.clone(), - parameters: clean_parameters(function.parameters.clone()), - }], - }), - // web_search 工具不转换为 Antigravity 格式 - Tool::WebSearch | Tool::WebSearch20250305 => None, - } - }) - .collect() - }); - - let tool_config = if tools.is_some() { - Some(serde_json::json!({ - "functionCallingConfig": { - "mode": "VALIDATED" - } - })) - } else { - None + let mut generation_config = GeminiGenerationConfig { + temperature: request.temperature, + max_output_tokens: request.max_tokens.map(|t| t as i32), + top_p: request.top_p, + top_k: None, + stop_sequences: None, + candidate_count: None, + thinking_config: None, + response_modalities: None, }; + // 处理 reasoning_effort(思维链配置) + if supports_thinking { + if let Some(ref effort) = request.reasoning_effort { + let effort_lower = effort.to_lowercase(); + if effort_lower != "none" { + if model_uses_thinking_levels(actual_model) { + // Gemini 3 使用离散级别 + generation_config.thinking_config = Some(ThinkingConfig { + include_thoughts: Some(true), + thinking_budget: None, + }); + } else { + // Gemini 2.5 使用数值预算 + let budget = match effort_lower.as_str() { + "low" => 1024, + "medium" => 8192, + "high" => 24576, + _ => 8192, + }; + generation_config.thinking_config = Some(ThinkingConfig { + include_thoughts: Some(true), + thinking_budget: Some(budget), + }); + } + } + } else if is_enable_thinking(&request.model) { + // 默认启用思维链 + generation_config.thinking_config = Some(ThinkingConfig { + include_thoughts: Some(true), + thinking_budget: Some(8192), + }); + } + } + + // 转换工具定义 + let tools: Option> = request.tools.as_ref().and_then(|tools| { + let mut function_declarations: Vec = Vec::new(); + + for t in tools { + match t { + Tool::Function { function } => { + // 转换 parameters -> parametersJsonSchema + let params_schema = function.parameters.as_ref().map(|p| { + let mut schema = clean_parameters(Some(p.clone())).unwrap_or_default(); + // 确保有 type 和 properties + if schema.get("type").is_none() { + schema["type"] = serde_json::json!("object"); + } + if schema.get("properties").is_none() { + schema["properties"] = serde_json::json!({}); + } + schema + }); + + function_declarations.push(GeminiFunctionDeclaration { + name: function.name.clone(), + description: function.description.clone(), + parameters_json_schema: params_schema, + }); + } + Tool::WebSearch | Tool::WebSearch20250305 => { + // web_search 工具不转换 + } + } + } + + if function_declarations.is_empty() { + None + } else { + Some(vec![GeminiTool { + function_declarations: Some(function_declarations), + google_search: None, + }]) + } + }); + let inner = AntigravityRequestInner { contents, system_instruction, - generation_config, + generation_config: Some(generation_config), tools, - tool_config, + tool_config: None, session_id: Some(generate_session_id()), + safety_settings: Some(default_safety_settings()), }; // 构建完整的 Antigravity 请求体 @@ -319,21 +618,9 @@ pub fn convert_openai_to_antigravity_with_context( }) } -/// 从之前的 model 消息中找到对应的 functionCall name -fn find_function_name(contents: &[GeminiContent], tool_id: &str) -> String { - for content in contents.iter().rev() { - if content.role == "model" { - for part in &content.parts { - if let Some(fc) = &part.function_call { - if fc.id.as_deref() == Some(tool_id) { - return fc.name.clone(); - } - } - } - } - } - String::new() -} +// ============================================================================ +// 辅助转换函数 +// ============================================================================ /// 清理参数中不需要的字段 fn clean_parameters(params: Option) -> Option { @@ -349,6 +636,7 @@ fn clean_value(value: serde_json::Value) -> serde_json::Value { "minItems", "maxItems", "uniqueItems", + "strict", // Gemini 不支持 strict ]; match value { @@ -383,6 +671,7 @@ fn convert_user_content(msg: &ChatMessage) -> Vec { inline_data: None, function_call: None, function_response: None, + thought_signature: None, }); } Some(MessageContent::Parts(content_parts)) => { @@ -394,6 +683,7 @@ fn convert_user_content(msg: &ChatMessage) -> Vec { inline_data: None, function_call: None, function_response: None, + thought_signature: None, }); } ContentPart::ImageUrl { image_url } => { @@ -407,6 +697,7 @@ fn convert_user_content(msg: &ChatMessage) -> Vec { }), function_call: None, function_response: None, + thought_signature: None, }); } } @@ -419,43 +710,6 @@ fn convert_user_content(msg: &ChatMessage) -> Vec { parts } -/// 转换助手消息内容 -fn convert_assistant_content(msg: &ChatMessage, _contents: &[GeminiContent]) -> Vec { - let mut parts = Vec::new(); - - // 文本内容 - let text = msg.get_content_text(); - if !text.is_empty() { - parts.push(GeminiPart { - text: Some(text.trim_end().to_string()), - inline_data: None, - function_call: None, - function_response: None, - }); - } - - // 工具调用 - if let Some(tool_calls) = &msg.tool_calls { - for tc in tool_calls { - let args: serde_json::Value = - serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::json!({})); - - parts.push(GeminiPart { - text: None, - inline_data: None, - function_call: Some(GeminiFunctionCall { - id: Some(tc.id.clone()), - name: tc.function.name.clone(), - args: serde_json::json!({ "query": args }), - }), - function_response: None, - }); - } - } - - parts -} - /// 解析 data URL fn parse_data_url(url: &str) -> Option<(String, String)> { if url.starts_with("data:") { @@ -470,17 +724,34 @@ fn parse_data_url(url: &str) -> Option<(String, String)> { None } +// ============================================================================ +// 响应转换函数 +// ============================================================================ + /// 将 Antigravity 响应转换为 OpenAI 格式 +/// +/// Antigravity 响应结构: +/// ```json +/// { +/// "response": { +/// "candidates": [...], +/// "usageMetadata": {...}, +/// "modelVersion": "...", +/// "responseId": "..." +/// } +/// } +/// ``` pub fn convert_antigravity_to_openai_response( antigravity_resp: &serde_json::Value, model: &str, ) -> serde_json::Value { - let mut choices = Vec::new(); + // Antigravity 响应可能在 response 字段下,也可能直接是 Gemini 格式 + let resp = antigravity_resp.get("response").unwrap_or(antigravity_resp); - if let Some(candidates) = antigravity_resp - .get("candidates") - .and_then(|c| c.as_array()) - { + let mut choices = Vec::new(); + let mut reasoning_content: Option = None; + + if let Some(candidates) = resp.get("candidates").and_then(|c| c.as_array()) { for (i, candidate) in candidates.iter().enumerate() { let mut content = String::new(); let mut tool_calls: Vec = Vec::new(); @@ -491,40 +762,116 @@ pub fn convert_antigravity_to_openai_response( .and_then(|p| p.as_array()) { for part in parts { - if let Some(text) = part.get("text").and_then(|t| t.as_str()) { - content.push_str(text); + // 检查是否是思维内容 + let is_thought = part + .get("thought") + .and_then(|t| t.as_bool()) + .unwrap_or(false); + + // 跳过纯 thoughtSignature 部分 + let has_thought_signature = part + .get("thoughtSignature") + .or_else(|| part.get("thought_signature")) + .and_then(|s| s.as_str()) + .map(|s| !s.is_empty()) + .unwrap_or(false); + + let has_content = part.get("text").is_some() + || part.get("functionCall").is_some() + || part.get("inlineData").is_some(); + + if has_thought_signature && !has_content { + continue; } + + if let Some(text) = part.get("text").and_then(|t| t.as_str()) { + if is_thought { + // 思维内容 + if let Some(ref mut rc) = reasoning_content { + rc.push_str(text); + } else { + reasoning_content = Some(text.to_string()); + } + } else { + content.push_str(text); + } + } + if let Some(fc) = part.get("functionCall") { - let call_id = format!("call_{}", &uuid::Uuid::new_v4().to_string()[..8]); + // 优先使用响应中的 id,否则生成新的 + let call_id = fc + .get("id") + .and_then(|id| id.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| { + format!("call_{}", &uuid::Uuid::new_v4().to_string()[..8]) + }); + + let default_args = serde_json::json!({}); + let args = fc.get("args").unwrap_or(&default_args); + let args_str = if args.is_string() { + args.as_str().unwrap_or("{}").to_string() + } else { + serde_json::to_string(args).unwrap_or_default() + }; + tool_calls.push(serde_json::json!({ "id": call_id, "type": "function", "function": { "name": fc.get("name").and_then(|n| n.as_str()).unwrap_or(""), - "arguments": serde_json::to_string(fc.get("args").unwrap_or(&serde_json::json!({}))).unwrap_or_default() + "arguments": args_str } })); } + + // 处理图片输出 + if let Some(inline_data) = + part.get("inlineData").or_else(|| part.get("inline_data")) + { + if let Some(data) = inline_data.get("data").and_then(|d| d.as_str()) { + let mime_type = inline_data + .get("mimeType") + .or_else(|| inline_data.get("mime_type")) + .and_then(|m| m.as_str()) + .unwrap_or("image/png"); + + // 将图片作为 data URL 添加到内容中 + let image_url = format!("data:{};base64,{}", mime_type, data); + if !content.is_empty() { + content.push_str("\n\n"); + } + content.push_str(&format!("![image]({})", image_url)); + } + } } } let finish_reason = candidate .get("finishReason") .and_then(|r| r.as_str()) - .map(|r| match r { + .map(|r| match r.to_uppercase().as_str() { "STOP" => "stop", "MAX_TOKENS" => "length", "SAFETY" => "content_filter", "RECITATION" => "content_filter", _ => "stop", }) - .unwrap_or("stop"); + .unwrap_or(if !tool_calls.is_empty() { + "tool_calls" + } else { + "stop" + }); let mut message = serde_json::json!({ "role": "assistant", "content": if content.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(content) } }); + if let Some(ref rc) = reasoning_content { + message["reasoning_content"] = serde_json::Value::String(rc.clone()); + } + if !tool_calls.is_empty() { message["tool_calls"] = serde_json::json!(tool_calls); } @@ -538,16 +885,58 @@ pub fn convert_antigravity_to_openai_response( } // 构建 usage - let usage = antigravity_resp.get("usageMetadata").map(|u| { - serde_json::json!({ - "prompt_tokens": u.get("promptTokenCount").and_then(|t| t.as_i64()).unwrap_or(0), - "completion_tokens": u.get("candidatesTokenCount").and_then(|t| t.as_i64()).unwrap_or(0), - "total_tokens": u.get("totalTokenCount").and_then(|t| t.as_i64()).unwrap_or(0) - }) + let usage = resp.get("usageMetadata").map(|u| { + let prompt_tokens = u + .get("promptTokenCount") + .and_then(|t| t.as_i64()) + .unwrap_or(0); + let completion_tokens = u + .get("candidatesTokenCount") + .and_then(|t| t.as_i64()) + .unwrap_or(0); + let total_tokens = u + .get("totalTokenCount") + .and_then(|t| t.as_i64()) + .unwrap_or(0); + let thoughts_tokens = u + .get("thoughtsTokenCount") + .and_then(|t| t.as_i64()) + .unwrap_or(0); + let cached_tokens = u + .get("cachedContentTokenCount") + .and_then(|t| t.as_i64()) + .unwrap_or(0); + + let mut usage_obj = serde_json::json!({ + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": total_tokens + }); + + if thoughts_tokens > 0 { + usage_obj["completion_tokens_details"] = serde_json::json!({ + "reasoning_tokens": thoughts_tokens + }); + } + + if cached_tokens > 0 { + usage_obj["prompt_tokens_details"] = serde_json::json!({ + "cached_tokens": cached_tokens + }); + } + + usage_obj }); + // 获取响应 ID + let response_id = resp + .get("responseId") + .and_then(|id| id.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("chatcmpl-{}", uuid::Uuid::new_v4())); + let mut response = serde_json::json!({ - "id": format!("chatcmpl-{}", uuid::Uuid::new_v4()), + "id": response_id, "object": "chat.completion", "created": chrono::Utc::now().timestamp(), "model": model, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2f0bfae69..7b6bfc144 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1152,6 +1152,7 @@ async fn check_api_compatibility( }], temperature: None, max_tokens: Some(100), + top_p: None, stream: false, tools: Some(vec![crate::models::openai::Tool::Function { function: crate::models::openai::FunctionDef { @@ -1170,6 +1171,7 @@ async fn check_api_compatibility( }, }]), tool_choice: None, + reasoning_effort: None, } } _ => { @@ -1186,9 +1188,11 @@ async fn check_api_compatibility( }], temperature: None, max_tokens: Some(10), + top_p: None, stream: false, tools: None, tool_choice: None, + reasoning_effort: None, } } }; diff --git a/src-tauri/src/models/openai.rs b/src-tauri/src/models/openai.rs index d23eeb20f..bef766471 100644 --- a/src-tauri/src/models/openai.rs +++ b/src-tauri/src/models/openai.rs @@ -117,12 +117,17 @@ pub struct ChatCompletionRequest { pub temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] pub max_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub top_p: Option, #[serde(default)] pub stream: bool, #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tool_choice: Option, + /// 思维链强度:none, low, medium, high + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/providers/antigravity.rs b/src-tauri/src/providers/antigravity.rs index 076997f10..54186af81 100644 --- a/src-tauri/src/providers/antigravity.rs +++ b/src-tauri/src/providers/antigravity.rs @@ -562,6 +562,9 @@ impl AntigravityProvider { } /// 构建 Antigravity 请求 + /// + /// 注意:此方法用于简单的非流式请求。 + /// 对于完整的 OpenAI 格式转换,请使用 `convert_openai_to_antigravity_with_context`。 fn build_antigravity_request( &self, model: &str, @@ -584,11 +587,19 @@ impl AntigravityProvider { // 设置会话 ID payload["request"]["sessionId"] = serde_json::json!(generate_session_id()); - // 删除安全设置 - if let Some(request) = payload.get_mut("request") { - if let Some(obj) = request.as_object_mut() { - obj.remove("safetySettings"); - } + // 添加默认安全设置(如果不存在) + if payload + .get("request") + .and_then(|r| r.get("safetySettings")) + .is_none() + { + payload["request"]["safetySettings"] = serde_json::json!([ + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"} + ]); } payload @@ -1572,6 +1583,7 @@ impl CredentialProvider for AntigravityProvider { // StreamingProvider Trait 实现 // ============================================================================ +use crate::converter::openai_to_antigravity::convert_openai_to_antigravity_with_context; use crate::models::openai::ChatCompletionRequest; use crate::providers::ProviderError; use crate::streaming::traits::{ @@ -1600,61 +1612,13 @@ impl StreamingProvider for AntigravityProvider { let project_id = self.project_id.clone().unwrap_or_else(generate_project_id); let actual_model = alias_to_model_name(&request.model); - // 构建 Antigravity 请求体 - // 将 OpenAI 格式转换为 Gemini/Antigravity 格式 - let mut contents = Vec::new(); - let mut system_instruction = None; + // 使用统一的转换函数构建请求体 + let payload = convert_openai_to_antigravity_with_context(request, &project_id); - for msg in &request.messages { - let role = &msg.role; - let text = match &msg.content { - Some(crate::models::openai::MessageContent::Text(t)) => t.clone(), - Some(crate::models::openai::MessageContent::Parts(parts)) => parts - .iter() - .filter_map(|p| { - if let crate::models::openai::ContentPart::Text { text } = p { - Some(text.clone()) - } else { - None - } - }) - .collect::>() - .join(""), - None => String::new(), - }; - - if role == "system" { - system_instruction = Some(serde_json::json!({ - "parts": [{ "text": text }] - })); - } else { - let gemini_role = if role == "assistant" { "model" } else { "user" }; - contents.push(serde_json::json!({ - "role": gemini_role, - "parts": [{ "text": text }] - })); - } - } - - let mut request_body = serde_json::json!({ - "contents": contents - }); - - if let Some(sys) = system_instruction { - request_body["systemInstruction"] = sys; - } - - // 构建 Antigravity 请求 - let mut payload = request_body.clone(); - payload["model"] = serde_json::json!(actual_model); - payload["userAgent"] = serde_json::json!("antigravity"); - payload["project"] = serde_json::json!(project_id); - payload["requestId"] = serde_json::json!(generate_request_id()); - - if payload.get("request").is_none() { - payload["request"] = serde_json::json!({}); - } - payload["request"]["sessionId"] = serde_json::json!(generate_session_id()); + tracing::debug!( + "[ANTIGRAVITY_STREAM] 请求体: {}", + serde_json::to_string_pretty(&payload).unwrap_or_default() + ); // 尝试多个 base URL let mut last_error: Option = None;