From 7456692e9c728b9eb1ca5a52f3a872d7bb7e51ee Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Sun, 15 Feb 2026 20:50:40 -0500 Subject: [PATCH] fix: pass OpenAI-style tool_calls from provider to parser The OpenAI-compatible provider was not properly handling tool_calls in API responses. When providers like MiniMax return tool_calls in OpenAI's native format, the provider was only extracting the content field and discarding the tool_calls. Changes: - Update ResponseMessage struct to include optional tool_calls field - Add ToolCall and Function structs for deserializing tool_calls - Serialize full message as JSON when tool_calls are present - Fall back to plain content when no tool_calls This allows the parse_tool_calls function in the agent loop to properly handle OpenAI-style tool_calls format. All 1080 tests pass. Related to #226 Co-Authored-By: Claude Opus 4.6 --- src/providers/compatible.rs | 46 +++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/providers/compatible.rs b/src/providers/compatible.rs index 5c1348c..a554e28 100644 --- a/src/providers/compatible.rs +++ b/src/providers/compatible.rs @@ -90,9 +90,25 @@ struct Choice { message: ResponseMessage, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] struct ResponseMessage { - content: String, + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ToolCall { + #[serde(rename = "type")] + kind: Option, + function: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Function { + name: Option, + arguments: Option, } #[derive(Debug, Serialize)] @@ -287,7 +303,17 @@ impl Provider for OpenAiCompatibleProvider { .choices .into_iter() .next() - .map(|c| c.message.content) + .map(|c| { + // If tool_calls are present, serialize the full message as JSON + // so parse_tool_calls can handle the OpenAI-style format + if c.message.tool_calls.is_some() && c.message.tool_calls.as_ref().map_or(false, |t| !t.is_empty()) { + serde_json::to_string(&c.message) + .unwrap_or_else(|_| c.message.content.unwrap_or_default()) + } else { + // No tool calls, return content as-is + c.message.content.unwrap_or_default() + } + }) .ok_or_else(|| anyhow::anyhow!("No response from {}", self.name)) } @@ -359,7 +385,17 @@ impl Provider for OpenAiCompatibleProvider { .choices .into_iter() .next() - .map(|c| c.message.content) + .map(|c| { + // If tool_calls are present, serialize the full message as JSON + // so parse_tool_calls can handle the OpenAI-style format + if c.message.tool_calls.is_some() && c.message.tool_calls.as_ref().map_or(false, |t| !t.is_empty()) { + serde_json::to_string(&c.message) + .unwrap_or_else(|_| c.message.content.unwrap_or_default()) + } else { + // No tool calls, return content as-is + c.message.content.unwrap_or_default() + } + }) .ok_or_else(|| anyhow::anyhow!("No response from {}", self.name)) } } @@ -431,7 +467,7 @@ mod tests { fn response_deserializes() { let json = r#"{"choices":[{"message":{"content":"Hello from Venice!"}}]}"#; let resp: ApiChatResponse = serde_json::from_str(json).unwrap(); - assert_eq!(resp.choices[0].message.content, "Hello from Venice!"); + assert_eq!(resp.choices[0].message.content, Some("Hello from Venice!".to_string())); } #[test]