When using chat_with_tools, assistant messages that include tool_calls are serialized without a content field. OpenRouter (and potentially other strict OpenAI-compatible APIs) reject these with:
Every message in the conversation, including those with tool_calls, must include a content field, even if it's an empty string ("").
Root cause
In src/providers/openai_compatible.rs, chat_message_to_openai_message sets content: None for MessageType::ToolUse:
content: match &chat_msg.message_type {
MessageType::Text => Some(Right(chat_msg.content.clone())),
// ...
MessageType::ToolUse(_) => None, // <-- content is dropped entirely
MessageType::ToolResult(_) => None,
},
Since OpenAIChatMessage.content is annotated with #[serde(skip_serializing_if = "Option::is_none")], the field is completely absent from the serialized JSON. The ChatMessage may actually carry content (e.g. a reasoning string alongside the tool call), but it is silently discarded.
Expected behavior
The content field should always be present. For ToolUse messages it should be set to chat_msg.content (which may be an empty string):
MessageType::ToolUse(_) => Some(Right(chat_msg.content.clone())),
Observed in llm 1.3.7.
When using
chat_with_tools, assistant messages that includetool_callsare serialized without acontentfield. OpenRouter (and potentially other strict OpenAI-compatible APIs) reject these with:Root cause
In
src/providers/openai_compatible.rs,chat_message_to_openai_messagesetscontent: NoneforMessageType::ToolUse:Since
OpenAIChatMessage.contentis annotated with#[serde(skip_serializing_if = "Option::is_none")], the field is completely absent from the serialized JSON. TheChatMessagemay actually carry content (e.g. a reasoning string alongside the tool call), but it is silently discarded.Expected behavior
The
contentfield should always be present. ForToolUsemessages it should be set tochat_msg.content(which may be an empty string):Observed in llm 1.3.7.