From 3679b7d00be33b2ec0fe9b49c9a153c119480fab Mon Sep 17 00:00:00 2001 From: truffle Date: Wed, 1 Jul 2026 11:21:30 +0000 Subject: [PATCH] fix: include content field on assistant tool_calls messages The ToolUse arm of chat_message_to_openai_message returned None for content. Because OpenAIChatMessage.content is skip_serializing_if = Option::is_none, that dropped the field from the serialized request. Strict OpenAI-compatible APIs such as OpenRouter reject an assistant message that carries tool_calls without a content field, breaking any multi-turn tool loop through those providers. Emit the message content string instead, matching the OpenAI schema. Fixes #111 --- src/providers/openai_compatible.rs | 35 +++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/providers/openai_compatible.rs b/src/providers/openai_compatible.rs index 5923becd..11c33739 100644 --- a/src/providers/openai_compatible.rs +++ b/src/providers/openai_compatible.rs @@ -1124,7 +1124,7 @@ pub fn chat_message_to_openai_message(chat_msg: ChatMessage) -> OpenAIChatMessag tool_output: None, tool_call_id: None, }])), - MessageType::ToolUse(_) => None, + MessageType::ToolUse(_) => Some(Right(chat_msg.content.clone())), MessageType::ToolResult(_) => None, }, tool_calls: match &chat_msg.message_type { @@ -1608,4 +1608,37 @@ mod tests { results[0] ); } + + #[test] + fn test_tool_use_message_serializes_content_field() { + // Strict OpenAI-compatible APIs (e.g. OpenRouter) reject an assistant + // message that carries `tool_calls` but omits `content`. Because + // OpenAIChatMessage.content is `skip_serializing_if = "Option::is_none"`, + // a `None` here drops the field from the wire entirely. The tool-use arm + // must therefore serialize a `content` string, not `None`. + let msg = ChatMessage { + role: ChatRole::Assistant, + message_type: MessageType::ToolUse(vec![ToolCall { + id: "call_1".to_string(), + call_type: "function".to_string(), + function: FunctionCall { + name: "get_weather".to_string(), + arguments: "{\"city\":\"Paris\"}".to_string(), + }, + }]), + content: String::new(), + }; + + let openai_msg = chat_message_to_openai_message(msg); + let json = serde_json::to_value(&openai_msg).unwrap(); + + assert!( + json.get("content").is_some(), + "assistant tool_calls message must serialize a `content` field, got: {json}" + ); + assert!( + json.get("tool_calls").is_some(), + "tool_calls array must still be present alongside content, got: {json}" + ); + } }