Commit 1945793e0 for llama.cpp
commit 1945793e00c53f69eac0bfc89e37429a5503f12a
Author: Ethan Lin <linsen458@gmail.com>
Date: Mon Nov 9 21:10:11 2026 +0800
server : fix response_format json_schema when schema is not nested in the json_schema wrapper (#28697)
* server : accept top-level schema in response_format with type json_schema
The OpenAI-compatible chat completions endpoint only applied the schema
when it was nested inside response_format.json_schema.schema. When the
schema was provided directly as response_format.schema (as documented in
the server README, and used by some clients), it was silently ignored
and generation ran unconstrained.
Also mention json_schema in the error message for invalid response_format
types, document the OpenAI wrapper form in the README, and add a test
case for the top-level schema form.
Fixes #10732
* server : let the json_schema wrapper take precedence by presence
Pick the schema source based on whether response_format has a
json_schema key, instead of falling back when the parsed wrapper schema
is empty. An empty schema {} is valid (any JSON value), so the old check
let a top-level schema override an explicit {} in the wrapper. Use
json_value() for the top-level schema too, like the rest of the
function, and add tests for the precedence.
---------
Co-authored-by: linsen <251731047+linsen458-spec@users.noreply.github.com>
diff --git a/tools/server/README.md b/tools/server/README.md
index 562e8dd9d..dad14aaf1 100644
--- a/tools/server/README.md
+++ b/tools/server/README.md
@@ -1446,7 +1446,7 @@ Given a ChatML-formatted json description in `messages`, it returns the predicte
See [OpenAI Chat Completions API documentation](https://platform.openai.com/docs/api-reference/chat). llama.cpp `/completion`-specific features such as `mirostat` are also supported.
-The `response_format` parameter supports both plain JSON output (e.g. `{"type": "json_object"}`) and schema-constrained JSON (e.g. `{"type": "json_object", "schema": {"type": "string", "minLength": 10, "maxLength": 100}}` or `{"type": "json_schema", "schema": {"properties": { "name": { "title": "Name", "type": "string" }, "date": { "title": "Date", "type": "string" }, "participants": { "items": {"type: "string" }, "title": "Participants", "type": "string" } } } }`), similar to other OpenAI-inspired API providers.
+The `response_format` parameter supports both plain JSON output (e.g. `{"type": "json_object"}`) and schema-constrained JSON (e.g. `{"type": "json_object", "schema": {"type": "string", "minLength": 10, "maxLength": 100}}` or `{"type": "json_schema", "schema": {"properties": { "name": { "title": "Name", "type": "string" }, "date": { "title": "Date", "type": "string" }, "participants": { "items": {"type: "string" }, "title": "Participants", "type": "string" } } } }`), similar to other OpenAI-inspired API providers. The OpenAI wrapper form `{"type": "json_schema", "json_schema": {"name": ..., "strict": ..., "schema": {...}}}` is also accepted.
`chat_template_kwargs`: Allows sending additional parameters to the json templating system. For example: `{"enable_thinking": false}`
diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp
index 311eb4f12..05fd04c29 100644
--- a/tools/server/server-common.cpp
+++ b/tools/server/server-common.cpp
@@ -1390,10 +1390,14 @@ json oaicompat_chat_params_parse(
json_schema = json_value(response_format, "schema", json::object());
}
} else if (response_type == "json_schema") {
- auto schema_wrapper = json_value(response_format, "json_schema", json::object());
- json_schema = json_value(schema_wrapper, "schema", json::object());
+ if (response_format.contains("json_schema")) {
+ auto schema_wrapper = json_value(response_format, "json_schema", json::object());
+ json_schema = json_value(schema_wrapper, "schema", json::object());
+ } else {
+ json_schema = json_value(response_format, "schema", json::object());
+ }
} else if (!response_type.empty() && response_type != "text") {
- throw std::invalid_argument("response_format type must be one of \"text\" or \"json_object\", but got: " + response_type);
+ throw std::invalid_argument("response_format type must be one of \"text\", \"json_object\" or \"json_schema\", but got: " + response_type);
}
}
diff --git a/tools/server/tests/unit/test_chat_completion.py b/tools/server/tests/unit/test_chat_completion.py
index afbcef030..380e0d1f2 100644
--- a/tools/server/tests/unit/test_chat_completion.py
+++ b/tools/server/tests/unit/test_chat_completion.py
@@ -238,6 +238,10 @@ def test_apply_chat_template():
({"type": "json_object", "schema": {"const": "42"}}, 6, "\"42\""),
({"type": "json_object", "schema": {"items": [{"type": "integer"}]}}, 10, "[ -3000 ]"),
({"type": "json_schema", "json_schema": {"schema": {"const": "foooooo"}}}, 10, "\"foooooo\""),
+ ({"type": "json_schema", "schema": {"const": "foooooo"}}, 10, "\"foooooo\""),
+ # the json_schema wrapper takes precedence over a top-level schema, even when its schema is {}
+ ({"type": "json_schema", "json_schema": {"schema": {"const": "foooooo"}}, "schema": {"const": "42"}}, 10, "\"foooooo\""),
+ ({"type": "json_schema", "json_schema": {"schema": {}}, "schema": {"type": "hiccup"}}, 10, ".+"),
({"type": "json_object"}, 10, "(\\{|John)+"),
({"type": "sound"}, 0, None),
# invalid response format (expected to fail)