Skip to content

Commit 134d6e5

Browse files
authored
common/chat, server: refactor, move all conversion functions to common, add tests (ggml-org#20690)
* Refactor conversion functions
1 parent ca7f7b7 commit 134d6e5

11 files changed

Lines changed: 772 additions & 639 deletions

File tree

common/chat.cpp

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,25 @@ json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg> & msg
397397
return render_message_to_json(msgs, c);
398398
}
399399

400+
json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & tools) {
401+
if (tools.empty()) {
402+
return json();
403+
}
404+
405+
auto result = json::array();
406+
for (const auto & tool : tools) {
407+
result.push_back({
408+
{ "type", "function" },
409+
{ "function", {
410+
{ "name", tool.name },
411+
{ "description", tool.description },
412+
{ "parameters", json::parse(tool.parameters) },
413+
}},
414+
});
415+
}
416+
return result;
417+
}
418+
400419
std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const json & tools) {
401420
std::vector<common_chat_tool> result;
402421

@@ -432,56 +451,6 @@ std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const json & too
432451
return result;
433452
}
434453

435-
json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & tools) {
436-
if (tools.empty()) {
437-
return json();
438-
}
439-
440-
auto result = json::array();
441-
for (const auto & tool : tools) {
442-
result.push_back({
443-
{ "type", "function" },
444-
{ "function",
445-
{
446-
{ "name", tool.name },
447-
{ "description", tool.description },
448-
{ "parameters", json::parse(tool.parameters) },
449-
} },
450-
});
451-
}
452-
return result;
453-
}
454-
455-
json common_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff) {
456-
json delta = json::object();
457-
if (!diff.reasoning_content_delta.empty()) {
458-
delta["reasoning_content"] = diff.reasoning_content_delta;
459-
}
460-
if (!diff.content_delta.empty()) {
461-
delta["content"] = diff.content_delta;
462-
}
463-
if (diff.tool_call_index != std::string::npos) {
464-
json tool_call;
465-
tool_call["index"] = diff.tool_call_index;
466-
if (!diff.tool_call_delta.id.empty()) {
467-
tool_call["id"] = diff.tool_call_delta.id;
468-
tool_call["type"] = "function";
469-
}
470-
if (!diff.tool_call_delta.name.empty() || !diff.tool_call_delta.arguments.empty()) {
471-
json function = json::object();
472-
if (!diff.tool_call_delta.name.empty()) {
473-
function["name"] = diff.tool_call_delta.name;
474-
}
475-
if (!diff.tool_call_delta.arguments.empty()) {
476-
function["arguments"] = diff.tool_call_delta.arguments;
477-
}
478-
tool_call["function"] = function;
479-
}
480-
delta["tool_calls"] = json::array({ tool_call });
481-
}
482-
return delta;
483-
}
484-
485454
bool common_chat_verify_template(const std::string & tmpl, bool use_jinja) {
486455
if (use_jinja) {
487456
try {

common/chat.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,13 @@ bool common_chat_templates_support_enable_thinking(const common_chat_templates *
256256
// Parses a JSON array of messages in OpenAI's chat completion API format.
257257
std::vector<common_chat_msg> common_chat_msgs_parse_oaicompat(const nlohmann::ordered_json & messages);
258258

259+
std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const nlohmann::ordered_json & tools);
260+
259261
// DEPRECATED: only used in tests
260262
nlohmann::ordered_json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg> & msgs, bool concat_typed_text = false);
261263

262-
std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const nlohmann::ordered_json & tools);
263264
nlohmann::ordered_json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & tools);
264265

