diff --git a/common/chat.cpp b/common/chat.cpp index 0e06fa591f9d..ad8c2e73f227 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -226,6 +226,11 @@ json common_chat_msg::to_json_oaicompat(bool concat_typed_text) const { }); } } + } else if (!content_present) { + // Preserve an omitted OpenAI-compatible content field. Some templates + // distinguish this from explicit null and an empty string. + } else if (content_is_null) { + jmsg["content"] = nullptr; } else { jmsg["content"] = ""; } @@ -390,6 +395,7 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa auto has_content = message.contains("content"); auto has_tool_calls = message.contains("tool_calls"); + msg.content_present = has_content; if (has_content) { const auto & content = message.at("content"); if (content.is_string()) { @@ -408,7 +414,9 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa msg_part.text = part.at("text"); msg.content_parts.push_back(msg_part); } - } else if (!content.is_null()) { + } else if (content.is_null()) { + msg.content_is_null = true; + } else { throw std::invalid_argument("Invalid 'content' type: expected string or array, got " + content.dump() + " (ref: https://github.com/ggml-org/llama.cpp/issues/8367)"); diff --git a/common/chat.h b/common/chat.h index 6d5b220aebb5..61763c13509a 100644 --- a/common/chat.h +++ b/common/chat.h @@ -86,6 +86,8 @@ struct common_chat_msg { std::string reasoning_content; std::string tool_name; std::string tool_call_id; + bool content_present = true; + bool content_is_null = false; nlohmann::ordered_json to_json_oaicompat(bool concat_typed_text = false) const; @@ -122,7 +124,8 @@ struct common_chat_msg { bool operator==(const common_chat_msg & other) const { return role == other.role && content == other.content && content_parts == other.content_parts && tool_calls == other.tool_calls && reasoning_content == other.reasoning_content && - tool_name == other.tool_name && tool_call_id == other.tool_call_id; + tool_name == other.tool_name && tool_call_id == other.tool_call_id && + content_present == other.content_present && content_is_null == other.content_is_null; } bool operator!=(const common_chat_msg & other) const { return !(*this == other); } diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index ef02fdde57ef..2fed779aaab4 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -1627,6 +1627,28 @@ static void test_msgs_oaicompat_json_conversion() { assert_equals(res[0].role, "assistant"); assert_equals(true, res[0].content.empty()); assert_equals(true, res[0].tool_calls.empty()); + assert_equals(false, res[0].content_present); + assert_equals( + std::string("[{\"role\":\"assistant\"}]"), + common_chat_msgs_to_json_oaicompat(res).dump()); + + auto null_content = + common_chat_msgs_parse_oaicompat(json::parse("[{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[]}]")); + assert_equals(1, null_content.size()); + assert_equals(true, null_content[0].content_present); + assert_equals(true, null_content[0].content_is_null); + assert_equals( + std::string("[{\"role\":\"assistant\",\"content\":null}]"), + common_chat_msgs_to_json_oaicompat(null_content).dump()); + + auto empty_content = + common_chat_msgs_parse_oaicompat(json::parse("[{\"role\":\"assistant\",\"content\":\"\",\"tool_calls\":[]}]")); + assert_equals(1, empty_content.size()); + assert_equals(true, empty_content[0].content_present); + assert_equals(false, empty_content[0].content_is_null); + assert_equals( + std::string("[{\"role\":\"assistant\",\"content\":\"\"}]"), + common_chat_msgs_to_json_oaicompat(empty_content).dump()); try { common_chat_msgs_parse_oaicompat(json::parse("[{\"role\": \"assistant\"}]"));