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");