Skip to content
Merged
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
69 changes: 19 additions & 50 deletions common/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,25 @@ json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg> & msg
return render_message_to_json(msgs, c);
}

json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & tools) {
if (tools.empty()) {
return json();
}

auto result = json::array();
for (const auto & tool : tools) {
result.push_back({
{ "type", "function" },
{ "function", {
{ "name", tool.name },
{ "description", tool.description },
{ "parameters", json::parse(tool.parameters) },
}},
});
}
return result;
}

std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const json & tools) {
std::vector<common_chat_tool> result;

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

json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & tools) {
if (tools.empty()) {
return json();
}

auto result = json::array();
for (const auto & tool : tools) {
result.push_back({
{ "type", "function" },
{ "function",
{
{ "name", tool.name },
{ "description", tool.description },
{ "parameters", json::parse(tool.parameters) },
} },
});
}
return result;
}

json common_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff) {
json delta = json::object();
if (!diff.reasoning_content_delta.empty()) {
delta["reasoning_content"] = diff.reasoning_content_delta;
}
if (!diff.content_delta.empty()) {
delta["content"] = diff.content_delta;
}
if (diff.tool_call_index != std::string::npos) {
json tool_call;
tool_call["index"] = diff.tool_call_index;
if (!diff.tool_call_delta.id.empty()) {
tool_call["id"] = diff.tool_call_delta.id;
tool_call["type"] = "function";
}
if (!diff.tool_call_delta.name.empty() || !diff.tool_call_delta.arguments.empty()) {
json function = json::object();
if (!diff.tool_call_delta.name.empty()) {
function["name"] = diff.tool_call_delta.name;
}
if (!diff.tool_call_delta.arguments.empty()) {
function["arguments"] = diff.tool_call_delta.arguments;
}
tool_call["function"] = function;
}
delta["tool_calls"] = json::array({ tool_call });
}
return delta;
}

bool common_chat_verify_template(const std::string & tmpl, bool use_jinja) {
if (use_jinja) {
try {
Expand Down
5 changes: 2 additions & 3 deletions common/chat.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,13 @@ bool common_chat_templates_support_enable_thinking(const common_chat_templates *
// Parses a JSON array of messages in OpenAI's chat completion API format.
std::vector<common_chat_msg> common_chat_msgs_parse_oaicompat(const nlohmann::ordered_json & messages);

std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const nlohmann::ordered_json & tools);

// DEPRECATED: only used in tests
nlohmann::ordered_json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg> & msgs, bool concat_typed_text = false);

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

nlohmann::ordered_json common_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff);

// get template caps, useful for reporting to server /props endpoint
std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_templates * chat_templates);

Expand Down
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS)
llama_build_and_test(test-grammar-integration.cpp)
llama_build_and_test(test-llama-grammar.cpp)
llama_build_and_test(test-chat.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
target_include_directories(test-chat PRIVATE ${PROJECT_SOURCE_DIR}/tools/server)
target_link_libraries(test-chat PRIVATE server-context)
# TODO: disabled on loongarch64 because the ggml-ci node lacks Python 3.8
if (NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "loongarch64")
llama_build_and_test(test-json-schema-to-grammar.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
Expand Down
130 changes: 128 additions & 2 deletions tests/test-chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//
#include "../src/llama-grammar.h"
#include "../src/unicode.h"
#include "../tools/server/server-chat.h"
#include "chat-auto-parser.h"
#include "chat.h"
#include "common.h"
Expand Down Expand Up @@ -1514,6 +1515,117 @@ static void test_tools_oaicompat_json_conversion() {
common_chat_tools_to_json_oaicompat({ special_function_tool }).dump(2));
}

static void test_convert_responses_to_chatcmpl() {
LOG_DBG("%s\n", __func__);

// Test basic conversion with input messages (user/assistant alternating)
{
json input = json::parse(R"({
"input": [
{
"type": "message",
"role": "user",
"content": "hi wassup"
},
{
"type": "message",
"role": "assistant",
"content": "Hey! 👋 Not much, just here ready to chat. What's up with you? Anything I can help you with today?"
},
{
"type": "message",
"role": "user",
"content": "hi"
}
],
"model": "gpt-5-mini",
"stream": false,
"text": {},
"reasoning": {
"effort": "medium"
}
})");

json result = server_chat_convert_responses_to_chatcmpl(input);

// Verify messages were converted correctly
assert_equals(true, result.contains("messages"));
assert_equals(true, result.at("messages").is_array());
assert_equals((size_t)3, result.at("messages").size());

// Check first message (user)
const auto & msg0 = result.at("messages")[0];
assert_equals(std::string("user"), msg0.at("role").get<std::string>());
assert_equals(true, msg0.at("content").is_array());
assert_equals(std::string("text"), msg0.at("content")[0].at("type").get<std::string>());
assert_equals(std::string("hi wassup"), msg0.at("content")[0].at("text").get<std::string>());

// Check second message (assistant)
const auto & msg1 = result.at("messages")[1];
assert_equals(std::string("assistant"), msg1.at("role").get<std::string>());
assert_equals(true, msg1.at("content").is_array());
assert_equals(std::string("text"), msg1.at("content")[0].at("type").get<std::string>());
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>());

// Check third message (user)
const auto & msg2 = result.at("messages")[2];
assert_equals(std::string("user"), msg2.at("role").get<std::string>());
assert_equals(true, msg2.at("content").is_array());
assert_equals(std::string("text"), msg2.at("content")[0].at("type").get<std::string>());
assert_equals(std::string("hi"), msg2.at("content")[0].at("text").get<std::string>());

// Verify other fields preserved
assert_equals(std::string("gpt-5-mini"), result.at("model").get<std::string>());
assert_equals(false, result.at("stream").get<bool>());
}

