| |
| 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 { |
| |
| let tools_val = request.tools.as_ref().map(|list| { |
| list.iter().map(|v| v.clone()).collect::<Vec<_>>() |
| }); |
|
|
| |
| 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()); |
| |
| |
| let mut system_instructions: Vec<String> = 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::<Vec<_>>().join("\n") |
| } |
| }) |
| }) |
| .collect(); |
|
|
| |
| 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()); |
|
|
| |
| 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()); |
| } |
| } |
| } |
|
|
| |
| let global_thought_sig = get_thought_signature(); |
| if global_thought_sig.is_some() { |
| tracing::info!("从全局存储获取到 thoughtSignature (长度: {})", global_thought_sig.as_ref().unwrap().len()); |
| } |
|
|
| |
| let contents: Vec<Value> = 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(); |
| |
| |
| if let Some(content) = &msg.content { |
| match content { |
| OpenAIContent::String(s) => { |
| if !s.is_empty() { |
| 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!("{}{}", 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 { |
| |
| let file_path = if image_url.url.starts_with("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); |
| |
| |
| if let Ok(file_bytes) = std::fs::read(&file_path) { |
| use base64::Engine as _; |
| let b64 = base64::engine::general_purpose::STANDARD.encode(&file_bytes); |
| |
| |
| 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); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
|
|
| |
| if let Some(tool_calls) = &msg.tool_calls { |
| for (index, tc) in tool_calls.iter().enumerate() { |
| |
| |
| |
| |
| |
| |
| |
|
|
| let args = serde_json::from_str::<Value>(&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 |
| } |
| }); |
|
|
| |
| if let Some(ref sig) = global_thought_sig { |
| func_call_part["thoughtSignature"] = json!(sig); |
| } |
|
|
| parts.push(func_call_part); |
| } |
| } |
|
|
| |
| 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::<Vec<_>>().join("\n"), |
| None => "".to_string() |
| }; |
|
|
| parts.push(json!({ |
| "functionResponse": { |
| "name": final_name, |
| "response": { "result": content_val } |
| } |
| })); |
| } |
|
|
| json!({ "role": role, "parts": parts }) |
| }) |
| .collect(); |
|
|
| |
| let mut merged_contents: Vec<Value> = Vec::new(); |
| for msg in contents { |
| if let Some(last) = merged_contents.last_mut() { |
| if last["role"] == msg["role"] { |
| |
| 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; |
|
|
| |
| 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" }, |
| ] |
| }); |
|
|
| |
| crate::proxy::mappers::common_utils::deep_clean_undefined(&mut inner_request); |
|
|
| |
| if let Some(tools) = &request.tools { |
| let mut function_declarations: Vec<Value> = 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")); |
| } |
| } |
| } |
|
|
| |
| if let Some(obj) = gemini_func.as_object_mut() { |
| obj.remove("format"); |
| obj.remove("strict"); |
| obj.remove("additionalProperties"); |
| obj.remove("type"); |
| } |
|
|
| if let Some(params) = gemini_func.get_mut("parameters") { |
| |
| crate::proxy::common::json_schema::clean_json_schema(params); |
|
|
| |
| |
| |
| if let Some(params_obj) = params.as_object_mut() { |
| if !params_obj.contains_key("type") { |
| params_obj.insert("type".to_string(), json!("OBJECT")); |
| } |
| } |
| |
| |
| 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"); |
| } |
| } |
|
|