From 1549bcee252cb24bf83242a460d5e2bffa4ebf6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Tue, 20 Jan 2026 16:22:02 +0100 Subject: [PATCH 1/2] server: fix json_schema response_format handling (#10732) - Add validation that `json_schema.schema` is present when using `response_format.type: "json_schema"`. Previously, a missing schema would silently result in an empty schema `{}` being used, which matches any JSON and doesn't constrain the output. - Update the error message for invalid response_format types to include "json_schema" as a valid option (was only listing "text" and "json_object"). Fixes: https://github.com/ggml-org/llama.cpp/issues/10732 --- tools/server/server-common.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp index 1bbe85322a..584f7fbb45 100644 --- a/tools/server/server-common.cpp +++ b/tools/server/server-common.cpp @@ -870,10 +870,15 @@ json oaicompat_chat_params_parse( if (response_type == "json_object") { json_schema = json_value(response_format, "schema", json::object()); } else if (response_type == "json_schema") { + // https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format + // OpenAI expects: response_format.json_schema.schema auto schema_wrapper = json_value(response_format, "json_schema", json::object()); - json_schema = json_value(schema_wrapper, "schema", json::object()); + if (!schema_wrapper.contains("schema")) { + throw std::invalid_argument("response_format type \"json_schema\" requires \"json_schema.schema\" to be set"); + } + json_schema = schema_wrapper.at("schema"); } 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); } } From cc7569dbdb73848397cf6d8c20801c14637e0bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Tue, 20 Jan 2026 17:09:38 +0100 Subject: [PATCH 2/2] server: add tests for json_schema response_format validation - Add test cases for OpenAI-style json_schema format with name/strict fields - Add test cases for missing json_schema.schema field (should fail with 400) - Add dedicated test to verify error messages mention proper field names --- .../server/tests/unit/test_chat_completion.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tools/server/tests/unit/test_chat_completion.py b/tools/server/tests/unit/test_chat_completion.py index d56a930f7c..108a890318 100644 --- a/tools/server/tests/unit/test_chat_completion.py +++ b/tools/server/tests/unit/test_chat_completion.py @@ -176,12 +176,18 @@ 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\""), + # json_schema with name field (OpenAI-style) + ({"type": "json_schema", "json_schema": {"name": "test", "schema": {"const": "bar"}, "strict": True}}, 6, "\"bar\""), ({"type": "json_object"}, 10, "(\\{|John)+"), ({"type": "sound"}, 0, None), # invalid response format (expected to fail) ({"type": "json_object", "schema": 123}, 0, None), ({"type": "json_object", "schema": {"type": 123}}, 0, None), ({"type": "json_object", "schema": {"type": "hiccup"}}, 0, None), + # json_schema missing required json_schema.schema field (should fail) + ({"type": "json_schema", "json_schema": {"name": "test"}}, 0, None), + ({"type": "json_schema", "json_schema": {}}, 0, None), + ({"type": "json_schema"}, 0, None), ]) def test_completion_with_response_format(response_format: dict, n_predicted: int, re_content: str | None): global server @@ -203,6 +209,31 @@ def test_completion_with_response_format(response_format: dict, n_predicted: int assert "error" in res.body +@pytest.mark.parametrize("response_format,expected_error_message", [ + # json_schema type requires json_schema.schema to be set + ({"type": "json_schema", "json_schema": {"name": "test"}}, "json_schema.schema"), + ({"type": "json_schema", "json_schema": {}}, "json_schema.schema"), + ({"type": "json_schema"}, "json_schema.schema"), + # invalid response_format type should mention valid options + ({"type": "invalid_type"}, "json_schema"), +]) +def test_response_format_error_messages(response_format: dict, expected_error_message: str): + """Test that invalid response_format configurations return helpful error messages.""" + global server + server.start() + res = server.make_request("POST", "/chat/completions", data={ + "max_tokens": 10, + "messages": [ + {"role": "user", "content": "test"}, + ], + "response_format": response_format, + }) + assert res.status_code == 400 + assert "error" in res.body + assert expected_error_message in res.body["error"]["message"], \ + f"Expected '{expected_error_message}' in error message, got: {res.body['error']['message']}" + + @pytest.mark.parametrize("jinja,json_schema,n_predicted,re_content", [ (False, {"const": "42"}, 6, "\"42\""), (True, {"const": "42"}, 6, "\"42\""),