From f0b8389957bae8749d9e191f11cb07c6bebbea1d Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Sat, 17 Oct 2026 13:21:35 -0500 Subject: [PATCH 1/4] grammar: preserve unions in transparent singleton allOf Assisted-by: Codex --- common/json-schema-to-grammar.cpp | 21 +++++++++++++++++++++ tests/test-chat.cpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index 0aee51b26e84..e3a2c4608a29 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -964,6 +964,27 @@ class common_schema_converter { properties, required, name, schema.contains("additionalProperties") ? schema["additionalProperties"] : json())); } + // A singleton union wrapper adds no constraint unless a sibling does. + if (schema.contains("allOf") && schema["allOf"].is_array() && schema["allOf"].size() == 1) { + bool transparent = true; + for (const auto & item : schema.items()) { + const auto & key = item.key(); + if (key != "allOf" && key != "description" && key != "title" && key != "$comment" && + key != "default" && key != "examples" && key != "definitions" && key != "$defs") { + transparent = false; + } + } + const json * component = &schema["allOf"][0]; + if (component->is_object() && component->contains("$ref")) { + auto ref = _refs.find((*component)["$ref"]); + if (ref != _refs.end() && component->size() == 1) { + component = &ref->second; + } + } + if (transparent && (component->contains("oneOf") || component->contains("anyOf"))) { + return visit(*component, name); + } + } if ((schema_type.is_null() || schema_type == "object" || schema_type == "string") && schema.contains("allOf")) { std::unordered_set required; std::vector> properties; diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 2c28892f54ec..6f45d1865be1 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -1,3 +1,4 @@ +#include "json-schema-to-grammar.h" // Tests chat handling, including grammar genration and parsing for tool calling, for various templates. // // Also acts as a CLI to generate a Markdown summary of the formats of Jinja templates, @@ -2116,6 +2117,34 @@ static void test_template_output_peg_parsers(bool detailed_debug) { // Qwen3.5 (basically same as Nemotron, but keeping separate tests just in case) auto tst = peg_tester("models/templates/Qwen3.5-4B.jinja", detailed_debug); + // Captured tagged edit schema: allOf -> local reference -> oneOf. + const common_chat_tool tagged_edit_tool{ + "edit_file", "Edit an existing file", R"SCHEMA({"definitions":{"EditMode":{"description":"How to edit a file (four modes).","oneOf":[{"description":"Replace content between line numbers (1-indexed, inclusive).","properties":{"end_line":{"format":"uint32","minimum":0.0,"type":"integer"},"new_content":{"type":"string"},"start_line":{"format":"uint32","minimum":1.0,"type":"integer"},"type":{"enum":["line_range"],"type":"string"}},"required":["end_line","new_content","start_line","type"],"type":"object"},{"description":"Find text and replace it.","properties":{"all":{"default":false,"type":"boolean"},"replace":{"type":"string"},"search":{"type":"string"},"type":{"enum":["search_replace"],"type":"string"}},"required":["replace","search","type"],"type":"object"},{"description":"Insert content at a specific line (pushes existing lines down).","properties":{"content":{"type":"string"},"line":{"format":"uint32","minimum":1.0,"type":"integer"},"type":{"enum":["insert_at"],"type":"string"}},"required":["content","line","type"],"type":"object"},{"description":"Append content to end of file.","properties":{"content":{"type":"string"},"type":{"enum":["append"],"type":"string"}},"required":["content","type"],"type":"object"}]}},"properties":{"description":{"description":"Optional note recorded in change history.","type":["string","null"]},"edit_mode":{"allOf":[{"$ref":"#/definitions/EditMode"}],"description":"Operation to apply."},"file_path":{"description":"Path relative to the workspace root.","type":"string"}},"required":["edit_mode","file_path"],"type":"object"})SCHEMA" + }; + const auto tagged_schema = common_json::parse(tagged_edit_tool.parameters); + const auto tagged_grammar = json_schema_to_grammar(tagged_schema); + const std::string edit_args = R"({"edit_mode":{"replace":"new","search":"old","type":"search_replace"},"file_path":"file.rs"})"; + assert(match_string(edit_args, build_grammar(tagged_grammar).get())); + assert(match_string(R"({"edit_mode":{"content":"new","type":"append"},"file_path":"file.rs"})", + build_grammar(tagged_grammar).get())); + assert(!match_string(R"({"edit_mode":{},"file_path":"file.rs"})", build_grammar(tagged_grammar).get())); + assert(!match_string(R"({"edit_mode":{"type":"unknown"},"file_path":"file.rs"})", + build_grammar(tagged_grammar).get())); + auto closed = tagged_schema; + closed["properties"]["edit_mode"]["additionalProperties"] = false; + assert(!match_string(edit_args, build_grammar(json_schema_to_grammar(closed)).get())); + auto constrained = tagged_schema; + constrained["properties"]["edit_mode"]["properties"] = {{"guard", {{"const", "kept"}}}}; + constrained["properties"]["edit_mode"]["required"] = {"guard"}; + assert(!match_string(edit_args, build_grammar(json_schema_to_grammar(constrained)).get())); + tst.test("\n\n" + "\n{\"replace\":\"new\",\"search\":\"old\",\"type\":\"search_replace\"}\n\n" + "\nfile.rs\n\n\n") + .enable_thinking(false) + .tools({tagged_edit_tool}) + .expect_tool_calls({{"edit_file", edit_args, {}}}) + .run(); + tst.test("I'm\nthinking\n\n\nHello, world!\nWhat's up?") .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .enable_thinking(true) From d3e6279d6646bb0af643480d9d2fb0c415ee5b76 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Sat, 17 Oct 2026 13:35:31 -0500 Subject: [PATCH 2/4] test: keep tagged edit grammar checks active in Release Assisted-by: Codex --- tests/test-chat.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 6f45d1865be1..41f019680677 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -1,4 +1,3 @@ -#include "json-schema-to-grammar.h" // Tests chat handling, including grammar genration and parsing for tool calling, for various templates. // // Also acts as a CLI to generate a Markdown summary of the formats of Jinja templates, @@ -15,6 +14,7 @@ #include "common.h" #include "ggml.h" #include "log.h" +#include "json-schema-to-grammar.h" #include #include @@ -2124,23 +2124,24 @@ static void test_template_output_peg_parsers(bool detailed_debug) { const auto tagged_schema = common_json::parse(tagged_edit_tool.parameters); const auto tagged_grammar = json_schema_to_grammar(tagged_schema); const std::string edit_args = R"({"edit_mode":{"replace":"new","search":"old","type":"search_replace"},"file_path":"file.rs"})"; - assert(match_string(edit_args, build_grammar(tagged_grammar).get())); - assert(match_string(R"({"edit_mode":{"content":"new","type":"append"},"file_path":"file.rs"})", + assert_equals(true, match_string(edit_args, build_grammar(tagged_grammar).get())); + assert_equals(true, match_string(R"({"edit_mode":{"content":"new","type":"append"},"file_path":"file.rs"})", build_grammar(tagged_grammar).get())); - assert(!match_string(R"({"edit_mode":{},"file_path":"file.rs"})", build_grammar(tagged_grammar).get())); - assert(!match_string(R"({"edit_mode":{"type":"unknown"},"file_path":"file.rs"})", + assert_equals(false, match_string(R"({"edit_mode":{},"file_path":"file.rs"})", build_grammar(tagged_grammar).get())); + assert_equals(false, match_string(R"({"edit_mode":{"type":"unknown"},"file_path":"file.rs"})", build_grammar(tagged_grammar).get())); auto closed = tagged_schema; closed["properties"]["edit_mode"]["additionalProperties"] = false; - assert(!match_string(edit_args, build_grammar(json_schema_to_grammar(closed)).get())); + assert_equals(false, match_string(edit_args, build_grammar(json_schema_to_grammar(closed)).get())); auto constrained = tagged_schema; constrained["properties"]["edit_mode"]["properties"] = {{"guard", {{"const", "kept"}}}}; constrained["properties"]["edit_mode"]["required"] = {"guard"}; - assert(!match_string(edit_args, build_grammar(json_schema_to_grammar(constrained)).get())); + assert_equals(false, match_string(edit_args, build_grammar(json_schema_to_grammar(constrained)).get())); tst.test("\n\n" "\n{\"replace\":\"new\",\"search\":\"old\",\"type\":\"search_replace\"}\n\n" "\nfile.rs\n\n\n") .enable_thinking(false) + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .tools({tagged_edit_tool}) .expect_tool_calls({{"edit_file", edit_args, {}}}) .run(); From 032e2bc97f15fe3450eb8e5966b24d7e5cced568 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Sat, 17 Oct 2026 13:42:03 -0500 Subject: [PATCH 3/4] fix: initialize speculative batch label weights explicitly Assisted-by: Codex --- common/speculative.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/speculative.cpp b/common/speculative.cpp index 321c6de3effb..4b67bbb88599 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -634,6 +634,7 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl { /*.seq_id =*/ nullptr, /*.logits =*/ nullptr, /*.label =*/ nullptr, + /*.label_weight =*/ nullptr, }; const int32_t rc = llama_encode(ctx_dft, enc_batch); if (rc != 0) { @@ -1128,6 +1129,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { /*.seq_id =*/ nullptr, /*.logits =*/ nullptr, /*.label =*/ nullptr, + /*.label_weight =*/ nullptr, }; int32_t rc = llama_encode(ctx_dft, enc_batch); From 50b02b8bb55102665500327b2152fe7e3348e1bc Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Sat, 17 Oct 2026 13:58:54 -0500 Subject: [PATCH 4/4] fix: build private walk fixture under supported library linkage Assisted-by: Codex --- tests/CMakeLists.txt | 7 +++---- tests/test-walk-exact.cpp | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b222eaf134da..498b70553bba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -150,10 +150,6 @@ endif () llama_build(test-recurrent-state-rollback.cpp) -# the exact walk trains on the window's whole gradient whatever the chunking: run on a pure-attention AND a -# hybrid model (-m), on a GPU backend as well as the CPU (a hybrid passed on the CPU while Metal tore its state) -llama_build(test-walk-exact.cpp) - # GGML_TENSOR_FLAG_GRAD: a leaf whose gradient the backward computes, never optimized (the walk reads the cached prefix K/V gradient) llama_build_and_test(test-grad-leaf.cpp) @@ -170,6 +166,9 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS) llama_build_and_test(test-llama-grammar.cpp) llama_build_and_test(test-batch-alloc.cpp) llama_build_and_test(test-walk-plan.cpp) + # the exact walk trains on the window's whole gradient whatever the chunking: run on a pure-attention AND a + # hybrid model (-m), on a GPU backend as well as the CPU (a hybrid passed on the CPU while Metal tore its state) + llama_build(test-walk-exact.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) diff --git a/tests/test-walk-exact.cpp b/tests/test-walk-exact.cpp index a75c647d3b4e..a1ebbb7140b6 100644 --- a/tests/test-walk-exact.cpp +++ b/tests/test-walk-exact.cpp @@ -829,7 +829,7 @@ int main(int argc, char ** argv) { GGML_ASSERT(llama_state_seq_set_data(ctx, generation_prefix.data(), generation_prefix.size(), 0) == generation_prefix.size()); llama_batch retained = llama_batch_init(WINDOW - r0, 0, 1); common_batch_clear(retained); - for (int32_t i = 0; i < WINDOW - r0; ++i) { + for (uint32_t i = 0; i < WINDOW - r0; ++i) { common_batch_add(retained, tokens[r0 + i], r0 + i, {0}, true); retained.label[i] = labels[i]; }