diff --git a/src-tauri/src/proxy/mappers/claude/request.rs b/src-tauri/src/proxy/mappers/claude/request.rs index c2447486a..a4a677a93 100644 --- a/src-tauri/src/proxy/mappers/claude/request.rs +++ b/src-tauri/src/proxy/mappers/claude/request.rs @@ -1723,13 +1723,6 @@ fn build_tools( let mut tool_list = Vec::new(); - // [优化] Gemini 2.0+ 及 3.0 系列模型通常支持混合工具调用 (Function Calling + Google Search) - // 只有针对老旧模型或特定受限环境才需要互斥。 - let model_lower = mapped_model.to_lowercase(); - let supports_mixed_tools = model_lower.contains("gemini-2.0") - || model_lower.contains("gemini-2.5") - || model_lower.contains("gemini-3"); - if !function_declarations.is_empty() { let mut func_obj = serde_json::Map::new(); func_obj.insert( @@ -1739,21 +1732,13 @@ fn build_tools( tool_list.push(json!(func_obj)); if has_google_search { - if supports_mixed_tools { - tracing::info!( - "[Claude-Request] Enabling MIXED tool calling for {}: Function Calling + Google Search.", - mapped_model - ); - let mut search_obj = serde_json::Map::new(); - search_obj.insert("googleSearch".to_string(), json!({})); - tool_list.push(json!(search_obj)); - } else { - tracing::info!( - "[Claude-Request] Skipping googleSearch injection for {} due to existing function declarations. \ - Older Gemini models may not support mixed tool types.", - mapped_model - ); - } + // [FIX] cloudcode-pa (v1internal) rejects googleSearch combined with + // functionDeclarations and ignores tool_config.includeServerSideToolInvocations + // (verified upstream 400). Never mix: drop googleSearch when function tools exist. + tracing::info!( + "[Claude-Request] Skipping googleSearch injection for {}: function declarations present (v1internal cannot mix built-in + function tools).", + mapped_model + ); } } else if has_google_search { let mut search_obj = serde_json::Map::new(); @@ -3005,9 +2990,9 @@ mod tests { } #[test] - fn test_mixed_tools_injection_for_gemini_2_0() { - // [场景] 使用 Gemini 2.0 模型,同时提供自定义工具和启用全网搜索 - // 期望: 转换后的请求应同时包含 googleSearch 和 functionDeclarations + fn test_no_mixed_tools_for_gemini_2_0() { + // [场景] Gemini 2.0 模型, 同时提供自定义工具和 web search + // 期望: v1internal 无法混合, googleSearch 被丢弃, 仅保留 functionDeclarations let req = ClaudeRequest { model: "claude-sonnet-4-6".to_string(), // 映射到 gemini-2.0-flash-exp messages: vec![Message { @@ -3054,13 +3039,10 @@ mod tests { .any(|t| t.get("functionDeclarations").is_some()); assert!( - has_google_search, - "Gemini 2.0 should support mixed Google Search" - ); - assert!( - has_functions, - "Gemini 2.0 should support mixed function declarations" + !has_google_search, + "v1internal cannot mix googleSearch with functionDeclarations; googleSearch must be dropped" ); + assert!(has_functions, "function declarations must be preserved"); } #[test] diff --git a/src-tauri/src/proxy/mappers/common_utils.rs b/src-tauri/src/proxy/mappers/common_utils.rs index 904b635f1..dd85678b1 100644 --- a/src-tauri/src/proxy/mappers/common_utils.rs +++ b/src-tauri/src/proxy/mappers/common_utils.rs @@ -79,7 +79,7 @@ pub fn resolve_request_config( // 检测是否有联网工具定义 (内置功能调用) let has_networking_tool = detects_networking_tool(tools); // 检测是否包含非联网工具 (如 MCP 本地工具) - let _has_non_networking = contains_non_networking_tool(tools); + 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"); @@ -101,7 +101,11 @@ pub fn resolve_request_config( // Determine if we should enable networking // [FIX] 禁用基于模型的自动联网逻辑,防止图像请求被联网搜索结果覆盖。 // 仅在用户显式请求联网时启用:1) -online 后缀 2) 携带联网工具定义 - let enable_networking = is_online_suffix || has_networking_tool; + // [FIX] cloudcode-pa (v1internal) rejects googleSearch mixed with functionDeclarations + // and does NOT honor tool_config.includeServerSideToolInvocations (verified: upstream + // returns HTTP 400 even with the flag set). So when real (non-networking) function tools + // are present, stay in agent mode rather than entering web-search mode and 400-ing. + let enable_networking = (is_online_suffix || has_networking_tool) && !has_non_networking; // 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. @@ -605,6 +609,30 @@ mod tests { assert!(!config.inject_google_search); } + #[test] + fn test_networking_disabled_when_function_tools_present() { + // A coding/function tool present alongside a web_search tool: v1internal cannot mix, + // so networking must stay off (agent mode) instead of injecting googleSearch. + let tools = Some(vec![ + json!({ "name": "get_weather" }), + json!({ "name": "web_search" }), + ]); + let config = + resolve_request_config("gemini-3-flash", "gemini-3-flash", &tools, None, None, None, None); + assert_eq!(config.request_type, "agent"); + assert!(!config.inject_google_search); + } + + #[test] + fn test_networking_enabled_for_pure_web_search() { + // Only a web_search tool (no other function tools): networking should be enabled. + let tools = Some(vec![json!({ "name": "web_search" })]); + let config = + resolve_request_config("gemini-3-flash", "gemini-3-flash", &tools, None, None, None, None); + assert!(config.inject_google_search); + assert_eq!(config.request_type, "web_search"); + } + #[test] fn test_image_model_excluded() { let config = resolve_request_config( diff --git a/src-tauri/src/proxy/mappers/gemini/wrapper.rs b/src-tauri/src/proxy/mappers/gemini/wrapper.rs index 57422283b..54b462ac4 100644 --- a/src-tauri/src/proxy/mappers/gemini/wrapper.rs +++ b/src-tauri/src/proxy/mappers/gemini/wrapper.rs @@ -1193,8 +1193,8 @@ mod tests { assert!(has_functions, "Should contain functionDeclarations"); assert!( - has_google_search, - "Should contain googleSearch (Gemini 2.0+ supports mixed tools)" + !has_google_search, + "v1internal cannot mix; googleSearch must not be injected when functionDeclarations exist" ); } } diff --git a/src-tauri/src/proxy/mappers/openai/request.rs b/src-tauri/src/proxy/mappers/openai/request.rs index 65ee6b736..f8a6bec53 100644 --- a/src-tauri/src/proxy/mappers/openai/request.rs +++ b/src-tauri/src/proxy/mappers/openai/request.rs @@ -1268,8 +1268,8 @@ mod tests { assert!(has_functions, "Should contain functionDeclarations"); assert!( - has_google_search, - "Should contain googleSearch (Gemini 2.0+ supports mixed tools)" + !has_google_search, + "function tools present: networking disabled, googleSearch must not be injected" ); } }