// OpenAI → Gemini 请求转换 use super::models::*; use serde_json::{json, Value}; use super::streaming::get_thought_signature; pub fn transform_openai_request(request: &OpenAIRequest, project_id: &str, mapped_model: &str) -> Value { // 将 OpenAI 工具转为 Value 数组以便探测 let tools_val = request.tools.as_ref().map(|list| { list.iter().map(|v| v.clone()).collect::>() }); // Resolve grounding config let config = crate::proxy::mappers::common_utils::resolve_request_config(&request.model, mapped_model, &tools_val); tracing::info!("[Debug] OpenAI Request: original='{}', mapped='{}', type='{}', has_image_config={}", request.model, mapped_model, config.request_type, config.image_config.is_some()); // 1. 提取所有 System Message 并注入补丁 let mut system_instructions: Vec = request.messages.iter() .filter(|msg| msg.role == "system") .filter_map(|msg| { msg.content.as_ref().map(|c| match c { OpenAIContent::String(s) => s.clone(), OpenAIContent::Array(blocks) => { blocks.iter().filter_map(|b| { if let OpenAIContentBlock::Text { text } = b { Some(text.clone()) } else { None } }).collect::>().join("\n") } }) }) .collect(); // 注入 Codex/Coding Agent 补丁 system_instructions.push("You are a coding agent. You MUST use the provided 'shell' tool to perform ANY filesystem operations (reading, writing, creating files). Do not output JSON code blocks for tool execution; invoke the functions directly. To create a file, use the 'shell' tool with 'New-Item' or 'Set-Content' (Powershell). NEVER simulate/hallucinate actions in text without calling the tool first.".to_string()); // Pre-scan to map tool_call_id to function name (for Codex) let mut tool_id_to_name = std::collections::HashMap::new(); for msg in &request.messages { if let Some(tool_calls) = &msg.tool_calls { for call in tool_calls { let name = &call.function.name; let final_name = if name == "local_shell_call" { "shell" } else { name }; tool_id_to_name.insert(call.id.clone(), final_name.to_string()); } } } // 从全局存储获取 thoughtSignature (PR #93 支持) let global_thought_sig = get_thought_signature(); if global_thought_sig.is_some() { tracing::info!("从全局存储获取到 thoughtSignature (长度: {})", global_thought_sig.as_ref().unwrap().len()); } // 2. 构建 Gemini contents (过滤掉 system) let contents: Vec = request .messages .iter() .filter(|msg| msg.role != "system") .map(|msg| { let role = match msg.role.as_str() { "assistant" => "model", "tool" | "function" => "user", _ => &msg.role, }; let mut parts = Vec::new(); // Handle content (multimodal or text) if let Some(content) = &msg.content { match content { OpenAIContent::String(s) => { if !s.is_empty() { if role == "user" && mapped_model.contains("gemini-3") { // 为 Gemini 3 用户消息添加提醒补丁 let reminder = "\n\n(SYSTEM REMINDER: You MUST use the 'shell' tool to perform this action. Do not simply state it is done.)"; parts.push(json!({"text": format!("{}{}", s, reminder)})); } else { parts.push(json!({"text": s})); } } } OpenAIContent::Array(blocks) => { for block in blocks { match block { OpenAIContentBlock::Text { text } => { if role == "user" && mapped_model.contains("gemini-3") { let reminder = "\n\n(SYSTEM REMINDER: You MUST use the 'shell' tool to perform this action. Do not simply state it is done.)"; parts.push(json!({ "text": format!("{}{}", text, reminder) })); } else { parts.push(json!({"text": text})); } } OpenAIContentBlock::ImageUrl { image_url } => { if image_url.url.starts_with("data:") { if let Some(pos) = image_url.url.find(",") { let mime_part = &image_url.url[5..pos]; let mime_type = mime_part.split(';').next().unwrap_or("image/jpeg"); let data = &image_url.url[pos + 1..]; parts.push(json!({ "inlineData": { "mimeType": mime_type, "data": data } })); } } else if image_url.url.starts_with("http") { parts.push(json!({ "fileData": { "fileUri": &image_url.url, "mimeType": "image/jpeg" } })); } else { // [NEW] 处理本地文件路径 (file:// 或 Windows/Unix 路径) let file_path = if image_url.url.starts_with("file://") { // 移除 file:// 前缀 #[cfg(target_os = "windows")] { image_url.url.trim_start_matches("file:///").replace('/', "\\") } #[cfg(not(target_os = "windows"))] { image_url.url.trim_start_matches("file://").to_string() } } else { image_url.url.clone() }; tracing::info!("[OpenAI-Request] Reading local image: {}", file_path); // 读取文件并转换为 base64 if let Ok(file_bytes) = std::fs::read(&file_path) { use base64::Engine as _; let b64 = base64::engine::general_purpose::STANDARD.encode(&file_bytes); // 根据文件扩展名推断 MIME 类型 let mime_type = if file_path.to_lowercase().ends_with(".png") { "image/png" } else if file_path.to_lowercase().ends_with(".gif") { "image/gif" } else if file_path.to_lowercase().ends_with(".webp") { "image/webp" } else { "image/jpeg" }; parts.push(json!({ "inlineData": { "mimeType": mime_type, "data": b64 } })); tracing::info!("[OpenAI-Request] Successfully loaded image: {} ({} bytes)", file_path, file_bytes.len()); } else { tracing::warn!("[OpenAI-Request] Failed to read local image: {}", file_path); } } } } } } } } // Handle tool calls (assistant message) if let Some(tool_calls) = &msg.tool_calls { for (index, tc) in tool_calls.iter().enumerate() { /* 暂时移除:防止 Codex CLI 界面碎片化 if index == 0 && parts.is_empty() { if mapped_model.contains("gemini-3") { parts.push(json!({"text": "Thinking Process: Determining necessary tool actions."})); } } */ let args = serde_json::from_str::(&tc.function.arguments).unwrap_or(json!({})); let mut func_call_part = json!({ "functionCall": { "name": if tc.function.name == "local_shell_call" { "shell" } else { &tc.function.name }, "args": args } }); // [修复] 为该消息内的所有工具调用注入 thoughtSignature (PR #114 优化) if let Some(ref sig) = global_thought_sig { func_call_part["thoughtSignature"] = json!(sig); } parts.push(func_call_part); } } // Handle tool response if msg.role == "tool" || msg.role == "function" { let name = msg.name.as_deref().unwrap_or("unknown"); let final_name = if name == "local_shell_call" { "shell" } else if let Some(id) = &msg.tool_call_id { tool_id_to_name.get(id).map(|s| s.as_str()).unwrap_or(name) } else { name }; let content_val = match &msg.content { Some(OpenAIContent::String(s)) => s.clone(), Some(OpenAIContent::Array(blocks)) => blocks.iter().filter_map(|b| if let OpenAIContentBlock::Text { text } = b { Some(text.clone()) } else { None }).collect::>().join("\n"), None => "".to_string() }; parts.push(json!({ "functionResponse": { "name": final_name, "response": { "result": content_val } } })); } json!({ "role": role, "parts": parts }) }) .collect(); // [PR #合并] 合并连续相同角色的消息 (Gemini 强制要求 user/model 交替) let mut merged_contents: Vec = Vec::new(); for msg in contents { if let Some(last) = merged_contents.last_mut() { if last["role"] == msg["role"] { // 合并 parts if let (Some(last_parts), Some(msg_parts)) = (last["parts"].as_array_mut(), msg["parts"].as_array()) { last_parts.extend(msg_parts.iter().cloned()); continue; } } } merged_contents.push(msg); } let contents = merged_contents; // 3. 构建请求体 let mut gen_config = json!({ "maxOutputTokens": request.max_tokens.unwrap_or(64000), "temperature": request.temperature.unwrap_or(1.0), "topP": request.top_p.unwrap_or(1.0), }); if let Some(stop) = &request.stop { if stop.is_string() { gen_config["stopSequences"] = json!([stop]); } else if stop.is_array() { gen_config["stopSequences"] = stop.clone(); } } if let Some(fmt) = &request.response_format { if fmt.r#type == "json_object" { gen_config["responseMimeType"] = json!("application/json"); } } let mut inner_request = json!({ "contents": contents, "generationConfig": gen_config, "safetySettings": [ { "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": "OFF" }, ] }); // 深度清理 [undefined] 字符串 (Cherry Studio 等客户端常见注入) crate::proxy::mappers::common_utils::deep_clean_undefined(&mut inner_request); // 4. Handle Tools (Merged Cleaning) if let Some(tools) = &request.tools { let mut function_declarations: Vec = Vec::new(); for tool in tools.iter() { let mut gemini_func = if let Some(func) = tool.get("function") { func.clone() } else { let mut func = tool.clone(); if let Some(obj) = func.as_object_mut() { obj.remove("type"); obj.remove("strict"); obj.remove("additionalProperties"); } func }; if let Some(name) = gemini_func.get("name").and_then(|v| v.as_str()) { // 跳过内置联网工具名称,避免重复定义 if name == "web_search" || name == "google_search" || name == "web_search_20250305" { continue; } if name == "local_shell_call" { if let Some(obj) = gemini_func.as_object_mut() { obj.insert("name".to_string(), json!("shell")); } } } // [NEW CRITICAL FIX] 清除函数定义根层级的非法字段 (解决报错持久化) if let Some(obj) = gemini_func.as_object_mut() { obj.remove("format"); obj.remove("strict"); obj.remove("additionalProperties"); obj.remove("type"); // [NEW] Gemini 不支持在 FunctionDeclaration 根层级出现 type: "function" } if let Some(params) = gemini_func.get_mut("parameters") { // [DEEP FIX] 统一调用公共库清洗:展开 $ref 并剔除所有层级的 format/definitions crate::proxy::common::json_schema::clean_json_schema(params); // Gemini v1internal 要求: // 1. type 必须是大写 (OBJECT, STRING 等) // 2. 根对象必须有 "type": "OBJECT" if let Some(params_obj) = params.as_object_mut() { if !params_obj.contains_key("type") { params_obj.insert("type".to_string(), json!("OBJECT")); } } // 递归转换 type 为大写 (符合 Protobuf 定义) enforce_uppercase_types(params); } function_declarations.push(gemini_func); } if !function_declarations.is_empty() { inner_request["tools"] = json!([{ "functionDeclarations": function_declarations }]); } } if !system_instructions.is_empty() { inner_request["systemInstruction"] = json!({ "parts": [{"text": system_instructions.join("\n\n")}] }); } if config.inject_google_search { crate::proxy::mappers::common_utils::inject_google_search_tool(&mut inner_request); } if let Some(image_config) = config.image_config { if let Some(obj) = inner_request.as_object_mut() { obj.remove("tools"); obj.remove("systemInstruction"); let gen_config = obj.entry("generationConfig").or_insert_with(|| json!({})); if let Some(gen_obj) = gen_config.as_object_mut() { gen_obj.remove("thinkingConfig"); gen_obj.remove("responseMimeType"); gen_obj.remove("responseModalities"); gen_obj.insert("imageConfig".to_string(), image_config); } } } json!({ "project": project_id, "requestId": format!("openai-{}", uuid::Uuid::new_v4()), "request": inner_request, "model": config.final_model, "userAgent": "antigravity", "requestType": config.request_type }) } fn enforce_uppercase_types(value: &mut Value) { if let Value::Object(map) = value { if let Some(type_val) = map.get_mut("type") { if let Value::String(ref mut s) = type_val { *s = s.to_uppercase(); } } if let Some(properties) = map.get_mut("properties") { if let Value::Object(ref mut props) = properties { for v in props.values_mut() { enforce_uppercase_types(v); } } } if let Some(items) = map.get_mut("items") { enforce_uppercase_types(items); } } else if let Value::Array(arr) = value { for item in arr { enforce_uppercase_types(item); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_transform_openai_request_multimodal() { let req = OpenAIRequest { model: "gpt-4-vision".to_string(), messages: vec![OpenAIMessage { role: "user".to_string(), content: Some(OpenAIContent::Array(vec![ OpenAIContentBlock::Text { text: "What is in this image?".to_string() }, OpenAIContentBlock::ImageUrl { image_url: OpenAIImageUrl { url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==".to_string(), detail: None } } ])), tool_calls: None, tool_call_id: None, name: None, }], stream: false, max_tokens: None, temperature: None, top_p: None, stop: None, response_format: None, tools: None, tool_choice: None, parallel_tool_calls: None, instructions: None, input: None, prompt: None, }; let result = transform_openai_request(&req, "test-v", "gemini-1.5-flash"); let parts = &result["request"]["contents"][0]["parts"]; assert_eq!(parts.as_array().unwrap().len(), 2); assert_eq!(parts[0]["text"].as_str().unwrap(), "What is in this image?"); assert_eq!(parts[1]["inlineData"]["mimeType"].as_str().unwrap(), "image/png"); } }