diff --git a/src-tauri/src/ai_model_tools.rs b/src-tauri/src/ai_model_tools.rs index fdc5a82..11de5fb 100644 --- a/src-tauri/src/ai_model_tools.rs +++ b/src-tauri/src/ai_model_tools.rs @@ -19,10 +19,6 @@ const MAGIC_BRUSH_CONTINUATION_WORDS: &[&str] = &[ "continue", "go ahead", "do it", "try it", "继续", "接着", "开始", "执行", "操作", "试试", "你来", "好的", "可以", "行", ]; -const MAGIC_BRUSH_MUTATION_WORDS: &[&str] = &[ - "create", "write", "edit", "update", "delete", "save", "创建", "新建", "写入", "编辑", "修改", - "更新", "删除", "保存", -]; const MAGIC_BRUSH_TOOL_JSON: &str = r#"{ "type": "function", "function": { @@ -89,14 +85,7 @@ pub(crate) fn openai_chat_payload(request: &AiModelStreamRequest) -> serde_json: }); if offers_tools { payload["tools"] = serde_json::Value::Array(openai_vault_tools()); - payload["tool_choice"] = serde_json::Value::String( - if message_continues_pending_tool_work(&request.message) { - "required" - } else { - "auto" - } - .into(), - ); + payload["tool_choice"] = serde_json::Value::String("auto".into()); } payload } @@ -235,33 +224,6 @@ fn message_needs_magic_brush(message: &str) -> bool { .any(|word| message.to_lowercase().contains(word)) } -fn message_continues_pending_tool_work(message: &str) -> bool { - let latest = message - .rsplit("[user]:") - .next() - .unwrap_or(message) - .to_lowercase(); - if !MAGIC_BRUSH_CONTINUATION_WORDS - .iter() - .any(|word| latest.contains(word)) - { - return false; - } - - let recent = message - .chars() - .rev() - .take(4_000) - .collect::() - .chars() - .rev() - .collect::() - .to_lowercase(); - MAGIC_BRUSH_MUTATION_WORDS - .iter() - .any(|word| recent.contains(word)) -} - fn selected_model_supports_tools(request: &AiModelStreamRequest) -> bool { request .provider @@ -944,7 +906,7 @@ mod tests { assert!(payload["tools"] .as_array() .is_some_and(|tools| !tools.is_empty())); - assert_eq!(payload["tool_choice"], "required"); + assert_eq!(payload["tool_choice"], "auto"); assert_eq!(payload["stream"], false); } diff --git a/src-tauri/src/ai_models.rs b/src-tauri/src/ai_models.rs index c42219f..428b602 100644 --- a/src-tauri/src/ai_models.rs +++ b/src-tauri/src/ai_models.rs @@ -220,10 +220,38 @@ where } run_openai_agent_loop(request, payload, emit, |payload| { - send_json_request(request, endpoint.clone(), payload) + send_openai_payload_with_tool_choice_fallback(payload, |attempt| { + send_json_request(request, endpoint.clone(), attempt) + }) }) } +fn send_openai_payload_with_tool_choice_fallback( + mut payload: serde_json::Value, + mut send: S, +) -> Result +where + S: FnMut(serde_json::Value) -> Result, +{ + match send(payload.clone()) { + Err(error) if tool_choice_compatibility_error(&error) => { + if let Some(object) = payload.as_object_mut() { + object.remove("tool_choice"); + } + send(payload) + } + result => result, + } +} + +fn tool_choice_compatibility_error(error: &str) -> bool { + let normalized = error.to_ascii_lowercase(); + normalized.contains("tool_choice") + && (normalized.contains("does not support") + || normalized.contains("unsupported") + || normalized.contains("not supported")) +} + fn run_openai_agent_loop( request: &AiModelStreamRequest, mut payload: serde_json::Value, @@ -907,6 +935,33 @@ mod tests { && message.contains("correct the arguments"))); } + #[test] + fn tool_choice_compatibility_error_retries_without_the_field() { + let payload = json!({ + "messages": [{ "role": "user", "content": "Continue" }], + "tools": [{ "type": "function" }], + "tool_choice": "auto", + "stream": false, + }); + let mut attempts = Vec::new(); + + let response = send_openai_payload_with_tool_choice_fallback(payload, |attempt| { + attempts.push(attempt); + if attempts.len() == 1 { + Err("AI provider returned 400 Bad Request: Thinking mode does not support this tool_choice".into()) + } else { + Ok(json!({ "choices": [{ "message": { "content": "continued" } }] })) + } + }) + .unwrap(); + + assert_eq!(response["choices"][0]["message"]["content"], "continued"); + assert_eq!(attempts.len(), 2); + assert_eq!(attempts[0]["tool_choice"], "auto"); + assert!(attempts[1].get("tool_choice").is_none()); + assert!(attempts[1]["tools"].is_array()); + } + #[test] fn agent_loop_reads_then_edits_before_returning_final_text() { let dir = tempfile::tempdir().unwrap();