Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion common/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"] = "";
}
Expand Down Expand Up @@ -390,6 +395,7 @@ std::vector<common_chat_msg> 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()) {
Expand All @@ -408,7 +414,9 @@ std::vector<common_chat_msg> 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)");
Expand Down
5 changes: 4 additions & 1 deletion common/chat.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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); }
Expand Down
22 changes: 22 additions & 0 deletions tests/test-chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,28 @@ static void test_msgs_oaicompat_json_conversion() {
assert_equals<std::string>(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<size_t>(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<size_t>(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\"}]"));
Expand Down