diff --git a/src/providers/helpers.rs b/src/providers/helpers.rs index 11f686aa..81bc214d 100644 --- a/src/providers/helpers.rs +++ b/src/providers/helpers.rs @@ -84,11 +84,41 @@ pub async fn messages_to_openai_json(messages: &[UnifiedMessage]) -> Result Result, AppError> { let mut result = Vec::new(); for m in messages { + // Tool-role messages: { role: "tool", content: "...", tool_call_id: "...", name: "..." } + if m.role == "tool" { + let text_content = m + .content + .first() + .map(|p| match p { + ContentPart::Text { text } => text.clone(), + ContentPart::Image(_) => "[Image]".to_string(), + }) + .unwrap_or_default(); + + let mut msg = serde_json::json!({ + "role": "tool", + "content": text_content + }); + if let Some(tool_call_id) = &m.tool_call_id { + msg["tool_call_id"] = serde_json::json!(tool_call_id); + } + if let Some(name) = &m.name { + msg["name"] = serde_json::json!(name); + } + result.push(msg); + continue; + } + + // Build content parts for non-tool messages (images become "[Image]" text) let mut parts = Vec::new(); for p in &m.content { match p { @@ -100,10 +130,26 @@ pub async fn messages_to_openai_json_text_only( } } } - result.push(serde_json::json!({ - "role": m.role, - "content": parts - })); + + let mut msg = serde_json::json!({ "role": m.role }); + + // For assistant messages with tool_calls, content can be null + if let Some(tool_calls) = &m.tool_calls { + if parts.is_empty() { + msg["content"] = serde_json::Value::Null; + } else { + msg["content"] = serde_json::json!(parts); + } + msg["tool_calls"] = serde_json::json!(tool_calls); + } else { + msg["content"] = serde_json::json!(parts); + } + + if let Some(name) = &m.name { + msg["name"] = serde_json::json!(name); + } + + result.push(msg); } Ok(result) }