// Test string input
{
json input = json::parse(R"({
"input": "Hello, world!",
"model": "test-model"
})");

json result = server_chat_convert_responses_to_chatcmpl(input);

assert_equals((size_t)1, result.at("messages").size());
const auto & msg = result.at("messages")[0];
assert_equals(std::string("user"), msg.at("role").get<std::string>());
assert_equals(std::string("Hello, world!"), msg.at("content").get<std::string>());
}

// Test with instructions (system message)
{
json input = json::parse(R"({
"input": "Hello",
"instructions": "You are a helpful assistant.",
"model": "test-model"
})");

json result = server_chat_convert_responses_to_chatcmpl(input);

assert_equals((size_t)2, result.at("messages").size());
const auto & sys_msg = result.at("messages")[0];
assert_equals(std::string("system"), sys_msg.at("role").get<std::string>());
assert_equals(std::string("You are a helpful assistant."), sys_msg.at("content").get<std::string>());
}

// Test with max_output_tokens conversion
{
json input = json::parse(R"({
"input": "Hello",
"model": "test-model",
"max_output_tokens": 100
})");

json result = server_chat_convert_responses_to_chatcmpl(input);

assert_equals(true, result.contains("max_tokens"));
assert_equals(false, result.contains("max_output_tokens"));
assert_equals(100, result.at("max_tokens").get<int>());
}
}

static void test_template_output_peg_parsers(bool detailed_debug) {
LOG_DBG("%s\n", __func__);

Expand Down Expand Up @@ -4197,7 +4309,7 @@ int main(int argc, char ** argv) {
bool detailed_debug = false;
bool only_run_filtered = false;

// Check for --template flag
// Check for --template and --detailed flags
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "--template" && i + 1 < argc) {
Expand All @@ -4222,7 +4334,20 @@ int main(int argc, char ** argv) {
}

#ifndef _WIN32
if (argc > 1) {
// Check if any argument is a .jinja file (for template format detection mode)
bool has_jinja_files = false;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "--detailed") {
continue;
}
if (arg.size() >= 6 && arg.rfind(".jinja") == arg.size() - 6) {
has_jinja_files = true;
break;
}
}

if (has_jinja_files) {
common_chat_templates_inputs inputs;
common_chat_msg msg;
msg.role = "user";
Expand Down Expand Up @@ -4255,6 +4380,7 @@ int main(int argc, char ** argv) {
test_msg_diffs_compute();
test_msgs_oaicompat_json_conversion();
test_tools_oaicompat_json_conversion();
test_convert_responses_to_chatcmpl();
test_developer_role_to_system_workaround();
test_template_output_peg_parsers(detailed_debug);
std::cout << "\n[chat] All tests passed!" << '\n';
Expand Down
2 changes: 2 additions & 0 deletions tools/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
set(TARGET server-context)

add_library(${TARGET} STATIC
server-chat.cpp
server-chat.h
server-task.cpp
server-task.h
server-queue.cpp
Expand Down
Loading
Loading