fix: retry tool agents across provider compatibility modes
This commit is contained in:
parent
612932f714
commit
85c56230d0
@ -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::<String>()
|
||||
.chars()
|
||||
.rev()
|
||||
.collect::<String>()
|
||||
.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);
|
||||
}
|
||||
|
||||
|
||||
@ -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<S>(
|
||||
mut payload: serde_json::Value,
|
||||
mut send: S,
|
||||
) -> Result<serde_json::Value, String>
|
||||
where
|
||||
S: FnMut(serde_json::Value) -> Result<serde_json::Value, String>,
|
||||
{
|
||||
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<F, S>(
|
||||
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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user