265-
nlohmann::ordered_json common_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff);
266-
267266
// get template caps, useful for reporting to server /props endpoint
268267
std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_templates * chat_templates);
269268

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS)
155155
llama_build_and_test(test-grammar-integration.cpp)
156156
llama_build_and_test(test-llama-grammar.cpp)
157157
llama_build_and_test(test-chat.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
158+
target_include_directories(test-chat PRIVATE ${PROJECT_SOURCE_DIR}/tools/server)
159+
target_link_libraries(test-chat PRIVATE server-context)
158160
# TODO: disabled on loongarch64 because the ggml-ci node lacks Python 3.8
159161
if (NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "loongarch64")
160162
llama_build_and_test(test-json-schema-to-grammar.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})

tests/test-chat.cpp

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88
#include "../src/llama-grammar.h"
99
#include "../src/unicode.h"
10+
#include "../tools/server/server-chat.h"
1011
#include "chat-auto-parser.h"
1112
#include "chat.h"
1213
#include "common.h"
@@ -1514,6 +1515,117 @@ static void test_tools_oaicompat_json_conversion() {
15141515
common_chat_tools_to_json_oaicompat({ special_function_tool }).dump(2));
15151516
}
15161517

1518+
static void test_convert_responses_to_chatcmpl() {
1519+
LOG_DBG("%s\n", __func__);
1520+
1521+
// Test basic conversion with input messages (user/assistant alternating)
1522+
{
1523+
json input = json::parse(R"({
1524+
"input": [
1525+
{
1526+
"type": "message",
1527+
"role": "user",
1528+
"content": "hi wassup"
1529+
},
1530+
{
1531+
"type": "message",
1532+
"role": "assistant",
1533+
"content": "Hey! 👋 Not much, just here ready to chat. What's up with you? Anything I can help you with today?"
1534+
},
1535+
{
1536+
"type": "message",
1537+
"role": "user",
1538+
"content": "hi"
1539+
}
1540+
],
1541+
"model": "gpt-5-mini",
1542+
"stream": false,
1543+
"text": {},
1544+
"reasoning": {
1545+
"effort": "medium"
1546+
}
1547+
})");
1548+
1549+
json result = server_chat_convert_responses_to_chatcmpl(input);
1550+
1551+
// Verify messages were converted correctly
1552+
assert_equals(true, result.contains("messages"));
1553+
assert_equals(true, result.at("messages").is_array());
1554+
assert_equals((size_t)3, result.at("messages").size());
1555+
1556+
// Check first message (user)
1557+
const auto & msg0 = result.at("messages")[0];
1558+
assert_equals(std::string("user"), msg0.at("role").get<std::string>());
1559+
assert_equals(true, msg0.at("content").is_array());
1560+
assert_equals(std::string("text"), msg0.at("content")[0].at("type").get<std::string>());
1561+
assert_equals(std::string("hi wassup"), msg0.at("content")[0].at("text").get<std::string>());
1562+
1563+
// Check second message (assistant)
1564+
const auto & msg1 = result.at("messages")[1];
1565+
assert_equals(std::string("assistant"), msg1.at("role").get<std::string>());
1566+
assert_equals(true, msg1.at("content").is_array());
1567+
assert_equals(std::string("text"), msg1.at("content")[0].at("type").get<std::string>());
1568+
assert_equals(std::string("Hey! 👋 Not much, just here ready to chat. What's up with you? Anything I can help you with today?"), msg1.at("content")[0].at("text").get<std::string>());
1569+
1570+
// Check third message (user)
1571+
const auto & msg2 = result.at("messages")[2];
1572+
assert_equals(std::string("user"), msg2.at("role").get<std::string>());
1573+
assert_equals(true, msg2.at("content").is_array());
1574+
assert_equals(std::string("text"), msg2.at("content")[0].at("type").get<std::string>());
1575+
assert_equals(std::string("hi"), msg2.at("content")[0].at("text").get<std::string>());
1576+
1577+
// Verify other fields preserved
1578+
assert_equals(std::string("gpt-5-mini"), result.at("model").get<std::string>());
1579+
assert_equals(false, result.at("stream").get<bool>());
1580+
}
1581+
1582+
// Test string input
1583+
{
1584+
json input = json::parse(R"({
1585+
"input": "Hello, world!",
1586+
"model": "test-model"
1587+
})");
1588+
1589+
json result = server_chat_convert_responses_to_chatcmpl(input);
1590+
1591+
assert_equals((size_t)1, result.at("messages").size());
1592+
const auto & msg = result.at("messages")[0];
1593+
assert_equals(std::string("user"), msg.at("role").get<std::string>());
1594+
assert_equals(std::string("Hello, world!"), msg.at("content").get<std::string>());
1595+
}
1596+
1597+
// Test with instructions (system message)
1598+
{
1599+
json input = json::parse(R"({
1600+
"input": "Hello",
1601+
"instructions": "You are a helpful assistant.",
1602+
"model": "test-model"
1603+
})");
1604+
1605+
json result = server_chat_convert_responses_to_chatcmpl(input);
1606+
1607+
assert_equals((size_t)2, result.at("messages").size());
1608+
const auto & sys_msg = result.at("messages")[0];
1609+
assert_equals(std::string("system"), sys_msg.at("role").get<std::string>());
1610+
assert_equals(std::string("You are a helpful assistant."), sys_msg.at("content").get<std::string>());
1611+
}
1612+
1613+
// Test with max_output_tokens conversion
1614+
{
1615+
json input = json::parse(R"({
1616+
"input": "Hello",
1617+
"model": "test-model",
1618+
"max_output_tokens": 100
1619+
})");
1620+
1621+
json result = server_chat_convert_responses_to_chatcmpl(input);
1622+
1623+
assert_equals(true, result.contains("max_tokens"));
1624+
assert_equals(false, result.contains("max_output_tokens"));
1625+
assert_equals(100, result.at("max_tokens").get<int>());
1626+
}
1627+
}
1628+
15171629
static void test_template_output_peg_parsers(bool detailed_debug) {
15181630
LOG_DBG("%s\n", __func__);
15191631

@@ -4291,7 +4403,7 @@ int main(int argc, char ** argv) {
42914403
bool detailed_debug = false;
42924404
bool only_run_filtered = false;
42934405

4294-
// Check for --template flag
4406+
// Check for --template and --detailed flags
42954407
for (int i = 1; i < argc; i++) {
42964408
std::string arg = argv[i];
42974409
if (arg == "--template" && i + 1 < argc) {
@@ -4316,7 +4428,20 @@ int main(int argc, char ** argv) {
43164428
}
43174429

43184430
#ifndef _WIN32
4319-
if (argc > 1) {
4431+
// Check if any argument is a .jinja file (for template format detection mode)
4432+
bool has_jinja_files = false;
4433+
for (int i = 1; i < argc; i++) {
4434+
std::string arg = argv[i];
4435+
if (arg == "--detailed") {
4436+
continue;
4437+
}
4438+
if (arg.size() >= 6 && arg.rfind(".jinja") == arg.size() - 6) {
4439+
has_jinja_files = true;
4440+
break;
4441+
}
4442+
}
4443+
4444+
if (has_jinja_files) {
43204445
common_chat_templates_inputs inputs;
43214446
common_chat_msg msg;
43224447
msg.role = "user";
@@ -4349,6 +4474,7 @@ int main(int argc, char ** argv) {
43494474
test_msg_diffs_compute();
43504475
test_msgs_oaicompat_json_conversion();
43514476
test_tools_oaicompat_json_conversion();
4477+
test_convert_responses_to_chatcmpl();
43524478
test_developer_role_to_system_workaround();
43534479
test_reka_edge_common_path();
43544480
test_template_output_peg_parsers(detailed_debug);

tools/server/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
55
set(TARGET server-context)
66

77
add_library(${TARGET} STATIC
8+
server-chat.cpp
9+
server-chat.h
810
server-task.cpp
911
server-task.h
1012
server-queue.cpp

0 commit comments

Comments
 (0)