Spaces:
Sleeping
Sleeping
| // Common utilities for request mapping across all protocols | |
| // Provides unified grounding/networking logic | |
| use serde_json::{json, Value}; | |
| /// Request configuration after grounding resolution | |
| pub struct RequestConfig { | |
| /// The request type: "agent", "web_search", or "image_gen" | |
| pub request_type: String, | |
| /// Whether to inject the googleSearch tool | |
| pub inject_google_search: bool, | |
| /// The final model name (with suffixes stripped) | |
| pub final_model: String, | |
| /// Image generation configuration (if request_type is image_gen) | |
| pub image_config: Option<Value>, | |
| } | |
| pub fn resolve_request_config( | |
| original_model: &str, | |
| mapped_model: &str, | |
| tools: &Option<Vec<Value>> | |
| ) -> RequestConfig { | |
| // 1. Image Generation Check (Priority) | |
| if mapped_model.starts_with("gemini-3-pro-image") { | |
| let (image_config, parsed_base_model) = parse_image_config(original_model); | |
| return RequestConfig { | |
| request_type: "image_gen".to_string(), | |
| inject_google_search: false, | |
| final_model: parsed_base_model, | |
| image_config: Some(image_config), | |
| }; | |
| } | |
| // 检测是否有联网工具定义 (内置功能调用) | |
| let has_networking_tool = detects_networking_tool(tools); | |
| // 检测是否包含非联网工具 (如 MCP 本地工具) | |
| let has_non_networking = contains_non_networking_tool(tools); | |
| // Strip -online suffix from original model if present (to detect networking intent) | |
| let is_online_suffix = original_model.ends_with("-online"); | |
| // High-quality grounding allowlist (Only for models known to support search and be relatively 'safe') | |
| let is_high_quality_model = mapped_model == "gemini-2.5-flash" | |
| || mapped_model == "gemini-1.5-pro" | |
| || mapped_model.starts_with("gemini-1.5-pro-") | |
| || mapped_model.starts_with("gemini-2.5-flash-") | |
| || mapped_model.starts_with("gemini-2.0-flash") | |
| || mapped_model.starts_with("gemini-3-") | |
| || mapped_model.contains("claude-3-5-sonnet") | |
| || mapped_model.contains("claude-3-opus") | |
| || mapped_model.contains("claude-sonnet") | |
| || mapped_model.contains("claude-opus") | |
| || mapped_model.contains("claude-4"); | |
| // Determine if we should enable networking | |
| // [FIX] 禁用基于模型的自动联网逻辑,防止图像请求被联网搜索结果覆盖。 | |
| // 仅在用户显式请求联网时启用:1) -online 后缀 2) 携带联网工具定义 | |
| let enable_networking = is_online_suffix || has_networking_tool; | |
| // The final model to send upstream should be the MAPPED model, | |
| // but if searching, we MUST ensure the model name is one the backend associates with search. | |
| // Based on ref_Antigravity2Api practice, we force a stable search model for search requests. | |
| let mut final_model = mapped_model.trim_end_matches("-online").to_string(); | |
| if enable_networking { | |
| // If it's a thinking model (which doesn't support tools) or a Claude-style alias, | |
| // fallback to gemini-2.5-flash which is the standard workhorse for search. | |
| if final_model.contains("thinking") || !final_model.starts_with("gemini-") { | |
| final_model = "gemini-2.5-flash".to_string(); | |
| } | |
| } | |
| RequestConfig { | |
| request_type: if enable_networking { | |
| "web_search".to_string() | |
| } else { | |
| "agent".to_string() | |
| }, | |
| inject_google_search: enable_networking, | |
| final_model, | |
| image_config: None, | |
| } | |
| } | |
| /// Parse image configuration from model name suffixes | |
| /// Returns (image_config, clean_model_name) | |
| fn parse_image_config(model_name: &str) -> (Value, String) { | |
| let mut aspect_ratio = "1:1"; | |
| let _image_size = "1024x1024"; // Default, not explicitly sent unless 4k/hd | |
| if model_name.contains("-16x9") { aspect_ratio = "16:9"; } | |
| else if model_name.contains("-9x16") { aspect_ratio = "9:16"; } | |
| else if model_name.contains("-4x3") { aspect_ratio = "4:3"; } | |
| else if model_name.contains("-3x4") { aspect_ratio = "3:4"; } | |
| else if model_name.contains("-1x1") { aspect_ratio = "1:1"; } | |
| let is_hd = model_name.contains("-4k") || model_name.contains("-hd"); | |
| let mut config = serde_json::Map::new(); | |
| config.insert("aspectRatio".to_string(), json!(aspect_ratio)); | |
| if is_hd { | |
| config.insert("imageSize".to_string(), json!("4K")); | |
| } | |
| // The upstream model must be EXACTLY "gemini-3-pro-image" | |
| (serde_json::Value::Object(config), "gemini-3-pro-image".to_string()) | |
| } | |
| /// Inject current googleSearch tool and ensure no duplicate legacy search tools | |
| pub fn inject_google_search_tool(body: &mut Value) { | |
| if let Some(obj) = body.as_object_mut() { | |
| let tools_entry = obj.entry("tools").or_insert_with(|| json!([])); | |
| if let Some(tools_arr) = tools_entry.as_array_mut() { | |
| // [安全校验] 如果数组中已经包含 functionDeclarations,严禁注入 googleSearch | |
| // 因为 Gemini v1internal 不支持在一次请求中混用 search 和 functions | |
| let has_functions = tools_arr.iter().any(|t| { | |
| t.as_object().map_or(false, |o| o.contains_key("functionDeclarations")) | |
| }); | |
| if has_functions { | |
| tracing::info!("Skipping googleSearch injection due to existing functionDeclarations"); | |
| return; | |
| } | |
| // 首先清理掉已存在的 googleSearch 或 googleSearchRetrieval,以防重复产生冲突 | |
| tools_arr.retain(|t| { | |
| if let Some(o) = t.as_object() { | |
| !(o.contains_key("googleSearch") || o.contains_key("googleSearchRetrieval")) | |
| } else { | |
| true | |
| } | |
| }); | |
| // 注入统一的 googleSearch (v1internal 规范) | |
| tools_arr.push(json!({ | |
| "googleSearch": {} | |
| })); | |
| } | |
| } | |
| } | |
| /// 深度迭代清理客户端发送的 [undefined] 脏字符串,防止 Gemini 接口校验失败 | |
| pub fn deep_clean_undefined(value: &mut Value) { | |
| match value { | |
| Value::Object(map) => { | |
| // 移除值为 "[undefined]" 的键 | |
| map.retain(|_, v| { | |
| if let Some(s) = v.as_str() { | |
| s != "[undefined]" | |
| } else { | |
| true | |
| } | |
| }); | |
| // 递归处理嵌套 | |
| for v in map.values_mut() { | |
| deep_clean_undefined(v); | |
| } | |
| } | |
| Value::Array(arr) => { | |
| for v in arr.iter_mut() { | |
| deep_clean_undefined(v); | |
| } | |
| } | |
| _ => {} | |
| } | |
| } | |
| /// Detects if the tool list contains a request for networking/web search. | |
| /// Supported keywords: "web_search", "google_search", "web_search_20250305" | |
| pub fn detects_networking_tool(tools: &Option<Vec<Value>>) -> bool { | |
| if let Some(list) = tools { | |
| for tool in list { | |
| // 1. 直发风格 (Claude/Simple OpenAI/Anthropic Builtin/Vertex): { "name": "..." } 或 { "type": "..." } | |
| if let Some(n) = tool.get("name").and_then(|v| v.as_str()) { | |
| if n == "web_search" || n == "google_search" || n == "web_search_20250305" || n == "google_search_retrieval" { | |
| return true; | |
| } | |
| } | |
| if let Some(t) = tool.get("type").and_then(|v| v.as_str()) { | |
| if t == "web_search_20250305" || t == "google_search" || t == "web_search" || t == "google_search_retrieval" { | |
| return true; | |
| } | |
| } | |
| // 2. OpenAI 嵌套风格: { "type": "function", "function": { "name": "..." } } | |
| if let Some(func) = tool.get("function") { | |
| if let Some(n) = func.get("name").and_then(|v| v.as_str()) { | |
| let keywords = ["web_search", "google_search", "web_search_20250305", "google_search_retrieval"]; | |
| if keywords.contains(&n) { | |
| return true; | |
| } | |
| } | |
| } | |
| // 3. Gemini 原生风格: { "functionDeclarations": [ { "name": "..." } ] } | |
| if let Some(decls) = tool.get("functionDeclarations").and_then(|v| v.as_array()) { | |
| for decl in decls { | |
| if let Some(n) = decl.get("name").and_then(|v| v.as_str()) { | |
| if n == "web_search" || n == "google_search" || n == "google_search_retrieval" { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| // 4. Gemini googleSearch 声明 (含 googleSearchRetrieval 变体) | |
| if tool.get("googleSearch").is_some() || tool.get("googleSearchRetrieval").is_some() { | |
| return true; | |
| } | |
| } | |
| } | |
| false | |
| } | |
| /// 探测是否包含非联网相关的本地函数工具 | |
| pub fn contains_non_networking_tool(tools: &Option<Vec<Value>>) -> bool { | |
| if let Some(list) = tools { | |
| for tool in list { | |
| let mut is_networking = false; | |
| // 简单逻辑:如果它是一个函数声明且名字不是联网关键词,则视为非联网工具 | |
| if let Some(n) = tool.get("name").and_then(|v| v.as_str()) { | |
| let keywords = ["web_search", "google_search", "web_search_20250305", "google_search_retrieval"]; | |
| if keywords.contains(&n) { is_networking = true; } | |
| } else if let Some(func) = tool.get("function") { | |
| if let Some(n) = func.get("name").and_then(|v| v.as_str()) { | |
| let keywords = ["web_search", "google_search", "web_search_20250305", "google_search_retrieval"]; | |
| if keywords.contains(&n) { is_networking = true; } | |
| } | |
| } else if tool.get("googleSearch").is_some() || tool.get("googleSearchRetrieval").is_some() { | |
| is_networking = true; | |
| } else if tool.get("functionDeclarations").is_some() { | |
| // 如果是 Gemini 风格的 functionDeclarations,进去看一眼 | |
| if let Some(decls) = tool.get("functionDeclarations").and_then(|v| v.as_array()) { | |
| for decl in decls { | |
| if let Some(n) = decl.get("name").and_then(|v| v.as_str()) { | |
| let keywords = ["web_search", "google_search", "google_search_retrieval"]; | |
| if !keywords.contains(&n) { | |
| return true; // 发现本地函数 | |
| } | |
| } | |
| } | |
| } | |
| is_networking = true; // 即使全是联网,外层也标记为联网 | |
| } | |
| if !is_networking { | |
| return true; | |
| } | |
| } | |
| } | |
| false | |
| } | |
| mod tests { | |
| use super::*; | |
| fn test_high_quality_model_auto_grounding() { | |
| let config = resolve_request_config("gpt-4o", "gemini-2.5-flash", &None); | |
| assert_eq!(config.request_type, "web_search"); | |
| assert!(config.inject_google_search); | |
| assert_eq!(config.final_model, "gemini-2.5-flash"); // 修正断言: final_model = mapped_model | |
| } | |
| fn test_gemini_native_tool_detection() { | |
| let tools = Some(vec![json!({ | |
| "functionDeclarations": [ | |
| { "name": "web_search", "parameters": {} } | |
| ] | |
| })]); | |
| assert!(detects_networking_tool(&tools)); | |
| } | |
| fn test_online_suffix_force_grounding() { | |
| let config = resolve_request_config("gemini-3-flash-online", "gemini-3-flash", &None); | |
| assert_eq!(config.request_type, "web_search"); | |
| assert!(config.inject_google_search); | |
| assert_eq!(config.final_model, "gemini-3-flash"); | |
| } | |
| fn test_default_no_grounding() { | |
| let config = resolve_request_config("claude-sonnet", "gemini-3-flash", &None); | |
| assert_eq!(config.request_type, "agent"); | |
| assert!(!config.inject_google_search); | |
| } | |
| fn test_image_model_excluded() { | |
| let config = resolve_request_config("gemini-3-pro-image", "gemini-3-pro-image", &None); | |
| assert_eq!(config.request_type, "image_gen"); | |
| assert!(!config.inject_google_search); | |
| } | |
| } | |