Commit 711f9f4c1 for llama.cpp
commit 711f9f4c117042570ef66ef58fba1f15835a331b
Author: Matt Thompson <111157855+boondocklabs@users.noreply.github.com>
Date: Mon Nov 9 16:43:38 2026 -0700
server: responses api: support conversion of text.format.json_schema to completions (#26145)
diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp
index 4d10815e5..60090d5c9 100644
--- a/tests/test-chat.cpp
+++ b/tests/test-chat.cpp
@@ -2043,6 +2043,55 @@ static void test_convert_responses_to_chatcmpl() {
assert_equals(false, result.contains("tools"));
}
+
+ // Test JSON schema conversion
+ {
+ json input = json::parse(R"({
+ "input": "Hello",
+ "model": "test-model",
+ "text": {
+ "format": {
+ "type": "json_schema",
+ "name": "TestOutput",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {"type": "string"}
+ },
+ "required": ["message"],
+ "additionalProperties": false
+ },
+ "strict": true
+ }
+ }
+ })");
+
+ json result = server_chat_convert_responses_to_chatcmpl(input);
+
+ assert_equals(false, result.contains("text"));
+ assert_equals(true, result.contains("response_format"));
+ const auto & response_format = result.at("response_format");
+ assert_equals(std::string("json_schema"), response_format.at("type").get<std::string>());
+ const auto & json_schema = response_format.at("json_schema");
+ assert_equals(std::string("TestOutput"), json_schema.at("name").get<std::string>());
+ assert_equals(true, json_schema.at("strict").get<bool>());
+ assert_equals(std::string("object"), json_schema.at("schema").at("type").get<std::string>());
+ assert_equals(false, json_schema.contains("type"));
+
+ input["extra_params"] = {{"seed", 42}, {"min_p", 0.1}};
+ json result_extra = server_chat_convert_responses_to_chatcmpl(input);
+
+ assert_equals(false, result_extra.contains("text"));
+ assert_equals(false, result_extra.contains("extra_params"));
+ assert_equals(42, result_extra.at("seed").get<int>());
+ assert_equals(0.1, result_extra.at("min_p").get<double>());
+ assert_equals(response_format.dump(), result_extra.at("response_format").dump());
+
+ input["extra_params"]["response_format"] = {{"type", "text"}};
+ result_extra = server_chat_convert_responses_to_chatcmpl(input);
+
+ assert_equals(input.at("extra_params").at("response_format").dump(), result_extra.at("response_format").dump());
+ }
}
// Shared LFM2 parser cases - all variants use one output format and parser
diff --git a/tools/server/server-chat.cpp b/tools/server/server-chat.cpp
index 8cf883a3a..7b080d17a 100644
--- a/tools/server/server-chat.cpp
+++ b/tools/server/server-chat.cpp
@@ -311,6 +311,34 @@ json server_chat_convert_responses_to_chatcmpl(const json & response_body) {
chatcmpl_body.erase("reasoning");
}
+ if (response_body.contains("text")) {
+ const json & text = response_body.at("text");
+ if (!text.is_object()) {
+ throw std::invalid_argument("'text' must be an object");
+ }
+
+ chatcmpl_body.erase("text");
+ if (text.contains("format")) {
+ const json & format = text.at("format");
+ if (!format.is_object()) {
+ throw std::invalid_argument("'text.format' must be an object");
+ }
+
+ const std::string type = json_value(format, "type", std::string());
+ if (type == "json_schema") {
+ chatcmpl_body["response_format"] = {
+ {"type", "json_schema"},
+ {"json_schema", format},
+ };
+ chatcmpl_body["response_format"]["json_schema"].erase("type");
+ } else if (type == "json_object" || type == "text") {
+ chatcmpl_body["response_format"] = format;
+ } else if (!type.empty()) {
+ throw std::invalid_argument("'text.format.type' must be one of 'text', 'json_object', or 'json_schema'");
+ }
+ }
+ }
+
// llama.cpp params (e.g. seed, min_p) go under extra_params and are merged into the converted body
if (response_body.contains("extra_params")) {
chatcmpl_body.erase("extra_params");